@zergai/ztc 0.2.6-dev.36102446685 → 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.
- package/README.md +43 -17
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +19 -5
- package/dist/agent/agent.d.ts +5 -1
- package/dist/agent/agent.js +51 -8
- package/dist/agent/backends/account_pool.d.ts +5 -6
- package/dist/agent/backends/account_pool.js +39 -14
- package/dist/agent/backends/account_rotation.d.ts +3 -0
- package/dist/agent/backends/account_rotation.js +14 -3
- package/dist/agent/backends/codex_account_status.d.ts +42 -0
- package/dist/agent/backends/codex_account_status.js +196 -0
- package/dist/agent/backends/codex_app_server.d.ts +12 -2
- package/dist/agent/backends/codex_app_server.js +66 -6
- package/dist/agent/backends/index.d.ts +2 -0
- package/dist/agent/backends/index.js +13 -5
- package/dist/agent/backends/openai.d.ts +2 -2
- package/dist/agent/backends/openai.js +4 -3
- package/dist/agent/backends/openai_compatible.js +10 -10
- package/dist/agent/backends/openai_responses.d.ts +17 -5
- package/dist/agent/backends/openai_responses.js +2 -4
- package/dist/agent/backends/token_usage.js +1 -1
- package/dist/agent/backends/types.d.ts +3 -0
- package/dist/agent/commands/accounts.d.ts +7 -1
- package/dist/agent/commands/accounts.js +299 -101
- package/dist/agent/commands/config.js +11 -3
- package/dist/agent/commands/config_profile_switch.d.ts +2 -0
- package/dist/agent/commands/config_profile_switch.js +22 -0
- package/dist/agent/commands/context.js +4 -3
- package/dist/agent/commands/index.js +1 -2
- package/dist/agent/commands/input_mode.js +1 -1
- package/dist/agent/commands/model.js +7 -2
- package/dist/agent/commands/plan.d.ts +12 -0
- package/dist/agent/commands/plan.js +39 -28
- package/dist/agent/commands/profile.d.ts +1 -1
- package/dist/agent/commands/profile.js +34 -257
- package/dist/agent/commands/provider_choices.d.ts +12 -1
- package/dist/agent/commands/provider_choices.js +29 -2
- package/dist/agent/commands/provider_selection.d.ts +3 -0
- package/dist/agent/commands/provider_selection.js +63 -0
- package/dist/agent/commands/settings.d.ts +2 -0
- package/dist/agent/commands/settings.js +655 -50
- package/dist/agent/commands/types.d.ts +19 -2
- package/dist/agent/tools/ask_user.js +2 -2
- package/dist/app/agent_runner.d.ts +1 -1
- package/dist/app/agent_runner.js +2 -2
- package/dist/app/ask_user_response.d.ts +3 -0
- package/dist/app/ask_user_response.js +16 -0
- package/dist/app/command_context.js +29 -13
- package/dist/app/controller.d.ts +17 -2
- package/dist/app/controller.js +24 -1
- package/dist/app/selection_completion.d.ts +9 -3
- package/dist/app/selection_completion.js +54 -22
- package/dist/cli_args.js +10 -1
- package/dist/cli_main.js +85 -2
- package/dist/cli_ratatui.d.ts +22 -4
- package/dist/cli_ratatui.js +421 -146
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +20 -0
- package/dist/config/profiles.d.ts +9 -0
- package/dist/config/profiles.js +87 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +283 -40
- package/dist/headless.js +14 -0
- package/dist/panes/model_catalog.d.ts +1 -1
- package/dist/panes/model_catalog.js +24 -2
- package/dist/panes/ztc_launch.js +6 -0
- package/dist/router_auth.js +3 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +3 -0
- package/dist/types.d.ts +23 -2
- package/dist/ui/views/app.d.ts +3 -1
- package/dist/ui/views/app.js +3 -2
- package/dist/ui/views/completion_menu.js +6 -6
- package/dist/ui/views/input_area.d.ts +2 -1
- package/dist/ui/views/input_area.js +9 -8
- package/dist/ui/views/status_bar.d.ts +2 -0
- package/dist/ui/views/status_bar.js +15 -4
- package/dist/ui/widgets/color_panel.d.ts +9 -0
- package/dist/ui/widgets/color_panel.js +41 -26
- package/dist/ui/widgets/navigator_panel.d.ts +9 -0
- package/dist/ui/widgets/navigator_panel.js +17 -16
- package/dist/ui/widgets/panel_frame.d.ts +83 -3
- package/dist/ui/widgets/panel_frame.js +107 -9
- package/dist/ui/widgets/questionnaire.d.ts +9 -0
- package/dist/ui/widgets/questionnaire.js +214 -16
- package/dist/ui/widgets/resume_panel.d.ts +9 -0
- package/dist/ui/widgets/resume_panel.js +18 -17
- package/dist/ui/widgets/rewind_panel.d.ts +9 -0
- package/dist/ui/widgets/rewind_panel.js +15 -16
- package/dist/ui/widgets/select.d.ts +6 -0
- package/dist/ui/widgets/select.js +47 -12
- package/dist/ui/widgets/settings_panel.d.ts +75 -0
- package/dist/ui/widgets/settings_panel.js +197 -39
- package/dist/ui/widgets/status_line_panel.d.ts +9 -0
- package/dist/ui/widgets/status_line_panel.js +18 -19
- package/dist/ui/widgets/transcript_preview.d.ts +8 -0
- package/dist/ui/widgets/transcript_preview.js +10 -0
- package/dist/utils/context.js +2 -0
- package/dist/utils/fireworks.d.ts +20 -0
- package/dist/utils/fireworks.js +23 -0
- package/dist/utils/models.d.ts +2 -1
- package/dist/utils/models.js +19 -0
- package/dist/web/accounts_mirror.d.ts +7 -0
- package/dist/web/accounts_mirror.js +59 -7
- package/dist/web/mirror_stream.js +19 -5
- package/dist/zlm-embed.mjs +5734 -5362
- package/package.json +2 -2
- package/dist/agent/commands/provider.d.ts +0 -2
- package/dist/agent/commands/provider.js +0 -75
package/README.md
CHANGED
|
@@ -51,25 +51,26 @@ you are ready for ZTC to make the proposed changes.
|
|
|
51
51
|
|
|
52
52
|
Authentication options are Zerg Router browser login (`/auth login`), a provider API key
|
|
53
53
|
(`/config key <api-key>`), or Claude Code OAuth (`claude setup-token`, then
|
|
54
|
-
`/
|
|
54
|
+
`/model claude_code`).
|
|
55
55
|
|
|
56
56
|
For browser login at **zergrouter.com**, run `ztc auth login` in your shell or
|
|
57
57
|
`/auth login` inside ZTC. Both use the same flow and credentials. See
|
|
58
58
|
[Zerg Router and DeepSeek setup](docs/zerg-router.md) for model selection,
|
|
59
59
|
Claude Code/Codex emulation, and `ztc "prompt"` or `ztc -p "prompt"` usage.
|
|
60
60
|
|
|
61
|
-
Use `/
|
|
62
|
-
local keys. Use `/
|
|
63
|
-
Router login, or `/
|
|
61
|
+
Use `/model openai [model]` or `/model deepseek [model]` for direct API calls with your
|
|
62
|
+
local keys. Use `/model zergrouter <upstream-provider> [model]` to route through your
|
|
63
|
+
Router login, or `/model modal [model]` for Modal shared endpoints. For Ollama,
|
|
64
64
|
LM Studio, or a custom server, configure `/config openai-compat` and select
|
|
65
|
-
`/
|
|
65
|
+
`/model local [model]`. Switching providers selects an appropriate default model;
|
|
66
66
|
selecting the same provider preserves your current model.
|
|
67
67
|
|
|
68
|
-
Type `/
|
|
68
|
+
Type `/model ` to search providers and current-provider models by ID, name, or alias.
|
|
69
69
|
Search is fuzzy, so `/model dskfl` finds DeepSeek Flash. Use Up/Down to select,
|
|
70
|
-
Tab or Enter to complete
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
Tab or Enter to complete a provider and continue to its models. Enter on a model
|
|
71
|
+
suggestion fills its ID; press Enter again to apply it. Escape dismisses suggestions.
|
|
72
|
+
Custom model IDs can still be entered directly.
|
|
73
|
+
After a provider is entered, the picker names it and lists only its models. A successful
|
|
73
74
|
live catalog replaces bundled suggestions, including when the catalog is empty;
|
|
74
75
|
bundled suggestions are used while loading or when discovery fails.
|
|
75
76
|
Modal lists registered aliases, such as `kimi-k3` and `deepseek-v4.1-flash`;
|
|
@@ -567,11 +568,17 @@ When a saved session includes `zmux` state, `ztc resume` restores the pane layou
|
|
|
567
568
|
| `/config statusline <left|right> <field,...>` | Set status line fields without opening the editor |
|
|
568
569
|
| `/config statusline variant <field> <raw\|short\|long>` | Set one field's verbosity without opening the editor |
|
|
569
570
|
| `/config statusline edit` | Open the status line field editor |
|
|
570
|
-
| `/provider [
|
|
571
|
-
| `/model <
|
|
571
|
+
| `/model <provider> [model]` | Select a provider and optional model; `/model <model>` keeps the active provider |
|
|
572
|
+
| `/model zergrouter <provider> [model]` | Select a Router provider and optional model |
|
|
572
573
|
| `/models` | List available models |
|
|
573
574
|
| `/permissions` | View/update tool permissions |
|
|
574
575
|
|
|
576
|
+
`/profile list` and `/settings` → **Config Scope** → **Active config profile** show one profile catalog. The built-in `ztc`, `ztc-coder`, `ztc-datascience`, and `scientist` profiles share the default settings and sign-ins; each selects its named emulation layer. They work without setup. An existing saved directory with one of those names remains an independent config profile, so its data stays accessible.
|
|
577
|
+
|
|
578
|
+
Custom config profiles keep settings and sign-ins separate. Start one with `ztc --profile work`, or switch the running session with `/profile work`. `/profile create work` starts a new profile. Switching keeps the session ID and conversation while rebuilding the agent. A later `ztc resume` restores the session's saved profile unless you specify `--profile`. The default config stays at `~/.ztc/config.json`; custom named configs live at `~/.ztc/profiles/<name>/config.json`. Session history stays shared. Switching does not rewrite settings after the one-time migration described below.
|
|
579
|
+
|
|
580
|
+
On first use of this release, each existing config profile adopts the new default settings once. Its account registrations, authentication, API keys, and credential-bearing integration settings are retained. Later edits to that profile are kept. New profiles start with the same defaults. The migration writes only the active profile's config file, so switching to an unused profile applies its migration then.
|
|
581
|
+
|
|
575
582
|
#### Bottom status line
|
|
576
583
|
|
|
577
584
|
The terminal status line has independently ordered left and right fields. Values that are unavailable are omitted. In narrow terminals, lower-priority fields are hidden before mode, connection state, or active errors.
|
|
@@ -608,7 +615,21 @@ Available fields are `mode`, `session-name`, `session-id`, `workspace`, `context
|
|
|
608
615
|
|
|
609
616
|
ZTC separates the official OpenAI Codex App Server integration from the older direct ChatGPT Codex backend.
|
|
610
617
|
|
|
611
|
-
Use `ztc --provider codex_app_server` for the documented OpenAI App Server path. It launches `codex app-server` over JSONL stdio and uses App Server auth
|
|
618
|
+
Use `ztc --provider codex_app_server` for the documented OpenAI App Server path. It launches `codex app-server` over JSONL stdio and uses App Server auth, model discovery, and dynamic tool calls. Run `codex login status` before starting ZTC if the machine-default login is unclear.
|
|
619
|
+
|
|
620
|
+
To keep several ChatGPT accounts signed in for manual switching, add each as a separate `codex_app_server` profile:
|
|
621
|
+
|
|
622
|
+
```text
|
|
623
|
+
/accounts add codex_app_server work Work
|
|
624
|
+
/accounts login work
|
|
625
|
+
/accounts add codex_app_server personal Personal
|
|
626
|
+
/accounts login personal
|
|
627
|
+
/accounts use work
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Each login uses the official Codex CLI with its own `CODEX_HOME` under `~/.ztc/accounts/`, file credential storage, and ChatGPT authentication. The accounts stay signed in on disk without keeping an App Server process running for every account. `/accounts use <id>` switches the active profile; the browser Accounts panel offers the same one-click selection. Codex accounts never rotate automatically when a quota is reached, even if auto-rotation is enabled for Claude or Kimi.
|
|
631
|
+
|
|
632
|
+
The Accounts panel and `/accounts list` refresh the selected profiles through the official App Server. They show the ChatGPT identity, remaining quota windows, and token activity when OpenAI reports them. A failed quota or usage read is marked stale and retains the last observed value; an expired login needs a new `/accounts login <id>`. A profile whose login cannot be verified does not replace the current active profile. The panel's Refresh button checks again immediately. If an administrator requires keyring-only credentials, Codex may refuse the per-profile file-store setting; that profile will need a supported organization policy before it can be added to this pool.
|
|
612
633
|
|
|
613
634
|
Use `ztc --provider codex_legacy` for the existing direct `chatgpt.com/backend-api/codex` backend backed by `~/.codex/auth.json`. Existing config values with `provider: "codex"` remain supported as a compatibility alias for `codex_legacy`; new configs should use the explicit provider IDs.
|
|
614
635
|
|
|
@@ -689,7 +710,7 @@ Optional App Server config:
|
|
|
689
710
|
|
|
690
711
|
| Shortcut | Action |
|
|
691
712
|
|----------|--------|
|
|
692
|
-
| `Enter` |
|
|
713
|
+
| `Enter` | Fill a partial completion; run an exact command or model, or submit input |
|
|
693
714
|
| `Shift+Enter` | Insert newline |
|
|
694
715
|
| `Ctrl+C` | Exit |
|
|
695
716
|
| `Ctrl+L` | Clear screen |
|
|
@@ -712,10 +733,15 @@ Optional App Server config:
|
|
|
712
733
|
| `Ctrl+Y` | Paste back the most recent kill — undo a cleared prompt |
|
|
713
734
|
| `Alt+Y` / `Ctrl+Alt+Y` | With Ctrl+Y, step through earlier kills |
|
|
714
735
|
|
|
715
|
-
Slash commands
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
736
|
+
Slash commands and `/model` share a fuzzy-search menu above the prompt. It shows
|
|
737
|
+
as many choices as fit above the prompt while reserving transcript space. The
|
|
738
|
+
menu shows the result position and current model/provider. The
|
|
739
|
+
first `/model` step suggests providers and models for the active provider. Tab
|
|
740
|
+
or Enter on a provider opens its model step. Enter on a partial model fills the
|
|
741
|
+
draft; press Enter again to run it. An exact model ID runs on the first Enter.
|
|
742
|
+
Press Esc, then Enter to switch providers without
|
|
743
|
+
choosing a model. Model suggestions use the selected provider and route.
|
|
744
|
+
Commands that need an argument open that step on Enter or Tab.
|
|
719
745
|
Argument syntax appears as one short usage line instead of selectable chips.
|
|
720
746
|
|
|
721
747
|
---
|
package/bin/zmux-darwin-arm64
CHANGED
|
Binary file
|
package/bin/ztc-tui-darwin-arm64
CHANGED
|
Binary file
|
package/bin/zterm-darwin-arm64
CHANGED
|
Binary file
|
package/dist/App.js
CHANGED
|
@@ -383,7 +383,7 @@ export const App = () => {
|
|
|
383
383
|
}, []);
|
|
384
384
|
const showToastRef = useRef(null);
|
|
385
385
|
/** Returns the post-enqueue size, or null if the queue was full. */
|
|
386
|
-
const enqueueMessage = useCallback((text) => {
|
|
386
|
+
const enqueueMessage = useCallback((text, delivery = 'end-of-turn') => {
|
|
387
387
|
const current = queueRef.current;
|
|
388
388
|
if (current.length >= MAX_QUEUE) {
|
|
389
389
|
showToastRef.current?.(`Queue full (${MAX_QUEUE}). Cancel or wait.`);
|
|
@@ -393,6 +393,7 @@ export const App = () => {
|
|
|
393
393
|
id: generateId(),
|
|
394
394
|
text,
|
|
395
395
|
queuedAt: new Date(),
|
|
396
|
+
delivery,
|
|
396
397
|
};
|
|
397
398
|
const nextSize = current.length + 1;
|
|
398
399
|
setQueue(prev => (prev.length >= MAX_QUEUE ? prev : [...prev, entry]));
|
|
@@ -574,6 +575,8 @@ export const App = () => {
|
|
|
574
575
|
// are intentionally dropped on idle, as before.)
|
|
575
576
|
contextTokens: s.contextTokens,
|
|
576
577
|
contextWindow: s.contextWindow,
|
|
578
|
+
cacheInputTokens: s.cacheInputTokens,
|
|
579
|
+
cachedInputTokens: s.cachedInputTokens,
|
|
577
580
|
tokensEstimated: s.tokensEstimated,
|
|
578
581
|
authError: false
|
|
579
582
|
}));
|
|
@@ -741,7 +744,10 @@ export const App = () => {
|
|
|
741
744
|
getContextWindow: () => agentStateRef.current.contextWindow,
|
|
742
745
|
// /clear -> 0 and /compact -> reduced size: reactively shrink the bar (the
|
|
743
746
|
// backend won't report the drop until the next turn).
|
|
744
|
-
setContextUsage: (used, estimated) => setAgentState(s => ({
|
|
747
|
+
setContextUsage: (used, estimated) => setAgentState(s => ({
|
|
748
|
+
...s, contextTokens: used, tokensEstimated: estimated,
|
|
749
|
+
cacheInputTokens: undefined, cachedInputTokens: undefined,
|
|
750
|
+
})),
|
|
745
751
|
model: configStore.get().model
|
|
746
752
|
}), [addMessage, clearMessages, getMessages, setMessages, reloadAgent, exit, shellController, retryLast, inputMode, promptController, formController]);
|
|
747
753
|
// Handle commands
|
|
@@ -1021,6 +1027,8 @@ export const App = () => {
|
|
|
1021
1027
|
// Provider-reported window (Codex) feeds the bar denominator; keep the
|
|
1022
1028
|
// last-known one when this event doesn't carry one.
|
|
1023
1029
|
contextWindow: event.contextWindow ?? s.contextWindow,
|
|
1030
|
+
cacheInputTokens: event.cacheInputTokens,
|
|
1031
|
+
cachedInputTokens: event.contextCachedInputTokens,
|
|
1024
1032
|
tokensEstimated: event.usage.estimated
|
|
1025
1033
|
}));
|
|
1026
1034
|
break;
|
|
@@ -1098,8 +1106,9 @@ export const App = () => {
|
|
|
1098
1106
|
React.useEffect(() => {
|
|
1099
1107
|
const prevStatus = prevStatusRef.current;
|
|
1100
1108
|
prevStatusRef.current = agentState.status;
|
|
1101
|
-
|
|
1102
|
-
|
|
1109
|
+
// Deliberately NOT gated on `inputMode`: /inputmode only selects what Enter
|
|
1110
|
+
// does while busy. A Tab-queued prompt promised end-of-turn delivery and
|
|
1111
|
+
// must still be sent when the run is configured to interrupt on Enter.
|
|
1103
1112
|
if (agentState.status !== 'idle')
|
|
1104
1113
|
return;
|
|
1105
1114
|
if (prevStatus === 'idle' && queue.length === 0)
|
|
@@ -1108,6 +1117,11 @@ export const App = () => {
|
|
|
1108
1117
|
return;
|
|
1109
1118
|
if (flushingRef.current)
|
|
1110
1119
|
return;
|
|
1120
|
+
// End-of-turn entries only; a `subturn` entry still waiting here is started
|
|
1121
|
+
// by its own fallback path, so taking it here would send it twice.
|
|
1122
|
+
const ready = queueRef.current.find(entry => entry.delivery !== 'subturn');
|
|
1123
|
+
if (!ready)
|
|
1124
|
+
return;
|
|
1111
1125
|
flushingRef.current = true;
|
|
1112
1126
|
const next = dequeueMessage();
|
|
1113
1127
|
if (!next) {
|
|
@@ -1117,7 +1131,7 @@ export const App = () => {
|
|
|
1117
1131
|
void handleSubmit(next.text).finally(() => {
|
|
1118
1132
|
flushingRef.current = false;
|
|
1119
1133
|
});
|
|
1120
|
-
}, [agentState.status, queue.length,
|
|
1134
|
+
}, [agentState.status, queue.length, dequeueMessage, handleSubmit]);
|
|
1121
1135
|
const showToast = useCallback((message) => {
|
|
1122
1136
|
setToast(message);
|
|
1123
1137
|
if (toastTimerRef.current) {
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -105,6 +105,8 @@ export interface AgentRunResult {
|
|
|
105
105
|
usage?: TokenUsage;
|
|
106
106
|
stopReason: 'complete' | 'interactive' | 'max_iterations';
|
|
107
107
|
reasoning?: string;
|
|
108
|
+
/** Ordered model-facing messages produced during a turn that accepted steering. */
|
|
109
|
+
turnMessages?: Message[];
|
|
108
110
|
}
|
|
109
111
|
export interface HarnessToolCallResult {
|
|
110
112
|
success: boolean;
|
|
@@ -224,6 +226,8 @@ export declare class Agent {
|
|
|
224
226
|
* immediately followed by tool_end.
|
|
225
227
|
*/
|
|
226
228
|
private emitObservation;
|
|
229
|
+
/** Ask a provider-owned tool loop to accept a busy user message. */
|
|
230
|
+
steerUserMessage(message: Message): Promise<boolean>;
|
|
227
231
|
/** Tools that end the agent turn when called (require user interaction before continuing). */
|
|
228
232
|
private static readonly TURN_ENDING_TOOLS;
|
|
229
233
|
/**
|
|
@@ -263,7 +267,7 @@ export declare class Agent {
|
|
|
263
267
|
* authoritative history and mirrors the accepted user/assistant pair around
|
|
264
268
|
* the same core loop used by unhosted ZTC.
|
|
265
269
|
*/
|
|
266
|
-
run(messages: Message[], signal?: AbortSignal): Promise<AgentRunResult>;
|
|
270
|
+
run(messages: Message[], signal?: AbortSignal, takeSteering?: () => Message[]): Promise<AgentRunResult>;
|
|
267
271
|
private runCore;
|
|
268
272
|
/**
|
|
269
273
|
* Release the backend. The backend lives as long as the Agent — the run loop
|
package/dist/agent/agent.js
CHANGED
|
@@ -896,6 +896,12 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
896
896
|
return;
|
|
897
897
|
}
|
|
898
898
|
}
|
|
899
|
+
/** Ask a provider-owned tool loop to accept a busy user message. */
|
|
900
|
+
async steerUserMessage(message) {
|
|
901
|
+
if (this.#conversationBinding)
|
|
902
|
+
return false;
|
|
903
|
+
return this.backend.steerUserMessage?.(message) ?? false;
|
|
904
|
+
}
|
|
899
905
|
/** Tools that end the agent turn when called (require user interaction before continuing). */
|
|
900
906
|
static TURN_ENDING_TOOLS = new Set([
|
|
901
907
|
'ask_user',
|
|
@@ -955,10 +961,10 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
955
961
|
* authoritative history and mirrors the accepted user/assistant pair around
|
|
956
962
|
* the same core loop used by unhosted ZTC.
|
|
957
963
|
*/
|
|
958
|
-
async run(messages, signal) {
|
|
964
|
+
async run(messages, signal, takeSteering) {
|
|
959
965
|
const binding = this.#conversationBinding;
|
|
960
966
|
if (!binding)
|
|
961
|
-
return this.runCore(messages, signal);
|
|
967
|
+
return this.runCore(messages, signal, takeSteering);
|
|
962
968
|
const userMessage = [...messages].reverse().find((message) => message.role === 'user'
|
|
963
969
|
&& !message.metadata?.synthetic
|
|
964
970
|
&& !message.metadata?.toolResult);
|
|
@@ -987,6 +993,9 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
987
993
|
if (!workingMessages.some((message) => message.id === userMessage.id)) {
|
|
988
994
|
workingMessages.push({ ...userMessage });
|
|
989
995
|
}
|
|
996
|
+
// The host driver publishes only the initial user and final assistant
|
|
997
|
+
// for this turn. Leave busy input pending for a new hosted turn so the
|
|
998
|
+
// authoritative conversation cannot silently omit it.
|
|
990
999
|
const result = await this.runCore(workingMessages, signal);
|
|
991
1000
|
const assistantMessage = {
|
|
992
1001
|
id: `host_assistant_${randomUUID()}`,
|
|
@@ -1033,11 +1042,13 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1033
1042
|
}
|
|
1034
1043
|
}
|
|
1035
1044
|
// Main execution loop shared by hosted and standalone turns.
|
|
1036
|
-
async runCore(messages, signal) {
|
|
1045
|
+
async runCore(messages, signal, takeSteering) {
|
|
1037
1046
|
if (!this.hasApiKey()) {
|
|
1038
1047
|
throw new AgentError('No API key configured. Use /config key <key> to set one.', 'NO_API_KEY');
|
|
1039
1048
|
}
|
|
1040
1049
|
const allToolCalls = [];
|
|
1050
|
+
const turnMessages = [];
|
|
1051
|
+
let acceptedSteering = false;
|
|
1041
1052
|
let iterations = 0;
|
|
1042
1053
|
// Each turn gets its own auto-continue budget; a long-running Agent must
|
|
1043
1054
|
// not inherit one spent by the previous turn.
|
|
@@ -1110,7 +1121,10 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1110
1121
|
const contextTokens = Math.max(0, usage.inputTokens - this._currentTransientOverageTokens);
|
|
1111
1122
|
// `usage` (this turn) may carry a provider-reported context window (Codex);
|
|
1112
1123
|
// forward it so the bar uses the real window over the static guess.
|
|
1113
|
-
this.emit({
|
|
1124
|
+
this.emit({
|
|
1125
|
+
type: 'token_usage', usage: totalUsage, contextTokens, contextWindow: usage.contextWindow,
|
|
1126
|
+
cacheInputTokens: usage.inputTokens, contextCachedInputTokens: usage.cachedInputTokens,
|
|
1127
|
+
});
|
|
1114
1128
|
// An incomplete provider response can still contain convincing prose or a
|
|
1115
1129
|
// partially assembled tool call. Treat it as a failed inference before
|
|
1116
1130
|
// either branch can persist the text as a final answer or execute an
|
|
@@ -1361,21 +1375,25 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1361
1375
|
// so maxIterations exhaustion still has something to return.
|
|
1362
1376
|
lastToolTurnText = textContent;
|
|
1363
1377
|
}
|
|
1364
|
-
|
|
1378
|
+
const toolUseMessage = {
|
|
1365
1379
|
id: `assistant_${Date.now()}`,
|
|
1366
1380
|
role: 'assistant',
|
|
1367
1381
|
content: textContent || '[Using tools...]',
|
|
1368
1382
|
timestamp: new Date(),
|
|
1369
1383
|
toolCalls: allToolCalls.slice(-toolUseBlocks.length),
|
|
1370
1384
|
...(response.reasoning ? { metadata: { reasoningContent: response.reasoning } } : {}),
|
|
1371
|
-
}
|
|
1372
|
-
messages.push(
|
|
1385
|
+
};
|
|
1386
|
+
messages.push(toolUseMessage);
|
|
1387
|
+
turnMessages.push(toolUseMessage);
|
|
1388
|
+
const toolResultMessage = {
|
|
1373
1389
|
id: `tool_${Date.now()}`,
|
|
1374
1390
|
role: 'user',
|
|
1375
1391
|
content: JSON.stringify(toolResults),
|
|
1376
1392
|
timestamp: new Date(),
|
|
1377
1393
|
metadata: { toolResult: true },
|
|
1378
|
-
}
|
|
1394
|
+
};
|
|
1395
|
+
messages.push(toolResultMessage);
|
|
1396
|
+
turnMessages.push(toolResultMessage);
|
|
1379
1397
|
// An explicit terminal tool may supply the answer after its bookkeeping
|
|
1380
1398
|
// succeeds. Keep the tool-use/result pair above for conversation replay.
|
|
1381
1399
|
if (combined.length === 1 && combined[0].toolCall.status === 'complete') {
|
|
@@ -1406,6 +1424,17 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1406
1424
|
stopReason = 'interactive';
|
|
1407
1425
|
break;
|
|
1408
1426
|
}
|
|
1427
|
+
// A busy user message belongs after the completed tool-use/result pair
|
|
1428
|
+
// and before the next inference. Keep terminal tools and exhausted turns
|
|
1429
|
+
// on the normal next-turn path because no model request remains here.
|
|
1430
|
+
if (iterations < this.config.maxIterations) {
|
|
1431
|
+
const steering = takeSteering?.() ?? [];
|
|
1432
|
+
if (steering.length > 0) {
|
|
1433
|
+
messages.push(...steering);
|
|
1434
|
+
turnMessages.push(...steering);
|
|
1435
|
+
acceptedSteering = true;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1409
1438
|
}
|
|
1410
1439
|
catch (err) {
|
|
1411
1440
|
const error = err.message;
|
|
@@ -1459,6 +1488,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1459
1488
|
usage: totalUsage || undefined,
|
|
1460
1489
|
stopReason,
|
|
1461
1490
|
...(finalReasoning ? { reasoning: finalReasoning } : {}),
|
|
1491
|
+
...(acceptedSteering ? { turnMessages } : {}),
|
|
1462
1492
|
};
|
|
1463
1493
|
}
|
|
1464
1494
|
/**
|
|
@@ -1790,6 +1820,19 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
|
|
|
1790
1820
|
if (statusCode === 403 || errorType === 'PERMISSION_DENIED') {
|
|
1791
1821
|
return { retryable: false, reason: admissionMessage || 'Permission denied' };
|
|
1792
1822
|
}
|
|
1823
|
+
// Router billing admission (402 billing_required) is the first thing an
|
|
1824
|
+
// unsubscribed user hits on the default route, so say what to do. Only the
|
|
1825
|
+
// Router's H3 envelope may supply its public wording; any other 402 body
|
|
1826
|
+
// stays private behind the canonical message.
|
|
1827
|
+
if (statusCode === 402) {
|
|
1828
|
+
const guidance = 'Subscribe in Zerg Router (Dashboard → Billing), or choose another model with /model.';
|
|
1829
|
+
return {
|
|
1830
|
+
retryable: false,
|
|
1831
|
+
reason: admissionMessage
|
|
1832
|
+
? `${admissionMessage.replace(/\.?\s*$/, '.')} ${guidance}`
|
|
1833
|
+
: `This model needs an active Zerg Router subscription. ${guidance}`,
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1793
1836
|
// Keep the Router's setup failure actionable without echoing arbitrary
|
|
1794
1837
|
// remote bodies. Only these known provider names can enter the message.
|
|
1795
1838
|
const missingRouterKey = typeof errorMessage === 'string'
|
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
* Account pool — model + resolution for switchable Claude Code + Codex
|
|
3
3
|
* subscription accounts.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* official CLI (or the direct auth manager) refreshes its own creds in its own
|
|
9
|
-
* per-account dir/file. Later slices add the `/accounts` ops and the rotation
|
|
10
|
-
* engine on top of this module.
|
|
5
|
+
* Resolve each active account to an isolated credential path for the backend.
|
|
6
|
+
* Official CLI logins write each profile's credentials; this module probes
|
|
7
|
+
* their files and builds the corresponding process environment.
|
|
11
8
|
*/
|
|
12
9
|
import type { Account, AccountProvider, AccountRotation, AccountSource, ZTCConfig } from '../../config/types.js';
|
|
13
10
|
/** Root dir for ztc-owned per-account credential dirs. */
|
|
@@ -69,6 +66,8 @@ export declare function defaultSourceFor(provider: AccountProvider, dir: string)
|
|
|
69
66
|
export declare function sourceDir(source: AccountSource): string;
|
|
70
67
|
/** Create an account's credential dir (0700) if missing. Returns the dir. */
|
|
71
68
|
export declare function ensureAccountDir(source: AccountSource): string;
|
|
69
|
+
/** A registered Codex profile already reading this same credential directory. */
|
|
70
|
+
export declare function findCodexHomeCollision(accounts: readonly Account[], source: AccountSource): Account | undefined;
|
|
72
71
|
/**
|
|
73
72
|
* Whether an account holds ISOLATED (per-account) credentials. Always probes
|
|
74
73
|
* the account's EXPLICIT path — never the cached default — and for the Claude
|
|
@@ -2,22 +2,21 @@
|
|
|
2
2
|
* Account pool — model + resolution for switchable Claude Code + Codex
|
|
3
3
|
* subscription accounts.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* official CLI (or the direct auth manager) refreshes its own creds in its own
|
|
9
|
-
* per-account dir/file. Later slices add the `/accounts` ops and the rotation
|
|
10
|
-
* engine on top of this module.
|
|
5
|
+
* Resolve each active account to an isolated credential path for the backend.
|
|
6
|
+
* Official CLI logins write each profile's credentials; this module probes
|
|
7
|
+
* their files and builds the corresponding process environment.
|
|
11
8
|
*/
|
|
12
9
|
import { spawnSync } from 'node:child_process';
|
|
13
|
-
import { mkdirSync } from 'node:fs';
|
|
10
|
+
import { mkdirSync, realpathSync } from 'node:fs';
|
|
14
11
|
import { homedir } from 'node:os';
|
|
15
|
-
import { basename, dirname, join } from 'node:path';
|
|
12
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
13
|
import { normalizeProviderId } from './provider_ids.js';
|
|
17
14
|
import { hasUsableClaudeCodeAuth } from './claude_code_auth.js';
|
|
18
15
|
import { hasUsableCodexAuth } from './codex_auth.js';
|
|
16
|
+
import { CODEX_SUBSCRIPTION_CONFIG_ARGS, buildCodexAppServerEnv } from './codex_app_server.js';
|
|
19
17
|
import { hasUsableKimiCodeAuth } from './kimi_code_auth.js';
|
|
20
18
|
import { runtimeDiagOnce } from '../../utils/debug_log.js';
|
|
19
|
+
import { profileConfigDir } from '../../config/profiles.js';
|
|
21
20
|
/** Root dir for ztc-owned per-account credential dirs. */
|
|
22
21
|
export const ACCOUNTS_ROOT = join(homedir(), '.ztc', 'accounts');
|
|
23
22
|
const ACCOUNT_PROVIDERS = [
|
|
@@ -107,7 +106,10 @@ export function resolveRotation(config) {
|
|
|
107
106
|
// ---------------------------------------------------------------------------
|
|
108
107
|
/** Default ztc-owned credential dir for a freshly-added account. */
|
|
109
108
|
export function defaultAccountDir(provider, id) {
|
|
110
|
-
|
|
109
|
+
// Resolve at call time, matching ConfigStore's ZTC_CONFIG_DIR override. This
|
|
110
|
+
// keeps managed credentials beside the selected config, including test homes.
|
|
111
|
+
const root = join(profileConfigDir(), 'accounts');
|
|
112
|
+
return join(root, provider, id);
|
|
111
113
|
}
|
|
112
114
|
/**
|
|
113
115
|
* The {@link AccountSource} for a provider whose credentials live under `dir`.
|
|
@@ -126,9 +128,9 @@ export function defaultSourceFor(provider, dir) {
|
|
|
126
128
|
case 'claude_code':
|
|
127
129
|
return { kind: 'claude_config_dir', path: dir };
|
|
128
130
|
case 'codex_app_server':
|
|
129
|
-
return { kind: 'codex_home', path: dir };
|
|
131
|
+
return { kind: 'codex_home', path: resolve(dir) };
|
|
130
132
|
case 'codex_legacy':
|
|
131
|
-
return { kind: 'codex_auth_file', path: basename(dir) === 'auth.json' ? dir : join(dir, 'auth.json') };
|
|
133
|
+
return { kind: 'codex_auth_file', path: basename(dir) === 'auth.json' ? resolve(dir) : join(resolve(dir), 'auth.json') };
|
|
132
134
|
case 'claude_code_direct':
|
|
133
135
|
return {
|
|
134
136
|
kind: 'claude_creds_file',
|
|
@@ -162,6 +164,24 @@ export function ensureAccountDir(source) {
|
|
|
162
164
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
163
165
|
return dir;
|
|
164
166
|
}
|
|
167
|
+
function canonicalAccountHome(path) {
|
|
168
|
+
// `.native` returns the on-disk case, so `CodexA` and `codexa` on a
|
|
169
|
+
// case-insensitive volume resolve to the same credential directory.
|
|
170
|
+
try {
|
|
171
|
+
return realpathSync.native(path);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return resolve(path);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** A registered Codex profile already reading this same credential directory. */
|
|
178
|
+
export function findCodexHomeCollision(accounts, source) {
|
|
179
|
+
if (source.kind !== 'codex_home' && source.kind !== 'codex_auth_file')
|
|
180
|
+
return undefined;
|
|
181
|
+
const home = canonicalAccountHome(sourceDir(source));
|
|
182
|
+
return accounts.find((account) => (account.source.kind === 'codex_home' || account.source.kind === 'codex_auth_file')
|
|
183
|
+
&& canonicalAccountHome(sourceDir(account.source)) === home);
|
|
184
|
+
}
|
|
165
185
|
/**
|
|
166
186
|
* Whether an account holds ISOLATED (per-account) credentials. Always probes
|
|
167
187
|
* the account's EXPLICIT path — never the cached default — and for the Claude
|
|
@@ -258,6 +278,10 @@ export function warnSharedKeychainAccount(account, keychainPresent = sharedClaud
|
|
|
258
278
|
+ `share that one login — switching or rotating between them has no effect until each `
|
|
259
279
|
+ `logs in per-account (/accounts).`);
|
|
260
280
|
}
|
|
281
|
+
/** Quote a path for the displayed POSIX shell command. */
|
|
282
|
+
function shellQuote(value) {
|
|
283
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
284
|
+
}
|
|
261
285
|
export function buildLoginCommand(account) {
|
|
262
286
|
const source = account.source;
|
|
263
287
|
switch (source.kind) {
|
|
@@ -274,11 +298,12 @@ export function buildLoginCommand(account) {
|
|
|
274
298
|
case 'codex_home':
|
|
275
299
|
case 'codex_auth_file': {
|
|
276
300
|
const dir = sourceDir(source);
|
|
301
|
+
const args = [...CODEX_SUBSCRIPTION_CONFIG_ARGS, 'login'];
|
|
277
302
|
return {
|
|
278
303
|
command: 'codex',
|
|
279
|
-
args
|
|
280
|
-
env:
|
|
281
|
-
display: `CODEX_HOME=${dir} codex login`,
|
|
304
|
+
args,
|
|
305
|
+
env: buildCodexAppServerEnv(dir),
|
|
306
|
+
display: `CODEX_HOME=${shellQuote(dir)} codex ${args.map((arg) => arg === '-c' || arg === 'login' ? arg : shellQuote(arg)).join(' ')}`,
|
|
282
307
|
};
|
|
283
308
|
}
|
|
284
309
|
case 'kimi_creds_file': {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { AccountStateStore } from './account_state.js';
|
|
12
12
|
import type { AgentBackend, BackendRequest, BackendResponse, StreamEvent } from './types.js';
|
|
13
13
|
import type { Account } from '../../config/types.js';
|
|
14
|
+
import type { Message } from '../../types.js';
|
|
14
15
|
export type RotationMode = 'failover' | 'round_robin';
|
|
15
16
|
export interface AccountPoolBindings {
|
|
16
17
|
/** Every configured account. */
|
|
@@ -99,7 +100,9 @@ export declare class RotatingBackend implements AgentBackend {
|
|
|
99
100
|
* close() can also dispose one that is still mid-turn.
|
|
100
101
|
*/
|
|
101
102
|
private current;
|
|
103
|
+
private steeringAccepted;
|
|
102
104
|
constructor(opts: RotatingBackendOptions);
|
|
105
|
+
steerUserMessage(message: Message): Promise<boolean>;
|
|
103
106
|
/** Close a per-turn inner and forget it as current if it still is. */
|
|
104
107
|
private releaseInner;
|
|
105
108
|
/**
|
|
@@ -120,14 +120,23 @@ export class RotatingBackend {
|
|
|
120
120
|
* close() can also dispose one that is still mid-turn.
|
|
121
121
|
*/
|
|
122
122
|
current = null;
|
|
123
|
+
steeringAccepted = false;
|
|
123
124
|
constructor(opts) {
|
|
124
125
|
this.opts = opts;
|
|
125
126
|
this.now = opts.now ?? (() => Date.now());
|
|
126
127
|
}
|
|
128
|
+
async steerUserMessage(message) {
|
|
129
|
+
const accepted = await (this.current?.steerUserMessage?.(message) ?? false);
|
|
130
|
+
if (accepted)
|
|
131
|
+
this.steeringAccepted = true;
|
|
132
|
+
return accepted;
|
|
133
|
+
}
|
|
127
134
|
/** Close a per-turn inner and forget it as current if it still is. */
|
|
128
135
|
releaseInner(inner) {
|
|
129
|
-
if (this.current === inner)
|
|
136
|
+
if (this.current === inner) {
|
|
130
137
|
this.current = null;
|
|
138
|
+
this.steeringAccepted = false;
|
|
139
|
+
}
|
|
131
140
|
inner.close?.();
|
|
132
141
|
}
|
|
133
142
|
/**
|
|
@@ -220,6 +229,7 @@ export class RotatingBackend {
|
|
|
220
229
|
if (!account)
|
|
221
230
|
throw this.terminalError();
|
|
222
231
|
const inner = this.current = this.opts.buildInner(account);
|
|
232
|
+
this.steeringAccepted = false;
|
|
223
233
|
try {
|
|
224
234
|
return await inner.generate(req);
|
|
225
235
|
}
|
|
@@ -240,7 +250,7 @@ export class RotatingBackend {
|
|
|
240
250
|
const next = this.rotate(account, sig);
|
|
241
251
|
// A dynamic tool already ran this turn -> replaying re-fires its side
|
|
242
252
|
// effect. Cool the failed account for the next turn, but surface this error.
|
|
243
|
-
if (replayUnsafe)
|
|
253
|
+
if (replayUnsafe || this.steeringAccepted)
|
|
244
254
|
throw error;
|
|
245
255
|
if (!next)
|
|
246
256
|
throw this.terminalError();
|
|
@@ -263,6 +273,7 @@ export class RotatingBackend {
|
|
|
263
273
|
throw this.terminalError();
|
|
264
274
|
let emitted = false;
|
|
265
275
|
const inner = this.current = this.opts.buildInner(account);
|
|
276
|
+
this.steeringAccepted = false;
|
|
266
277
|
try {
|
|
267
278
|
// generateStream is required on AgentBackend — no fallback probe.
|
|
268
279
|
for await (const event of inner.generateStream(req)) {
|
|
@@ -288,7 +299,7 @@ export class RotatingBackend {
|
|
|
288
299
|
const next = this.rotate(account, sig);
|
|
289
300
|
// Mid-stream partial output OR a dynamic tool that already ran -> can't
|
|
290
301
|
// replay cleanly. Pool is rotated for the next turn; surface this error.
|
|
291
|
-
if (emitted || replayUnsafe)
|
|
302
|
+
if (emitted || replayUnsafe || this.steeringAccepted)
|
|
292
303
|
throw error;
|
|
293
304
|
if (!next)
|
|
294
305
|
throw this.terminalError();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface CodexQuotaWindow {
|
|
2
|
+
name: string;
|
|
3
|
+
remainingPercent: number;
|
|
4
|
+
resetAt?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CodexAccountStatus {
|
|
7
|
+
state: 'ready' | 'reauth_required' | 'quota_unavailable' | 'unavailable';
|
|
8
|
+
email?: string;
|
|
9
|
+
plan?: string;
|
|
10
|
+
windows: CodexQuotaWindow[];
|
|
11
|
+
observedAt?: number;
|
|
12
|
+
usage?: {
|
|
13
|
+
lifetimeTokens?: number;
|
|
14
|
+
todayTokens?: number;
|
|
15
|
+
observedAt: number;
|
|
16
|
+
};
|
|
17
|
+
usageStale?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export type CodexStatusProbe = (home: string) => Promise<CodexAccountStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* Minimum spacing between probes of one credential home, even when forced, for
|
|
22
|
+
* the shared mirror cache. A forced read spawns the Codex App Server and
|
|
23
|
+
* refreshes the login's token, so repeated refresh clicks (or a looping client)
|
|
24
|
+
* share one recent probe.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CODEX_STATUS_MIN_FORCED_INTERVAL_MS = 30000;
|
|
27
|
+
/** Cache official Codex status per credential home so a mirror poll does not spawn a CLI each time. */
|
|
28
|
+
export declare class CodexStatusCache {
|
|
29
|
+
private readonly probe;
|
|
30
|
+
private readonly entries;
|
|
31
|
+
private readonly pending;
|
|
32
|
+
private readonly generations;
|
|
33
|
+
private readonly minForcedIntervalMs;
|
|
34
|
+
constructor(probe: CodexStatusProbe, options?: {
|
|
35
|
+
minForcedIntervalMs?: number;
|
|
36
|
+
});
|
|
37
|
+
read(home: string, force?: boolean): Promise<CodexAccountStatus>;
|
|
38
|
+
invalidate(home: string): void;
|
|
39
|
+
}
|
|
40
|
+
/** Read identity and quota through the official Codex App Server under one isolated CODEX_HOME. */
|
|
41
|
+
export declare function inspectCodexAccount(home: string): Promise<CodexAccountStatus>;
|
|
42
|
+
export declare const codexStatusCache: CodexStatusCache;
|