diff --git a/.github/workflows/nightly-fork.yml b/.github/workflows/nightly-fork.yml new file mode 100644 index 00000000000..c2ff8cca792 --- /dev/null +++ b/.github/workflows/nightly-fork.yml @@ -0,0 +1,262 @@ +# Fork-local nightly for jetblk/t3code. +# +# Upstream's release.yml is unusable here: it needs pingdotgg's self-hosted +# `blacksmith-*` runners, their Clerk/Cloudflare/Apple/Azure secrets, and it +# publishes the `t3` npm package we don't own. This builds the one artifact our +# nodes actually consume — the self-contained `vp pack` server bundle — and +# publishes it as a rolling `nightly` GitHub Release. +# +# Nodes pull that asset on service restart (restart == update), mirroring the +# `npx t3@nightly` behaviour we replaced. The repo is public, so nodes need no +# auth to download it. +# +# Checks every 3 hours (mirroring upstream's release.yml cadence), and only +# builds when main actually moved (see check_changes) — a quiet window costs a +# ~10s gate check, not a build. Need a build sooner? Dispatch it manually; that +# path always builds. +name: Fork nightly + +on: + # Every 3 hours — same cadence upstream's release.yml uses for its nightly + # channel. The offset minute is deliberate: crons on the hour queue behind + # GitHub's peak load and get delayed or dropped. + schedule: + - cron: "37 */3 * * *" + # Manual runs skip the change gate below and always build — this is the + # "I want my merge on a node now" path. + workflow_dispatch: + +permissions: + contents: read + +# Never cancel in flight: the publish step deletes the `nightly` release before +# recreating it, and a cancel inside that window would leave nodes with no +# release to pull at all (their ExecStartPre is best-effort, so they would +# silently keep running a stale build). Upstream's release.yml likewise has no +# concurrency group. +concurrency: + group: fork-nightly + cancel-in-progress: false + +jobs: + check_changes: + name: Check for changes since last nightly + # Only the cron needs a gate; a manual dispatch is an explicit "build now". + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + has_changes: ${{ steps.check.outputs.has_changes }} + env: + GH_TOKEN: ${{ github.token }} + steps: + # No checkout on purpose — the compare API answers this in seconds, so a + # quiet night costs ~10s instead of a full install. Every failure path + # here builds rather than skips: a missed build is worse than a spare one. + - id: check + name: Compare HEAD to the last published nightly + shell: bash + run: | + set -euo pipefail + + build() { echo "has_changes=true" >> "$GITHUB_OUTPUT"; exit 0; } + + # The rolling release records the exact commit it was built from. + last="$(gh release view nightly --repo "$GITHUB_REPOSITORY" \ + --json targetCommitish -q .targetCommitish 2>/dev/null || true)" + if [ -z "$last" ]; then + echo "No nightly release to compare against — building." + build + fi + + if ! json="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${last}...${GITHUB_SHA}" 2>/dev/null)"; then + echo "Compare against ${last} failed (force-push? deleted commit?) — building." + build + fi + + # The compare API caps .files at 300. A diff that large is an upstream + # sync, which has code in it by definition — don't trust the truncated + # list, just build. + if [ "$(jq '.files | length' <<<"$json")" -ge 300 ]; then + echo "Compare truncated at 300 files — building." + build + fi + + # Same intent as the old `paths-ignore: docs/**, **/*.md`, but applied + # to the whole day's diff rather than one push: a day of nothing but + # docs commits now skips, where paths-ignore judged each push alone. + # (Path filters only apply to push/pull_request, so they would be dead + # config under `schedule`.) + code="$(jq -r '.files[].filename' <<<"$json" | grep -vE '^docs/|\.md$' || true)" + if [ -z "$code" ]; then + echo "No code changes since ${last} — skipping this build." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "Code changed since ${last}:" + echo "$code" + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + + build: + name: Build and publish server bundle + needs: [check_changes] + # `!failure() && !cancelled()` is what lets a *skipped* gate through (a + # skipped `needs` job otherwise skips its dependents) while still halting on + # a *broken* one — `always()` would build even when the gate errored. + if: | + !failure() && !cancelled() && + (github.event_name != 'schedule' || needs.check_changes.outputs.has_changes == 'true') + # Only this job publishes; the gate above needs read-only. + permissions: + contents: write + # GitHub-hosted: the fork has no access to upstream's blacksmith runners. + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Vite+ + uses: voidzero-dev/setup-vp@v1 + with: + # package.json pins Node ^24.13.1 — required by the TS build scripts. + node-version-file: package.json + cache: true + run-install: | + args: + - --filter=t3... + # scripts/update-release-package-versions.ts imports + # @effect/platform-node, which only @t3tools/scripts pulls in. + - --filter=@t3tools/scripts... + # The server serves the web client from dist/client. + - --filter=@t3tools/web... + + - id: version + name: Resolve fork version + shell: bash + env: + RUN_STARTED_AT: ${{ github.run_started_at }} + run: | + set -euo pipefail + # Track upstream's stable base from package.json (nightly versions are + # never committed upstream, so main always carries the last stable), + # then mark it as ours so it is obvious which build a node is running. + base="$(node -p "require('./apps/server/package.json').version")" + # Date the run, not the clock: bare `date -u` reads whenever this step + # happens to run, so a build starting at 23:59 UTC would stamp + # tomorrow. Mirrors upstream's NIGHTLY_DATE: github.run_started_at. + day="$(date -u -d "$RUN_STARTED_AT" +%Y%m%d)" + version="${base}-jetblk.${day}.${GITHUB_RUN_NUMBER}" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Building $version" + + - name: Stamp version into releasable packages + run: node scripts/update-release-package-versions.ts "${{ steps.version.outputs.version }}" + + # Gate on typecheck + the provider-usage tests only. The full server suite + # has pre-existing ACP-transport failures unrelated to this fork; gating on + # it would block every release. + - name: Typecheck + run: vp run --filter t3 --filter @t3tools/contracts typecheck + + # `vp test` is vitest; run it from each package so its own config applies. + - name: Test contracts (provider usage) + working-directory: packages/contracts + run: vp test run src/providerUsage.test.ts + + - name: Test server (provider usage) + working-directory: apps/server + run: | + vp test run \ + src/provider/Layers/ClaudeUsage.test.ts \ + src/provider/Layers/CodexUsage.test.ts \ + src/provider/Layers/ProviderUsageService.test.ts + + - name: Build web client + run: vp run --filter @t3tools/web build + + - name: Build server bundle + run: vp run --filter t3 build:bundle + + # `vp pack` only produces dist/bin.mjs. The server serves the browser UI + # from dist/client, so without this it answers "No static directory + # configured and no dev URL set." This mirrors what apps/server/scripts/ + # cli.ts `build` does (copy apps/web/dist -> dist/client); we do it inline + # rather than calling that script so we skip its *development* icon + # overrides, which would ship dev icons in a release build. + - name: Bundle web client into dist/client + run: | + set -euo pipefail + test -f apps/web/dist/index.html + rm -rf apps/server/dist/client + cp -r apps/web/dist apps/server/dist/client + test -f apps/server/dist/client/index.html + + - name: Package self-contained bundle + shell: bash + run: | + set -euo pipefail + echo "${{ steps.version.outputs.version }}" > apps/server/dist/VERSION + # `vp pack` externalises dependencies (and 5 of them use pnpm's + # `catalog:` protocol), so dist/ alone cannot run. `pnpm deploy` copies + # the package plus its *production* deps — including the native ones + # (node-pty, sqlite) — into one portable tree. Safe because every node + # is linux-x64, matching this runner; a non-linux node would need its + # own build. + corepack enable # packageManager pins pnpm@11.10.0 + pnpm deploy --filter t3 --prod --legacy deploy-out + tar -czf t3-server.tgz -C deploy-out . + du -h t3-server.tgz + + # Two releases per build, on purpose: + # v immutable, never deleted — the pin/rollback target. + # nightly rolling pointer, recreated each run — the stable URL nodes + # curl, so they never need to know the current version. + # The asset is uploaded twice (GitHub can't alias an asset across + # releases); that's the price of keeping both a fixed URL and a history. + - name: Publish releases + shell: bash + run: | + set -euo pipefail + ver="${{ steps.version.outputs.version }}" + + cat > notes.md </dev/null || true + gh release create nightly t3-server.tgz \ + --title "Fork nightly ${ver} (rolling)" \ + --notes-file notes.md \ + --prerelease \ + --target "${GITHUB_SHA}" diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 4a0c761f2c6..9761bf66681 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -98,9 +98,21 @@ function resolveAppVariant(value: string | undefined): AppVariant { } const variant = VARIANT_CONFIG[APP_VARIANT]; +// Upstream's personal-team mode (reduced capabilities) takes precedence. +// `T3CODE_IOS_BUNDLE_ID` is the fork's escape hatch for building on a *paid* +// team that isn't T3 Tools: the widgets app group is derived from the bundle ID +// (`group.`) and app group identifiers are globally unique across +// Apple teams, so the bundle ID must change with the team — but unlike a +// personal team, a paid team can still sign widgets/sharing/Sign in with Apple. const iosBundleIdentifier = isIosPersonalTeamBuild ? personalTeamBundleIdentifier! - : variant.iosBundleIdentifier; + : (repoEnv.T3CODE_IOS_BUNDLE_ID ?? variant.iosBundleIdentifier); + +// Fork identity, deliberately user-visible only: the internal `@t3tools/*` +// package names stay upstream's, which is what keeps upstream merges cheap +// (renaming them would touch ~1000 files and conflict on every sync). Set +// T3CODE_APP_NAME to override, or to `variant.appName` for an unbranded build. +const appName = repoEnv.T3CODE_APP_NAME ?? `${variant.appName} (JetBlk)`; const dmSansFonts = { regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf", @@ -157,7 +169,7 @@ const sharingPlugin: NonNullable[number] = [ // family names without waiting for runtime font loading. const config: ExpoConfig = { - name: variant.appName, + name: appName, slug: "t3-code", platforms: ["ios", "android"], scheme: variant.scheme, @@ -184,8 +196,9 @@ const config: ExpoConfig = { bundleIdentifier: iosBundleIdentifier, // Pin code signing to the T3 Tools team so non-interactive `expo run:ios` // does not fall back to a personal team (which cannot sign app groups, - // Sign in with Apple, or push notification entitlements). - appleTeamId: "ARK85ZXQ4Z", + // Sign in with Apple, or push notification entitlements). Contributors + // without T3 Tools membership can override with their own (paid) team. + appleTeamId: repoEnv.T3CODE_APPLE_TEAM_ID ?? "ARK85ZXQ4Z", associatedDomains: [ `applinks:${variant.relyingParty}`, `webcredentials:${variant.relyingParty}`, diff --git a/apps/mobile/global.css b/apps/mobile/global.css index 6d36bf77caf..56067d24ed5 100644 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -42,6 +42,7 @@ --color-secondary-foreground: #262626; --color-secondary-border: rgba(0, 0, 0, 0.08); --color-switch-active: #34c759; + --color-warning: #f59e0b; /* Danger */ --color-danger: #fef2f2; @@ -136,6 +137,7 @@ --color-secondary-foreground: #f5f5f5; --color-secondary-border: rgba(255, 255, 255, 0.06); --color-switch-active: #30d158; + --color-warning: #fbbf24; /* Danger */ --color-danger: rgba(239, 68, 68, 0.14); diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index db244d7c726..24e33efa319 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -44,6 +44,7 @@ import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider"; import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen"; import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen"; import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen"; +import { SettingsProviderUsageRouteScreen } from "./features/settings/SettingsProviderUsageRouteScreen"; import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen"; @@ -177,6 +178,13 @@ const SettingsSheetStack = createNativeStackNavigator({ title: "Client Storage", }, }), + SettingsProviderUsage: createNativeStackScreen({ + screen: SettingsProviderUsageRouteScreen, + linking: "provider-usage", + options: { + title: "Usage & Limits", + }, + }), SettingsAuth: createNativeStackScreen({ screen: SettingsAuthRouteScreen, linking: "auth", diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 6c1b1038698..d4917e20d60 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -6,27 +6,60 @@ type ProviderIconProps = { readonly size?: number; }; +// Ported from the web/desktop client's provider marks (apps/web Icons.tsx → +// PROVIDER_ICON_BY_PROVIDER). Two-tone/monochrome fills track the app theme so +// the marks read on both grounds. Unknown drivers (and Codex) fall back to the +// OpenAI mark, matching the web client's mapping. +const CLAUDE_PATH = + "m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"; +const OPENAI_PATH = + "M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"; +const CURSOR_PATH = + "M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"; +const GROK_PATH_1 = + "M9.26905 15.284L17.2479 9.36086C17.6391 9.07047 18.1981 9.18374 18.3845 9.63478C19.3655 12.0135 18.9272 14.8721 16.9755 16.8349C15.0238 18.7976 12.3082 19.228 9.8261 18.2477L7.1146 19.5102C11.0037 22.1834 15.7263 21.5223 18.6774 18.5525C21.0182 16.1985 21.7432 12.9897 21.0653 10.0961L21.0714 10.1023C20.0884 5.85143 21.3131 4.15233 23.8218 0.677913C23.8812 0.595532 23.9406 0.513151 24 0.428711L20.6987 3.74866V3.73836L9.267 15.2861"; +const GROK_PATH_2 = + "M7.62249 16.7237C4.83113 14.0422 5.3124 9.89222 7.69417 7.49905C9.45541 5.72786 12.341 5.00497 14.86 6.06768L17.5653 4.81138C17.0779 4.45714 16.4533 4.07613 15.7365 3.80839C12.4966 2.46764 8.6178 3.13492 5.98413 5.78141C3.45081 8.32904 2.65415 12.2463 4.02219 15.5889C5.04412 18.0871 3.36889 19.8541 1.68137 21.6377C1.08337 22.2699 0.483318 22.9022 0 23.5716L7.62045 16.7257"; +// OpenCode's block glyph is two overlaid marks (frame + inner square). +const OPENCODE_INNER = "M24 32H8V16H24V32Z"; +const OPENCODE_FRAME = "M24 8H8V32H24V8ZM32 40H0V0H32V40Z"; + export function ProviderIcon(props: ProviderIconProps) { const isDarkMode = useColorScheme() === "dark"; const size = props.size ?? 16; - if (props.provider === "claudeAgent") { - return ( - - - - ); + switch (props.provider) { + case "claudeAgent": + return ( + + + + ); + case "cursor": + return ( + + + + ); + case "grok": + return ( + + + + + ); + case "opencode": + return ( + + + + + ); + default: + return ( + + + + ); } - - return ( - - - - ); } diff --git a/apps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsx new file mode 100644 index 00000000000..c73e64d3259 --- /dev/null +++ b/apps/mobile/src/features/settings/SettingsProviderUsageRouteScreen.tsx @@ -0,0 +1,481 @@ +import type { EnvironmentId } from "@t3tools/contracts"; +import { SymbolView } from "expo-symbols"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { ActivityIndicator, RefreshControl, ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { AppText as Text } from "../../components/AppText"; +import { ProviderIcon } from "../../components/ProviderIcon"; +import { useThemeColor } from "../../lib/useThemeColor"; +import { useEnvironments } from "../../state/environments"; +import { + aggregateProviderUsage, + areProviderUsageResultsComplete, + formatCredits, + formatExpiresIn, + formatResetsIn, + formatRetriesIn, + percentLeft, + providerUsageCreditsHaveMeter, + providerUsageCreditsUsedPercent, + type EnvironmentUsageInput, + type NodeStatus, + type ProviderUsageCard as ProviderUsageCardData, +} from "../../state/providerUsage"; +import { useEnvironmentQuery } from "../../state/query"; +import { serverEnvironment } from "../../state/server"; + +export function SettingsProviderUsageRouteScreen() { + const insets = useSafeAreaInsets(); + const mutedColor = useThemeColor("--color-foreground-muted"); + const iconColor = useThemeColor("--color-icon"); + const { isReady, environments } = useEnvironments(); + + const sortedEnvironments = useMemo( + () => [...environments].sort((a, b) => a.label.localeCompare(b.label)), + [environments], + ); + + const [results, setResults] = useState>({}); + const [refreshNonce, setRefreshNonce] = useState(0); + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + + const handleResult = useCallback((next: EnvironmentUsageInput) => { + setResults((prev) => { + const existing = prev[next.environmentId]; + if ( + existing && + existing.snapshots === next.snapshots && + existing.isPending === next.isPending && + existing.error === next.error && + existing.environmentLabel === next.environmentLabel + ) { + return prev; + } + return { ...prev, [next.environmentId]: next }; + }); + }, []); + + const handleRemove = useCallback((environmentId: string) => { + setResults((prev) => { + if (!(environmentId in prev)) return prev; + const next = { ...prev }; + delete next[environmentId]; + return next; + }); + }, []); + + // Aggregate only the environments still mounted, in their sorted order, so a + // disconnected node's stale result never lingers in the merged view. + const aggregate = useMemo(() => { + const inputs = sortedEnvironments + .map((environment) => results[environment.environmentId]) + .filter((value): value is EnvironmentUsageInput => value !== undefined); + return aggregateProviderUsage(inputs); + }, [results, sortedEnvironments]); + + const handleRefresh = useCallback(() => { + setRefreshNonce((value) => value + 1); + }, []); + + // Deterministic pull-to-refresh spinner. The usage atoms are stale-while- + // revalidate, so their `waiting` flag never cleanly settles (it lingers + // through background revalidation across environments) — driving the + // RefreshControl off it leaves the spinner stuck. Instead show it for a + // bounded window each pull and let cards update as environments resolve. + // Matches the isPullRefreshing pattern in GitOverviewSheet. + useEffect(() => { + if (refreshNonce === 0) return; + setIsPullRefreshing(true); + const timeout = setTimeout(() => setIsPullRefreshing(false), 1000); + return () => clearTimeout(timeout); + }, [refreshNonce]); + + const nowMs = Date.now(); + const hasContent = + aggregate.cards.length > 0 || + aggregate.pendingNodes.length > 0 || + aggregate.failedNodes.length > 0; + const hasAllResults = areProviderUsageResultsComplete( + sortedEnvironments.map((environment) => environment.environmentId), + results, + ); + const isInitialLoading = + isReady && !hasContent && sortedEnvironments.length > 0 && !hasAllResults; + + return ( + + {sortedEnvironments.map((environment) => ( + + ))} + + + } + > + {!isReady || isInitialLoading ? ( + + + Loading usage… + + ) : !hasContent ? ( + + + No usage to show + + Connect an environment signed in to a provider to see its subscription limits here. + + + ) : ( + + Providers + + {aggregate.cards.map((card) => ( + + ))} + {aggregate.pendingNodes.map((node) => ( + + ))} + {aggregate.failedNodes.map((node) => ( + + ))} + + + Usage reflects your provider subscription's rate-limit windows. Pull to refresh. + + + )} + + + ); +} + +/** + * One environment's usage subscription. Renders nothing — it exists so each + * environment's `useEnvironmentQuery` hook is called at a stable position + * (rules of hooks) while the list of environments changes. Results and refresh + * are lifted to the screen, which aggregates across all probes. + */ +function EnvironmentUsageProbe(props: { + readonly environmentId: EnvironmentId; + readonly environmentLabel: string; + readonly refreshNonce: number; + readonly onResult: (result: EnvironmentUsageInput) => void; + readonly onRemove: (environmentId: string) => void; +}) { + const { environmentId, environmentLabel, refreshNonce, onResult, onRemove } = props; + const query = useEnvironmentQuery(serverEnvironment.providerUsage({ environmentId, input: {} })); + const snapshots = query.data ? query.data.usage : null; + + useEffect(() => { + onResult({ + environmentId, + environmentLabel, + snapshots, + isPending: query.isPending, + error: query.error, + }); + }, [environmentId, environmentLabel, snapshots, query.isPending, query.error, onResult]); + + useEffect(() => () => onRemove(environmentId), [environmentId, onRemove]); + + // Refresh on demand without re-subscribing when the refresh fn's identity + // changes between renders. + const refreshRef = useRef(query.refresh); + refreshRef.current = query.refresh; + const didMount = useRef(false); + useEffect(() => { + if (!didMount.current) { + didMount.current = true; + return; + } + refreshRef.current(); + }, [refreshNonce]); + + return null; +} + +function ProviderUsageCard(props: { + readonly card: ProviderUsageCardData; + readonly nowMs: number; +}) { + const { card, nowMs } = props; + const subtitle = buildSubtitle(card.account, card.sourceNodes); + + return ( + + + + + + {card.displayName} + + {subtitle ? ( + + {subtitle} + + ) : null} + + {card.planLabel ? ( + + + {card.planLabel} + + + ) : null} + + + + + ); +} + +function ProviderUsageBody(props: { + readonly card: ProviderUsageCardData; + readonly nowMs: number; +}) { + const { card, nowMs } = props; + const dangerColor = useThemeColor("--color-danger-foreground"); + const iconColor = useThemeColor("--color-icon"); + const warningColor = useThemeColor("--color-warning"); + + if (card.status === "ok") { + const credits = card.credits ? formatCredits(card.credits) : null; + const resetCredits = card.resetCredits; + const hasResetCredits = resetCredits !== undefined && resetCredits.availableCount > 0; + const isStale = card.freshness?.state === "stale"; + const retry = formatRetriesIn(card.freshness?.retryAt, nowMs); + return ( + + {isStale ? ( + + + + {card.message ?? "Showing the most recent usage because live usage is unavailable."} + {retry ? ` ${retry}.` : null} + + + ) : null} + {card.windows.length === 0 && !credits && !hasResetCredits ? ( + No active limits reported. + ) : null} + {card.windows.map((window) => ( + + ))} + {card.credits && credits ? ( + + ) : null} + {resetCredits && resetCredits.availableCount > 0 ? ( + + ) : null} + + ); + } + + if (card.status === "unsupported") { + return ( + + Usage isn't available for this provider. + + ); + } + + const isError = card.status === "error"; + return ( + + + + {card.message ?? + (isError ? "Couldn't load usage." : "Sign in to this provider to see usage.")} + + + ); +} + +function ResetCreditsRow(props: { + readonly resetCredits: NonNullable; + readonly nowMs: number; +}) { + return ( + + + Rate limit resets + + {props.resetCredits.availableCount} available + + + {props.resetCredits.credits.length > 0 ? ( + + {props.resetCredits.credits.map((credit) => { + const expiry = formatExpiresIn(credit.expiresAt, props.nowMs); + return ( + + + + {credit.title ?? "Rate limit reset"} + + {expiry ? ( + {expiry} + ) : null} + + {credit.description ? ( + {credit.description} + ) : null} + + ); + })} + + ) : null} + + ); +} + +function UsageWindowRow(props: { + readonly label: string; + readonly usedPercent: number | null; + readonly resetsAt?: string; + readonly nowMs?: number; + /** Overrides the "% left" line (used for credit balances). */ + readonly valueText?: string; +}) { + const resets = props.nowMs !== undefined ? formatResetsIn(props.resetsAt, props.nowMs) : null; + const left = + props.valueText ?? + (props.usedPercent === null ? "" : `${Math.round(percentLeft(props.usedPercent))}% left`); + return ( + + + {props.label} + + {props.usedPercent === null ? null : } + + {left} + {resets ? {resets} : null} + + + ); +} + +/** + * A slim "% consumed" meter. The fill grows toward the limit and steps + * green → amber → red as the window is exhausted. + */ +function UsageMeter(props: { readonly usedPercent: number }) { + const trackColor = useThemeColor("--color-subtle"); + const okColor = useThemeColor("--color-switch-active"); + const warningColor = useThemeColor("--color-warning"); + const dangerColor = useThemeColor("--color-danger-foreground"); + const used = Math.max(0, Math.min(100, props.usedPercent)); + const fillColor = used >= 95 ? dangerColor : used >= 75 ? warningColor : okColor; + + return ( + + 0 ? 3 : 0)}%`, backgroundColor: fillColor }} + /> + + ); +} + +function NodeStatusRow(props: { readonly node: NodeStatus; readonly kind: "pending" | "failed" }) { + const mutedColor = useThemeColor("--color-foreground-muted"); + const dangerColor = useThemeColor("--color-danger-foreground"); + const isFailed = props.kind === "failed"; + return ( + + {isFailed ? ( + + ) : ( + + )} + + + {props.node.environmentLabel} + + + {isFailed ? (props.node.error ?? "Unreachable") : "Loading usage…"} + + + + ); +} + +function buildSubtitle( + account: string | undefined, + sourceNodes: ReadonlyArray, +): string | null { + const parts: string[] = []; + if (account) parts.push(account); + if (sourceNodes.length > 1) parts.push(`via ${sourceNodes.join(", ")}`); + else if (sourceNodes.length === 1) parts.push(sourceNodes[0]); + return parts.length > 0 ? parts.join(" · ") : null; +} diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 6c67a4d89e8..c41cde8b7dc 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -117,6 +117,11 @@ function LocalSettingsRouteScreen() { value={`${environmentCount}`} target="SettingsEnvironments" /> + @@ -466,6 +471,11 @@ function ConfiguredSettingsRouteScreen() { value={`${environmentCount}`} target="SettingsEnvironments" /> + = {}): ProviderUsageSnapshot => ({ + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + status: "ok", + windows: [], + fetchedAt: "2025-01-15T10:00:00.000Z", + ...overrides, +}); + +describe("aggregateProviderUsage", () => { + it("dedupes accounts across nodes and keeps the freshest values", () => { + const result = aggregateProviderUsage([ + { + environmentId: "vps-1", + environmentLabel: "vps-1", + snapshots: [ + makeSnapshot({ + account: "person@example.com", + planLabel: "Older plan", + windows: [{ id: "primary", label: "Session", kind: "session", usedPercent: 20 }], + fetchedAt: "2025-01-15T10:00:00.000Z", + }), + ], + isPending: false, + error: null, + }, + { + environmentId: "local", + environmentLabel: "local", + snapshots: [ + makeSnapshot({ + account: "person@example.com", + planLabel: "Fresh plan", + windows: [{ id: "primary", label: "Session", kind: "session", usedPercent: 80 }], + fetchedAt: "2025-01-15T11:00:00.000Z", + }), + ], + isPending: false, + error: null, + }, + ]); + + expect(result.cards).toHaveLength(1); + expect(result.cards[0]).toMatchObject({ + key: "account:codex:person@example.com", + planLabel: "Fresh plan", + fetchedAt: "2025-01-15T11:00:00.000Z", + sourceNodes: ["local", "vps-1"], + }); + expect(result.cards[0]?.windows[0]?.usedPercent).toBe(80); + }); + + it("keeps account-less instances as separate cards", () => { + const result = aggregateProviderUsage([ + { + environmentId: "local", + environmentLabel: "local", + snapshots: [makeSnapshot()], + isPending: false, + error: null, + }, + { + environmentId: "vps-1", + environmentLabel: "vps-1", + snapshots: [makeSnapshot()], + isPending: false, + error: null, + }, + ]); + + expect(result.cards.map((card) => card.key).sort()).toEqual([ + "instance:local:codex", + "instance:vps-1:codex", + ]); + }); + + it("separates pending and failed nodes from returned cards", () => { + const inputs: ReadonlyArray = [ + { + environmentId: "loading", + environmentLabel: "Loading node", + snapshots: null, + isPending: true, + error: null, + }, + { + environmentId: "broken", + environmentLabel: "Broken node", + snapshots: null, + isPending: false, + error: "boom", + }, + { + environmentId: "healthy", + environmentLabel: "Healthy node", + snapshots: [makeSnapshot()], + isPending: false, + error: null, + }, + ]; + + expect(aggregateProviderUsage(inputs)).toMatchObject({ + cards: [{ key: "instance:healthy:codex" }], + pendingNodes: [{ environmentId: "loading", environmentLabel: "Loading node" }], + failedNodes: [{ environmentId: "broken", environmentLabel: "Broken node", error: "boom" }], + }); + }); +}); + +describe("usage presentation helpers", () => { + const nowMs = Date.parse("2025-01-15T10:00:00.000Z"); + const isoIn = (milliseconds: number) => new Date(nowMs + milliseconds).toISOString(); + + it("formats reset durations", () => { + expect(formatResetsIn(undefined, nowMs)).toBeNull(); + expect(formatResetsIn(isoIn(0), nowMs)).toBeNull(); + expect(formatResetsIn("not-a-date", nowMs)).toBeNull(); + expect(formatResetsIn(isoIn(48 * 60_000), nowMs)).toBe("Resets in 48m"); + expect(formatResetsIn(isoIn((3 * 60 + 48) * 60_000), nowMs)).toBe("Resets in 3h 48m"); + expect(formatResetsIn(isoIn(3 * 60 * 60_000), nowMs)).toBe("Resets in 3h"); + expect(formatResetsIn(isoIn((2 * 24 + 4) * 60 * 60_000), nowMs)).toBe("Resets in 2d 4h"); + }); + + it("formats credits", () => { + expect(formatCredits({ label: "Credits", unlimited: true })).toBe("Unlimited"); + expect(formatCredits({ label: "Credits", usedCredits: 57.5, monthlyLimit: 200 })).toBe( + "$142.50 left · $200.00 limit", + ); + expect(formatCredits({ label: "Credits", balance: "42 credits" })).toBe("42 credits"); + expect(formatCredits({ label: "Credits" })).toBeNull(); + }); + + it("calculates clamped percentage left", () => { + expect(percentLeft(18)).toBe(82); + expect(percentLeft(130)).toBe(0); + expect(percentLeft(-5)).toBe(100); + }); + + it("uses known driver names unless a snapshot name is provided", () => { + expect(providerDisplayName("claudeAgent")).toBe("Claude"); + expect(providerDisplayName("codex")).toBe("Codex"); + expect(providerDisplayName("cursor")).toBe("Cursor"); + expect(providerDisplayName("customDriver")).toBe("customDriver"); + expect(providerDisplayName("codex", "Work Codex")).toBe("Work Codex"); + }); +}); diff --git a/apps/mobile/src/state/providerUsage.ts b/apps/mobile/src/state/providerUsage.ts new file mode 100644 index 00000000000..39e3b807806 --- /dev/null +++ b/apps/mobile/src/state/providerUsage.ts @@ -0,0 +1,16 @@ +export { + aggregateProviderUsage, + areProviderUsageResultsComplete, + formatProviderUsageCredits as formatCredits, + formatProviderUsageExpiry as formatExpiresIn, + formatProviderUsageReset as formatResetsIn, + formatProviderUsageRetry as formatRetriesIn, + providerUsageCreditsHaveMeter, + providerUsageCreditsUsedPercent, + providerUsageDisplayName as providerDisplayName, + providerUsagePercentLeft as percentLeft, + type AggregatedProviderUsage, + type EnvironmentUsageInput, + type ProviderUsageCard, + type ProviderUsageNodeStatus as NodeStatus, +} from "@t3tools/client-runtime/provider-usage"; diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index ee9cddf949c..6a432d852a7 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -28,6 +28,7 @@ import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts"; +import { makeClaudeUsage } from "../Layers/ClaudeUsage.ts"; import { checkClaudeProviderStatus, makePendingClaudeProvider, @@ -133,7 +134,10 @@ export const ClaudeDriver: ProviderDriver = { binaryPath: effectiveConfig.binaryPath, env: processEnv, }); - const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); + const continuationGroupKey = yield* makeClaudeContinuationGroupKey( + effectiveConfig, + processEnv, + ); const stampIdentity = withInstanceIdentity({ instanceId, displayName, @@ -148,6 +152,15 @@ export const ClaudeDriver: ProviderDriver = { }; const adapter = yield* makeClaudeAdapter(effectiveConfig, adapterOptions); const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv); + const usage = yield* makeClaudeUsage( + effectiveConfig, + { + instanceId, + driverKind: DRIVER_KIND, + displayName, + }, + processEnv, + ); // Per-instance capabilities cache: keyed on binary + resolved HOME so // account-specific probes never share auth metadata across instances. @@ -159,7 +172,11 @@ export const ClaudeDriver: ProviderDriver = { Effect.provideService(Path.Path, path), ), }); - const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey(effectiveConfig, cwd); + const capabilitiesCacheKey = yield* makeClaudeCapabilitiesCacheKey( + effectiveConfig, + processEnv, + cwd, + ); const checkProvider = checkClaudeProviderStatus( effectiveConfig, @@ -213,6 +230,7 @@ export const ClaudeDriver: ProviderDriver = { snapshot, adapter, textGeneration, + usage, } satisfies ProviderInstance; }), }; diff --git a/apps/server/src/provider/Drivers/ClaudeHome.test.ts b/apps/server/src/provider/Drivers/ClaudeHome.test.ts index a7ff41fc89e..ddd605a608f 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.test.ts @@ -39,11 +39,31 @@ it.layer(NodeServices.layer)("ClaudeHome", (it) => { }), ); + it.effect("uses the effective environment HOME when no explicit override is configured", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const environmentHome = path.resolve(NodeOS.tmpdir(), "claude-environment-home"); + const environment = { ...process.env, HOME: environmentHome }; + + expect(yield* resolveClaudeHomePath({ homePath: "" }, environment)).toBe(environmentHome); + expect(yield* makeClaudeEnvironment({ homePath: "" }, environment)).toBe(environment); + expect(yield* makeClaudeContinuationGroupKey({ homePath: "" }, environment)).toBe( + `claude:home:${environmentHome}`, + ); + expect( + yield* makeClaudeCapabilitiesCacheKey( + { binaryPath: "claude", homePath: "" }, + environment, + ), + ).toBe(`claude\0${environmentHome}\0`); + }), + ); + it.effect("separates capability probes by cwd", () => Effect.gen(function* () { const config = { binaryPath: "claude", homePath: "" }; - const first = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-a"); - const second = yield* makeClaudeCapabilitiesCacheKey(config, "/repo-b"); + const first = yield* makeClaudeCapabilitiesCacheKey(config, process.env, "/repo-a"); + const second = yield* makeClaudeCapabilitiesCacheKey(config, process.env, "/repo-b"); expect(first).not.toBe(second); }), ); diff --git a/apps/server/src/provider/Drivers/ClaudeHome.ts b/apps/server/src/provider/Drivers/ClaudeHome.ts index b3ef22b640c..7c33ed15834 100644 --- a/apps/server/src/provider/Drivers/ClaudeHome.ts +++ b/apps/server/src/provider/Drivers/ClaudeHome.ts @@ -8,10 +8,14 @@ import { expandHomePath } from "../../pathExpansion.ts"; export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function* ( config: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, ): Effect.fn.Return { const path = yield* Path.Path; const homePath = config.homePath.trim(); - return path.resolve(homePath.length > 0 ? expandHomePath(homePath) : NodeOS.homedir()); + const environmentHome = baseEnv.HOME?.trim(); + return path.resolve( + homePath.length > 0 ? expandHomePath(homePath) : environmentHome || NodeOS.homedir(), + ); }); export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* ( @@ -21,7 +25,7 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function const resolvedBaseEnv = baseEnv ?? process.env; const homePath = config.homePath.trim(); if (homePath.length === 0) return resolvedBaseEnv; - const resolvedHomePath = yield* resolveClaudeHomePath(config); + const resolvedHomePath = yield* resolveClaudeHomePath(config, resolvedBaseEnv); return { ...resolvedBaseEnv, // Isolate this instance's config via CLAUDE_CONFIG_DIR rather than HOME. @@ -35,8 +39,11 @@ export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function }); export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationGroupKey")( - function* (config: Pick): Effect.fn.Return { - const resolvedHomePath = yield* resolveClaudeHomePath(config); + function* ( + config: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, + ): Effect.fn.Return { + const resolvedHomePath = yield* resolveClaudeHomePath(config, baseEnv); return `claude:home:${resolvedHomePath}`; }, ); @@ -44,9 +51,10 @@ export const makeClaudeContinuationGroupKey = Effect.fn("makeClaudeContinuationG export const makeClaudeCapabilitiesCacheKey = Effect.fn("makeClaudeCapabilitiesCacheKey")( function* ( config: Pick, + baseEnv: NodeJS.ProcessEnv = process.env, cwd?: string, ): Effect.fn.Return { - const resolvedHomePath = yield* resolveClaudeHomePath(config); + const resolvedHomePath = yield* resolveClaudeHomePath(config, baseEnv); return `${config.binaryPath}\0${resolvedHomePath}\0${cwd ?? ""}`; }, ); diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index ffcc94ca77d..a939b7aa2bd 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -36,6 +36,7 @@ import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeCodexAdapter } from "../Layers/CodexAdapter.ts"; +import { makeCodexUsage } from "../Layers/CodexUsage.ts"; import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts"; import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; @@ -161,6 +162,11 @@ export const CodexDriver: ProviderDriver = { ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), }); const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv); + const usage = yield* makeCodexUsage( + effectiveConfig, + { instanceId, driverKind: DRIVER_KIND, displayName }, + processEnv, + ); // Build a managed snapshot whose settings never change — mutations come // in as instance rebuilds from the registry rather than in-place @@ -209,6 +215,7 @@ export const CodexDriver: ProviderDriver = { snapshot, adapter, textGeneration, + usage, } satisfies ProviderInstance; }), }; diff --git a/apps/server/src/provider/Drivers/GrokDriver.ts b/apps/server/src/provider/Drivers/GrokDriver.ts index 4eb32c20c47..5d96c06f0d1 100644 --- a/apps/server/src/provider/Drivers/GrokDriver.ts +++ b/apps/server/src/provider/Drivers/GrokDriver.ts @@ -13,6 +13,7 @@ import { ServerSettingsService } from "../../serverSettings.ts"; import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeGrokAdapter } from "../Layers/GrokAdapter.ts"; +import { makeGrokUsage } from "../Layers/GrokUsage.ts"; import { buildInitialGrokProviderSnapshot, checkGrokProviderStatus, @@ -87,6 +88,7 @@ export const GrokDriver: ProviderDriver = { const crypto = yield* Crypto.Crypto; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const httpClient = yield* HttpClient.HttpClient; + const serverConfig = yield* ServerConfig; const serverSettings = yield* ServerSettingsService; const eventLoggers = yield* ProviderEventLoggers; const processEnv = mergeProviderInstanceEnvironment(environment); @@ -112,6 +114,12 @@ export const GrokDriver: ProviderDriver = { instanceId, }); const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv); + const usage = yield* makeGrokUsage( + effectiveConfig, + { instanceId, driverKind: DRIVER_KIND, displayName }, + processEnv, + serverConfig.cwd, + ); const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv).pipe( Effect.map(stampIdentity), @@ -159,6 +167,7 @@ export const GrokDriver: ProviderDriver = { snapshot, adapter, textGeneration, + usage, } satisfies ProviderInstance; }), }; diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index af8f5d6704b..a57cf54ff78 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -389,7 +389,7 @@ function toTitleCaseWords(value: string): string { return parts.join(" "); } -function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined { +export function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined { const normalized = subscriptionType?.toLowerCase().replace(/[\s_-]+/g, ""); if (!normalized) return undefined; diff --git a/apps/server/src/provider/Layers/ClaudeUsage.test.ts b/apps/server/src/provider/Layers/ClaudeUsage.test.ts new file mode 100644 index 00000000000..423ed31c0e7 --- /dev/null +++ b/apps/server/src/provider/Layers/ClaudeUsage.test.ts @@ -0,0 +1,252 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { describe, expect, it } from "@effect/vitest"; +import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import { HttpClient, HttpClientResponse } from "effect/unstable/http"; + +import { + claudeRetryAfterMillis, + claudeUsagePlanLabel, + makeClaudeUsage, + mapClaudeUsageResponse, + parseClaudeOauthCredentials, +} from "./ClaudeUsage.ts"; + +describe("mapClaudeUsageResponse", () => { + it("maps all supported windows and enabled extra usage", () => { + const result = mapClaudeUsageResponse({ + five_hour: { utilization: 25, resets_at: "2025-01-15T12:00:00.000Z" }, + seven_day: { utilization: 50, resets_at: "2025-01-20T12:00:00.000Z" }, + seven_day_sonnet: { utilization: 75, resets_at: "2025-01-20T12:00:00.000Z" }, + limits: [ + { + scope: { model: { display_name: "Haiku" } }, + percent: 40, + resets_at: "2025-01-20T12:00:00.000Z", + }, + { + scope: { model: { display_name: "Haiku" } }, + percent: 99, + resets_at: "2025-01-21T12:00:00.000Z", + }, + ], + extra_usage: { + is_enabled: true, + used_credits: 1_234, + monthly_limit: 5_000, + }, + }); + + expect(result.windows).toEqual([ + { + id: "five_hour", + label: "Session", + kind: "session", + usedPercent: 25, + resetsAt: "2025-01-15T12:00:00.000Z", + windowMinutes: 300, + }, + { + id: "seven_day", + label: "Weekly", + kind: "weekly", + usedPercent: 50, + resetsAt: "2025-01-20T12:00:00.000Z", + windowMinutes: 10_080, + }, + { + id: "model:sonnet", + label: "Sonnet", + kind: "model", + usedPercent: 75, + resetsAt: "2025-01-20T12:00:00.000Z", + windowMinutes: 10_080, + }, + { + id: "model:haiku", + label: "Haiku", + kind: "model", + usedPercent: 40, + resetsAt: "2025-01-20T12:00:00.000Z", + windowMinutes: 10_080, + }, + ]); + expect(result.credits).toEqual({ + label: "Extra usage", + usedCredits: 12.34, + monthlyLimit: 50, + }); + }); + + it("derives model labels from dynamic response values", () => { + const result = mapClaudeUsageResponse({ + seven_day_fable: { utilization: 20 }, + limits: [ + { + scope: { model: { display_name: "SomeNewModel" } }, + percent: 30, + }, + ], + }); + + expect(result.windows).toEqual([ + { + id: "model:fable", + label: "Fable", + kind: "model", + usedPercent: 20, + windowMinutes: 10_080, + }, + { + id: "model:somenewmodel", + label: "SomeNewModel", + kind: "model", + usedPercent: 30, + windowMinutes: 10_080, + }, + ]); + }); + + it("returns only session and weekly windows when no model windows are present", () => { + const result = mapClaudeUsageResponse({ + five_hour: { utilization: 20 }, + seven_day: { utilization: 30 }, + }); + + expect(result.windows.map((window) => window.id)).toEqual(["five_hour", "seven_day"]); + }); + + it("omits credits when extra usage is disabled or absent", () => { + expect(mapClaudeUsageResponse({ extra_usage: { is_enabled: false } }).credits).toBeUndefined(); + expect(mapClaudeUsageResponse({}).credits).toBeUndefined(); + }); + + it("clamps utilization to a valid percentage", () => { + const result = mapClaudeUsageResponse({ five_hour: { utilization: 130 } }); + + expect(result.windows[0]?.usedPercent).toBe(100); + }); + + it("returns an empty result for non-record and empty inputs", () => { + expect(mapClaudeUsageResponse(null)).toEqual({ windows: [], credits: undefined }); + expect(mapClaudeUsageResponse([])).toEqual({ windows: [], credits: undefined }); + expect(mapClaudeUsageResponse({})).toEqual({ windows: [], credits: undefined }); + }); +}); + +describe("parseClaudeOauthCredentials", () => { + it("parses valid Claude OAuth credentials", () => { + expect( + parseClaudeOauthCredentials({ + claudeAiOauth: { + accessToken: "token", + expiresAt: 1_700_000_000_000, + subscriptionType: "max", + rateLimitTier: "default_claude_max_20x", + scopes: ["user:profile", "other:scope"], + }, + }), + ).toEqual({ + accessToken: "token", + expiresAt: 1_700_000_000_000, + subscriptionType: "max", + rateLimitTier: "default_claude_max_20x", + scopes: ["user:profile", "other:scope"], + }); + }); + + it("rejects missing OAuth envelopes and access tokens", () => { + expect(parseClaudeOauthCredentials({})).toBeUndefined(); + expect(parseClaudeOauthCredentials({ claudeAiOauth: {} })).toBeUndefined(); + }); + + it("drops invalid scopes while preserving the valid credentials", () => { + expect( + parseClaudeOauthCredentials({ + claudeAiOauth: { accessToken: "token", scopes: "user:profile" }, + }), + ).toEqual({ + accessToken: "token", + expiresAt: undefined, + subscriptionType: undefined, + rateLimitTier: undefined, + scopes: undefined, + }); + expect( + parseClaudeOauthCredentials({ + claudeAiOauth: { accessToken: "token", scopes: ["user:profile", 42] }, + }), + ).toMatchObject({ scopes: undefined }); + }); +}); + +describe("Claude usage metadata", () => { + it("adds a rate-limit multiplier without duplicating one already in the plan", () => { + expect(claudeUsagePlanLabel("max", "default_claude_max_20x")).toBe("Max 20x"); + expect(claudeUsagePlanLabel("claude_max_5x_subscription", "default_claude_max_5x")).toBe( + "Max 5x", + ); + expect(claudeUsagePlanLabel("pro", undefined)).toBe("Pro"); + }); + + it("parses Retry-After seconds and dates with a five-minute fallback", () => { + const now = Date.parse("2026-07-18T10:00:00.000Z"); + expect(claudeRetryAfterMillis("90", now)).toBe(now + 90_000); + expect(claudeRetryAfterMillis("Sat, 18 Jul 2026 10:02:00 GMT", now)).toBe(now + 120_000); + expect(claudeRetryAfterMillis("invalid", now)).toBe(now + 5 * 60_000); + }); +}); + +it.layer(NodeServices.layer)("Claude usage cooldown", (it) => { + it.effect("serves the last good snapshot and avoids repeated requests during a 429", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const configDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-usage-" }); + yield* fs.writeFileString( + path.join(configDir, ".credentials.json"), + '{"claudeAiOauth":{"accessToken":"test-token","subscriptionType":"pro","scopes":["user:profile"]}}', + ); + + let requestCount = 0; + const httpClient = HttpClient.make((request) => + Effect.sync(() => { + requestCount += 1; + const response = + requestCount === 1 + ? Response.json({ five_hour: { utilization: 25 } }) + : new Response(null, { status: 429, headers: { "retry-after": "300" } }); + return HttpClientResponse.fromWeb(request, response); + }), + ); + const usage = yield* makeClaudeUsage( + { homePath: configDir }, + { + instanceId: ProviderInstanceId.make("claudeAgent"), + driverKind: ProviderDriverKind.make("claudeAgent"), + displayName: undefined, + }, + ).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); + + const fresh = yield* usage.fetchUsage; + const throttled = yield* usage.fetchUsage; + const duringCooldown = yield* usage.fetchUsage; + + expect(requestCount).toBe(2); + expect(fresh.status).toBe("ok"); + expect(throttled).toMatchObject({ + status: "ok", + windows: fresh.windows, + freshness: { state: "stale" }, + }); + expect(duringCooldown).toMatchObject({ + status: "ok", + windows: fresh.windows, + freshness: { state: "stale" }, + }); + expect(throttled.freshness?.retryAt).toBe(duringCooldown.freshness?.retryAt); + }).pipe(Effect.scoped), + ); +}); diff --git a/apps/server/src/provider/Layers/ClaudeUsage.ts b/apps/server/src/provider/Layers/ClaudeUsage.ts new file mode 100644 index 00000000000..a423cec3f0c --- /dev/null +++ b/apps/server/src/provider/Layers/ClaudeUsage.ts @@ -0,0 +1,439 @@ +/** + * ClaudeUsage — account-level subscription usage for the Claude driver. + * + * Reads the Claude Code OAuth token from `/.credentials.json` + * (the same CLAUDE_CONFIG_DIR the driver runs the CLI with; defaults to + * `$HOME/.claude` when no instance config dir is set) and calls Anthropic's + * OAuth usage endpoint. The endpoint is undocumented and shared with Claude + * Code itself, so the response is parsed leniently: unknown fields are + * ignored, model-scoped window labels come from the response verbatim + * (model names are renamed upstream without notice), and shape drift + * degrades to fewer windows rather than an error. + * + * Deliberate v1 boundaries: + * - Credentials-file only. No macOS Keychain fallback — server nodes are + * typically headless Linux; a missing file is surfaced as + * `unauthenticated` with CLI login guidance. + * - No OAuth token refresh. Writing a rotated refresh token back would + * race Claude Code's own refresh; an expired token is reported as + * `unauthenticated` and heals the next time the CLI runs. + * + * @module provider/Layers/ClaudeUsage + */ +import type { + ClaudeSettings, + ProviderDriverKind, + ProviderInstanceId, + ProviderUsageCredits, + ProviderUsageSnapshot, + ProviderUsageWindow, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Ref from "effect/Ref"; +import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; +import { HttpClient, HttpClientRequest } from "effect/unstable/http"; + +import type { ProviderUsageShape } from "../Services/ProviderUsage.ts"; +import { resolveClaudeHomePath } from "../Drivers/ClaudeHome.ts"; +import { claudeSubscriptionLabel } from "./ClaudeProvider.ts"; + +const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"; +const CLAUDE_OAUTH_BETA_HEADER = "oauth-2025-04-20"; +// The usage endpoint is only served to Claude Code's own OAuth client, so +// present as a recent CLI build the same way other local usage tools do. +const CLAUDE_USAGE_USER_AGENT = "claude-code/2.1.0"; +const CLAUDE_USAGE_TIMEOUT = Duration.seconds(15); +const CLAUDE_RATE_LIMIT_FALLBACK = Duration.minutes(5); +const CLAUDE_PROFILE_SCOPE = "user:profile"; + +const SIGN_IN_MESSAGE = "Sign in with the `claude` CLI on the server machine."; +const decodeUnknownJsonString = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); + +export interface ClaudeUsageMeta { + readonly instanceId: ProviderInstanceId; + readonly driverKind: ProviderDriverKind; + readonly displayName: string | undefined; +} + +interface ClaudeOauthCredentials { + readonly accessToken: string; + readonly expiresAt: number | undefined; + readonly subscriptionType: string | undefined; + readonly rateLimitTier: string | undefined; + readonly scopes: ReadonlyArray | undefined; +} + +interface ClaudeUsageState { + readonly credentialKey: string; + readonly lastGood: ProviderUsageSnapshot | undefined; + readonly cooldownUntilMillis: number | undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function numberField(record: Record, key: string): number | undefined { + const value = record[key]; + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function stringField(record: Record, key: string): string | undefined { + const value = record[key]; + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + +function clampPercent(value: number): number { + return Math.min(100, Math.max(0, value)); +} + +function toTitleCaseWords(value: string): string { + const parts: Array = []; + for (const part of value.split(/[\s_-]+/g)) { + if (part.length > 0) { + parts.push(part[0]!.toUpperCase() + part.slice(1).toLowerCase()); + } + } + return parts.join(" "); +} + +export function parseClaudeOauthCredentials(raw: unknown): ClaudeOauthCredentials | undefined { + if (!isRecord(raw)) return undefined; + const oauth = raw["claudeAiOauth"]; + if (!isRecord(oauth)) return undefined; + const accessToken = stringField(oauth, "accessToken"); + if (!accessToken) return undefined; + const scopes = oauth["scopes"]; + return { + accessToken, + expiresAt: numberField(oauth, "expiresAt"), + subscriptionType: stringField(oauth, "subscriptionType"), + rateLimitTier: stringField(oauth, "rateLimitTier"), + scopes: + Array.isArray(scopes) && scopes.every((scope) => typeof scope === "string") + ? scopes + : undefined, + }; +} + +export function claudeUsagePlanLabel( + subscriptionType: string | undefined, + rateLimitTier: string | undefined, +): string | undefined { + const base = claudeSubscriptionLabel(subscriptionType); + const multiplier = /\d+x/i.exec(rateLimitTier ?? "")?.[0]?.toLowerCase(); + if (!base || !multiplier || base.toLowerCase().includes(multiplier)) return base; + return `${base} ${multiplier}`; +} + +/** Resolve Retry-After seconds or an HTTP date, with a conservative fallback. */ +export function claudeRetryAfterMillis(value: string | undefined, nowMillis: number): number { + const trimmed = value?.trim(); + if (trimmed) { + const seconds = Number(trimmed); + if (Number.isFinite(seconds) && seconds >= 0) { + return nowMillis + Math.ceil(seconds * 1000); + } + const dateMillis = Date.parse(trimmed); + if (Number.isFinite(dateMillis) && dateMillis > nowMillis) return dateMillis; + } + return nowMillis + Duration.toMillis(CLAUDE_RATE_LIMIT_FALLBACK); +} + +const WEEK_MINUTES = 7 * 24 * 60; +const SESSION_WINDOW_MINUTES = 5 * 60; + +function usageWindowFromJson(input: { + readonly raw: unknown; + readonly id: string; + readonly label: string; + readonly kind: ProviderUsageWindow["kind"]; + readonly windowMinutes: number | undefined; +}): ProviderUsageWindow | undefined { + if (!isRecord(input.raw)) return undefined; + const utilization = numberField(input.raw, "utilization"); + if (utilization === undefined) return undefined; + const resetsAt = stringField(input.raw, "resets_at"); + return { + id: input.id, + label: input.label, + kind: input.kind, + usedPercent: clampPercent(utilization), + ...(resetsAt ? { resetsAt } : {}), + ...(input.windowMinutes !== undefined ? { windowMinutes: input.windowMinutes } : {}), + }; +} + +/** + * Map the raw `/api/oauth/usage` JSON to normalized windows + credits. + * Exported for fixture-driven tests. Every field is optional — unknown or + * missing pieces produce fewer windows, never a failure. + */ +export function mapClaudeUsageResponse(raw: unknown): { + readonly windows: ReadonlyArray; + readonly credits: ProviderUsageCredits | undefined; +} { + if (!isRecord(raw)) return { windows: [], credits: undefined }; + + const windowsById = new Map(); + const addWindow = (window: ProviderUsageWindow | undefined) => { + if (window && !windowsById.has(window.id)) windowsById.set(window.id, window); + }; + + addWindow( + usageWindowFromJson({ + raw: raw["five_hour"], + id: "five_hour", + label: "Session", + kind: "session", + windowMinutes: SESSION_WINDOW_MINUTES, + }), + ); + addWindow( + usageWindowFromJson({ + raw: raw["seven_day"], + id: "seven_day", + label: "Weekly", + kind: "weekly", + windowMinutes: WEEK_MINUTES, + }), + ); + + // Model-scoped weekly windows appear both as `seven_day_` keys and + // as `limits[]` entries depending on account/era. Labels are derived from + // the payload (key suffix or display name) — never hardcoded model names. + for (const [key, value] of Object.entries(raw)) { + const match = /^seven_day_(.+)$/.exec(key); + if (!match) continue; + const modelSlug = match[1]!; + addWindow( + usageWindowFromJson({ + raw: value, + id: `model:${modelSlug.toLowerCase()}`, + label: toTitleCaseWords(modelSlug), + kind: "model", + windowMinutes: WEEK_MINUTES, + }), + ); + } + + const limits = raw["limits"]; + if (Array.isArray(limits)) { + for (const entry of limits) { + if (!isRecord(entry)) continue; + const scope = entry["scope"]; + const model = isRecord(scope) ? scope["model"] : undefined; + const displayName = isRecord(model) ? stringField(model, "display_name") : undefined; + if (!displayName) continue; + const percent = numberField(entry, "percent"); + if (percent === undefined) continue; + const resetsAt = stringField(entry, "resets_at"); + addWindow({ + id: `model:${displayName.toLowerCase()}`, + label: displayName, + kind: "model", + usedPercent: clampPercent(percent), + ...(resetsAt ? { resetsAt } : {}), + windowMinutes: WEEK_MINUTES, + }); + } + } + + let credits: ProviderUsageCredits | undefined; + const extraUsage = raw["extra_usage"]; + if (isRecord(extraUsage) && extraUsage["is_enabled"] === true) { + const usedCents = numberField(extraUsage, "used_credits"); + const limitCents = numberField(extraUsage, "monthly_limit"); + credits = { + label: "Extra usage", + ...(usedCents !== undefined ? { usedCredits: usedCents / 100 } : {}), + ...(limitCents !== undefined ? { monthlyLimit: limitCents / 100 } : {}), + }; + } + + return { windows: Array.from(windowsById.values()), credits }; +} + +/** + * Build the usage capability for one Claude instance. Captures the + * infrastructure services at create time so `fetchUsage` runs with + * `R = never`, matching the other instance shapes. + */ +export const makeClaudeUsage = Effect.fn("makeClaudeUsage")(function* ( + config: Pick, + meta: ClaudeUsageMeta, + environment: NodeJS.ProcessEnv = process.env, +): Effect.fn.Return< + ProviderUsageShape, + never, + FileSystem.FileSystem | HttpClient.HttpClient | Path.Path +> { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const httpClient = yield* HttpClient.HttpClient; + // Mirror where the driver's CLI stores credentials: an instance homePath is + // used directly as CLAUDE_CONFIG_DIR; otherwise the CLI falls back to the + // environment's CLAUDE_CONFIG_DIR, then $HOME/.claude. + const resolvedHome = yield* resolveClaudeHomePath(config, environment); + const configDir = + config.homePath.trim().length > 0 + ? resolvedHome + : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(resolvedHome, ".claude"); + const credentialsPath = path.join(configDir, ".credentials.json"); + const state = yield* Ref.make(undefined); + + const fetchUsage = Effect.gen(function* () { + const fetchedAt = DateTime.formatIso(yield* DateTime.now); + const base = { + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + windows: [], + fetchedAt, + } satisfies Partial & { windows: ReadonlyArray }; + const failed = ( + status: "unauthenticated" | "error", + message: string, + ): ProviderUsageSnapshot => ({ ...base, status, message }); + + const rawCredentials = yield* fs.readFileString(credentialsPath).pipe(Effect.result); + if (Result.isFailure(rawCredentials)) { + return failed("unauthenticated", SIGN_IN_MESSAGE); + } + const credentialsJson = yield* decodeUnknownJsonString(rawCredentials.success).pipe( + Effect.result, + ); + if (Result.isFailure(credentialsJson)) { + return failed("unauthenticated", SIGN_IN_MESSAGE); + } + const credentials = parseClaudeOauthCredentials(credentialsJson.success); + if (!credentials) { + return failed("unauthenticated", SIGN_IN_MESSAGE); + } + + const now = yield* DateTime.now; + const nowMillis = DateTime.toEpochMillis(now); + let currentState = yield* Ref.get(state); + if (currentState?.credentialKey !== credentials.accessToken) { + currentState = { + credentialKey: credentials.accessToken, + lastGood: undefined, + cooldownUntilMillis: undefined, + }; + yield* Ref.set(state, currentState); + } + if (credentials.expiresAt !== undefined && credentials.expiresAt <= nowMillis) { + return failed( + "unauthenticated", + "Claude token expired — it refreshes the next time Claude runs on this machine.", + ); + } + if (credentials.scopes && !credentials.scopes.includes(CLAUDE_PROFILE_SCOPE)) { + return failed( + "unauthenticated", + "The stored Claude token cannot read usage — sign in again with a recent `claude` CLI (a full login, not `claude setup-token`).", + ); + } + + const planLabel = claudeUsagePlanLabel(credentials.subscriptionType, credentials.rateLimitTier); + const rateLimitedSnapshot = ( + usageState: ClaudeUsageState, + cooldownUntilMillis: number, + ): ProviderUsageSnapshot => { + const retryAt = DateTime.formatIso(DateTime.makeUnsafe(cooldownUntilMillis)); + const message = "Live Claude usage is rate limited. Showing the last available values."; + const freshness = { state: "stale" as const, retryAt }; + return usageState.lastGood + ? { ...usageState.lastGood, freshness, message } + : { + ...base, + status: "error", + ...(planLabel ? { planLabel } : {}), + freshness, + message: "Live Claude usage is rate limited. No previous values are available yet.", + }; + }; + if ( + currentState.cooldownUntilMillis !== undefined && + currentState.cooldownUntilMillis > nowMillis + ) { + return rateLimitedSnapshot(currentState, currentState.cooldownUntilMillis); + } + + const request = HttpClientRequest.get(CLAUDE_USAGE_URL).pipe( + HttpClientRequest.bearerToken(credentials.accessToken), + HttpClientRequest.setHeader("anthropic-beta", CLAUDE_OAUTH_BETA_HEADER), + HttpClientRequest.setHeader("user-agent", CLAUDE_USAGE_USER_AGENT), + HttpClientRequest.acceptJson, + ); + const response = yield* httpClient + .execute(request) + .pipe(Effect.timeoutOption(CLAUDE_USAGE_TIMEOUT), Effect.result); + if (Result.isFailure(response)) { + return failed("error", `Claude usage request failed: ${String(response.failure)}`); + } + if (Option.isNone(response.success)) { + return failed("error", "Claude usage request timed out."); + } + const httpResponse = response.success.value; + if (httpResponse.status === 429) { + const cooldownUntilMillis = claudeRetryAfterMillis( + httpResponse.headers["retry-after"], + nowMillis, + ); + currentState = { ...currentState, cooldownUntilMillis }; + yield* Ref.set(state, currentState); + return rateLimitedSnapshot(currentState, cooldownUntilMillis); + } + if (httpResponse.status === 401 || httpResponse.status === 403) { + return failed( + "unauthenticated", + httpResponse.status === 403 + ? "The stored Claude token cannot read usage — sign in again with a recent `claude` CLI." + : SIGN_IN_MESSAGE, + ); + } + if (httpResponse.status < 200 || httpResponse.status >= 300) { + return failed("error", `Claude usage endpoint returned HTTP ${httpResponse.status}.`); + } + const payload = yield* httpResponse.json.pipe(Effect.result); + if (Result.isFailure(payload)) { + return failed("error", "Claude usage endpoint returned an unreadable response."); + } + + const { windows, credits } = mapClaudeUsageResponse(payload.success); + const snapshot = { + ...base, + status: "ok", + ...(planLabel ? { planLabel } : {}), + windows, + ...(credits ? { credits } : {}), + } satisfies ProviderUsageSnapshot; + yield* Ref.set(state, { + credentialKey: credentials.accessToken, + lastGood: snapshot, + cooldownUntilMillis: undefined, + }); + return snapshot; + }).pipe( + Effect.catchDefect((defect: unknown) => + Effect.map(DateTime.now, (now) => ({ + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + status: "error" as const, + windows: [], + message: `Claude usage fetch crashed: ${String(defect)}`, + fetchedAt: DateTime.formatIso(now), + })), + ), + ); + + return { fetchUsage } satisfies ProviderUsageShape; +}); diff --git a/apps/server/src/provider/Layers/CodexUsage.test.ts b/apps/server/src/provider/Layers/CodexUsage.test.ts new file mode 100644 index 00000000000..73c61b12e28 --- /dev/null +++ b/apps/server/src/provider/Layers/CodexUsage.test.ts @@ -0,0 +1,220 @@ +import { describe, expect, it } from "@effect/vitest"; +import type * as CodexSchema from "effect-codex-app-server/schema"; + +import { codexEpochToIso, codexPlanLabel, mapCodexRateLimits } from "./CodexUsage.ts"; + +const makeRateLimitsResponse = ( + rateLimits: Record, + overrides: Record = {}, +): CodexSchema.V2GetAccountRateLimitsResponse => + ({ rateLimits, ...overrides }) as unknown as CodexSchema.V2GetAccountRateLimitsResponse; + +describe("mapCodexRateLimits", () => { + it("maps primary, secondary, spend, credits, and plan usage", () => { + const result = mapCodexRateLimits( + makeRateLimitsResponse( + { + primary: { usedPercent: 20, resetsAt: 1_700_000_000, windowDurationMins: 300 }, + secondary: { usedPercent: 80, resetsAt: 1_700_000_000_000 }, + individualLimit: { + remainingPercent: 60, + resetsAt: 1_700_000_000, + limit: "100", + used: "40", + }, + credits: { unlimited: true, hasCredits: false }, + planType: "pro", + }, + { + rateLimitResetCredits: { + availableCount: 2, + credits: [ + { + id: "later", + status: "available", + resetType: "codexRateLimits", + grantedAt: 1_700_000_000, + expiresAt: 1_700_200_000, + title: "Later reset", + }, + { + id: "used", + status: "redeemed", + resetType: "codexRateLimits", + grantedAt: 1_700_000_000, + }, + { + id: "sooner", + status: "available", + resetType: "codexRateLimits", + grantedAt: 1_700_000_000, + expiresAt: 1_700_100_000, + description: "Expires first", + }, + ], + }, + }, + ), + ); + + expect(result.windows).toEqual([ + { + id: "primary", + label: "Session", + kind: "session", + usedPercent: 20, + resetsAt: "2023-11-14T22:13:20.000Z", + windowMinutes: 300, + }, + { + id: "secondary", + label: "Weekly", + kind: "weekly", + usedPercent: 80, + resetsAt: "2023-11-14T22:13:20.000Z", + windowMinutes: 10_080, + }, + { + id: "spend", + label: "Spend limit", + kind: "other", + usedPercent: 40, + resetsAt: "2023-11-14T22:13:20.000Z", + }, + ]); + expect(result.credits).toEqual({ label: "Credits", unlimited: true }); + expect(result.resetCredits).toEqual({ + availableCount: 2, + credits: [ + { + id: "sooner", + description: "Expires first", + expiresAt: "2023-11-16T02:00:00.000Z", + }, + { + id: "later", + title: "Later reset", + expiresAt: "2023-11-17T05:46:40.000Z", + }, + ], + }); + expect(result.planLabel).toBe("ChatGPT Pro 20x"); + }); + + it("omits a null secondary window", () => { + const result = mapCodexRateLimits( + makeRateLimitsResponse({ + primary: { usedPercent: 10 }, + secondary: null, + }), + ); + + expect(result.windows).toEqual([ + { + id: "primary", + label: "Session", + kind: "session", + usedPercent: 10, + windowMinutes: 300, + }, + ]); + }); + + it("classifies a sole weekly window by duration even when Codex puts it in primary", () => { + const result = mapCodexRateLimits( + makeRateLimitsResponse({ + primary: { usedPercent: 35, windowDurationMins: 10_080 }, + secondary: null, + }), + ); + + expect(result.windows).toEqual([ + { + id: "primary", + label: "Weekly", + kind: "weekly", + usedPercent: 35, + windowMinutes: 10_080, + }, + ]); + }); + + it("maps additional limit buckets without hardcoding model names", () => { + const result = mapCodexRateLimits( + makeRateLimitsResponse( + { limitId: "codex", planType: "plus" }, + { + rateLimitsByLimitId: { + codex: { + limitId: "codex", + primary: { usedPercent: 10, windowDurationMins: 300 }, + secondary: { usedPercent: 20, windowDurationMins: 10_080 }, + }, + "gpt-5.3-codex-spark": { + limitId: "gpt-5.3-codex-spark", + limitName: "GPT-5.3-Codex-Spark", + primary: { usedPercent: 30, windowDurationMins: 300 }, + secondary: { usedPercent: 40, windowDurationMins: 10_080 }, + }, + }, + }, + ), + ); + + expect(result.windows).toEqual([ + { + id: "primary", + label: "Session", + kind: "session", + usedPercent: 10, + windowMinutes: 300, + }, + { + id: "secondary", + label: "Weekly", + kind: "weekly", + usedPercent: 20, + windowMinutes: 10_080, + }, + { + id: "limit:gpt-5.3-codex-spark:session", + label: "GPT-5.3-Codex-Spark", + kind: "model", + usedPercent: 30, + windowMinutes: 300, + }, + { + id: "limit:gpt-5.3-codex-spark:weekly", + label: "GPT-5.3-Codex-Spark Weekly", + kind: "model", + usedPercent: 40, + windowMinutes: 10_080, + }, + ]); + }); +}); + +describe("codexEpochToIso", () => { + it("accepts seconds and millisecond epochs while rejecting invalid epochs", () => { + expect(codexEpochToIso(1_700_000_000)).toBe("2023-11-14T22:13:20.000Z"); + expect(codexEpochToIso(1_700_000_000_000)).toBe("2023-11-14T22:13:20.000Z"); + expect(codexEpochToIso(0)).toBeUndefined(); + expect(codexEpochToIso(-1)).toBeUndefined(); + expect(codexEpochToIso(Number.NaN)).toBeUndefined(); + expect(codexEpochToIso(null)).toBeUndefined(); + }); +}); + +describe("codexPlanLabel", () => { + it("maps known plans and omits unmapped values", () => { + expect(codexPlanLabel("pro")).toBe("ChatGPT Pro 20x"); + expect(codexPlanLabel("plus")).toBe("ChatGPT Plus"); + expect(codexPlanLabel("unknown")).toBe("ChatGPT"); + expect( + codexPlanLabel( + "not-a-plan" as unknown as CodexSchema.V2GetAccountRateLimitsResponse__PlanType, + ), + ).toBeUndefined(); + expect(codexPlanLabel(null)).toBeUndefined(); + }); +}); diff --git a/apps/server/src/provider/Layers/CodexUsage.ts b/apps/server/src/provider/Layers/CodexUsage.ts new file mode 100644 index 00000000000..e316378310a --- /dev/null +++ b/apps/server/src/provider/Layers/CodexUsage.ts @@ -0,0 +1,378 @@ +/** + * CodexUsage — account-level subscription usage for the Codex driver. + * + * Spawns a short-lived `codex app-server` (the same probe pattern as + * `CodexProvider`) and asks it for `account/rateLimits/read`. Auth and token + * refresh stay entirely inside the codex CLI — this module never touches + * `auth.json`, which avoids racing the CLI's own token rotation. + * + * @module provider/Layers/CodexUsage + */ +import type { + CodexSettings, + ProviderDriverKind, + ProviderInstanceId, + ProviderUsageCredits, + ProviderUsageResetCredits, + ProviderUsageSnapshot, + ProviderUsageWindow, +} from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Result from "effect/Result"; +import * as ChildProcess from "effect/unstable/process/ChildProcess"; +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; +import * as CodexClient from "effect-codex-app-server/client"; +import type * as CodexSchema from "effect-codex-app-server/schema"; + +import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import { expandHomePath } from "../../pathExpansion.ts"; +import type { ProviderUsageShape } from "../Services/ProviderUsage.ts"; +import { buildCodexInitializeParams } from "./CodexProvider.ts"; + +const CODEX_USAGE_TIMEOUT = Duration.seconds(15); +const CODEX_USAGE_FORCE_KILL_AFTER = "2 seconds" as const; +const WEEK_MINUTES = 7 * 24 * 60; +const SESSION_WINDOW_MINUTES = 5 * 60; + +const LOGIN_MESSAGE = "Run `codex login` on the server machine."; + +export interface CodexUsageMeta { + readonly instanceId: ProviderInstanceId; + readonly driverKind: ProviderDriverKind; + readonly displayName: string | undefined; +} + +function clampPercent(value: number): number { + return Math.min(100, Math.max(0, value)); +} + +function toTitleCaseWords(value: string): string { + return value + .split(/[\s_-]+/g) + .filter((part) => part.length > 0) + .map((part) => part[0]!.toUpperCase() + part.slice(1).toLowerCase()) + .join(" "); +} + +/** + * `resetsAt` is an int64 epoch with an unspecified unit; treat values large + * enough to only make sense as milliseconds as milliseconds and everything + * else as seconds (10^12 ms ≈ 2001-09, far below any plausible reset time). + */ +export function codexEpochToIso(epoch: number | null | undefined): string | undefined { + if (typeof epoch !== "number" || !Number.isFinite(epoch) || epoch <= 0) return undefined; + const millis = epoch >= 1e12 ? epoch : epoch * 1000; + return DateTime.formatIso(DateTime.makeUnsafe(millis)); +} + +export function codexPlanLabel( + planType: CodexSchema.V2GetAccountRateLimitsResponse__PlanType | null | undefined, +): string | undefined { + switch (planType) { + case "free": + return "ChatGPT Free"; + case "go": + return "ChatGPT Go"; + case "plus": + return "ChatGPT Plus"; + case "pro": + return "ChatGPT Pro 20x"; + case "prolite": + return "ChatGPT Pro 5x"; + case "team": + return "ChatGPT Team"; + case "self_serve_business_usage_based": + case "business": + return "ChatGPT Business"; + case "enterprise_cbp_usage_based": + case "enterprise": + return "ChatGPT Enterprise"; + case "edu": + return "ChatGPT Edu"; + case "unknown": + return "ChatGPT"; + default: + return undefined; + } +} + +type CodexRateLimitSnapshot = CodexSchema.V2GetAccountRateLimitsResponse["rateLimits"]; +type ClassifiedWindowKind = "session" | "weekly"; + +function exactWindowKind( + window: CodexSchema.V2GetAccountRateLimitsResponse__RateLimitWindow, +): ClassifiedWindowKind | undefined { + if (window.windowDurationMins === SESSION_WINDOW_MINUTES) return "session"; + if (window.windowDurationMins === WEEK_MINUTES) return "weekly"; + return undefined; +} + +function rateLimitWindow(input: { + readonly window: CodexSchema.V2GetAccountRateLimitsResponse__RateLimitWindow; + readonly id: string; + readonly label: string; + readonly kind: ProviderUsageWindow["kind"]; + readonly fallbackWindowMinutes: number; +}): ProviderUsageWindow { + const resetsAt = codexEpochToIso(input.window.resetsAt); + const windowMinutes = input.window.windowDurationMins ?? input.fallbackWindowMinutes; + return { + id: input.id, + label: input.label, + kind: input.kind, + usedPercent: clampPercent(input.window.usedPercent), + ...(resetsAt ? { resetsAt } : {}), + windowMinutes, + }; +} + +/** + * Codex can move a temporarily sole weekly limit into the primary slot. Use + * explicit duration first, then fall back to the historical slot meaning only + * when duration is absent or unfamiliar. + */ +function classifiedRateLimitWindows(input: { + readonly rateLimits: CodexRateLimitSnapshot; + readonly idPrefix?: string; + readonly modelLabel?: string; +}): ReadonlyArray { + const candidates = [ + input.rateLimits.primary + ? { window: input.rateLimits.primary, fallbackKind: "session" as const, slot: "primary" } + : undefined, + input.rateLimits.secondary + ? { window: input.rateLimits.secondary, fallbackKind: "weekly" as const, slot: "secondary" } + : undefined, + ].filter((candidate): candidate is NonNullable => candidate !== undefined); + + const result: Array = []; + for (const kind of ["session", "weekly"] as const) { + const candidate = + candidates.find((entry) => exactWindowKind(entry.window) === kind) ?? + candidates.find( + (entry) => exactWindowKind(entry.window) === undefined && entry.fallbackKind === kind, + ); + if (!candidate) continue; + const modelLabel = input.modelLabel; + result.push( + rateLimitWindow({ + window: candidate.window, + id: input.idPrefix ? `${input.idPrefix}:${kind}` : candidate.slot, + label: + modelLabel !== undefined + ? kind === "weekly" + ? `${modelLabel} Weekly` + : modelLabel + : kind === "weekly" + ? "Weekly" + : "Session", + kind: modelLabel !== undefined ? "model" : kind, + fallbackWindowMinutes: kind === "weekly" ? WEEK_MINUTES : SESSION_WINDOW_MINUTES, + }), + ); + } + return result; +} + +function mapCodexResetCredits( + summary: CodexSchema.V2GetAccountRateLimitsResponse["rateLimitResetCredits"], +): ProviderUsageResetCredits | undefined { + if (!summary) return undefined; + const credits = (summary.credits ?? []).flatMap((credit) => { + if (credit.status !== "available" || credit.id.trim().length === 0) return []; + const expiresAt = codexEpochToIso(credit.expiresAt); + return [ + { + id: credit.id, + ...(credit.title?.trim() ? { title: credit.title.trim() } : {}), + ...(credit.description?.trim() ? { description: credit.description.trim() } : {}), + ...(expiresAt ? { expiresAt } : {}), + }, + ]; + }); + credits.sort((a, b) => (a.expiresAt ?? "\uffff").localeCompare(b.expiresAt ?? "\uffff")); + return { + availableCount: Math.max(0, Math.floor(summary.availableCount)), + credits, + }; +} + +/** + * Map `account/rateLimits/read` to normalized windows + credits + plan. + * Exported for fixture-driven tests. + */ +export function mapCodexRateLimits(response: CodexSchema.V2GetAccountRateLimitsResponse): { + readonly windows: ReadonlyArray; + readonly credits: ProviderUsageCredits | undefined; + readonly resetCredits: ProviderUsageResetCredits | undefined; + readonly planLabel: string | undefined; +} { + const rateLimits = response.rateLimits; + const byLimitId = Object.entries(response.rateLimitsByLimitId ?? {}); + const coreEntry = + (rateLimits.limitId + ? byLimitId.find(([limitId]) => limitId === rateLimits.limitId) + : undefined) ?? + byLimitId.find(([limitId]) => limitId.toLowerCase() === "codex") ?? + (byLimitId.length === 1 ? byLimitId[0] : undefined); + const coreRateLimits = coreEntry?.[1] ?? rateLimits; + const windows: Array = [ + ...classifiedRateLimitWindows({ rateLimits: coreRateLimits }), + ]; + + for (const [limitId, bucket] of byLimitId) { + if (limitId === coreEntry?.[0]) continue; + const label = bucket.limitName?.trim() || toTitleCaseWords(limitId); + windows.push( + ...classifiedRateLimitWindows({ + rateLimits: bucket, + idPrefix: `limit:${limitId.toLowerCase()}`, + modelLabel: label, + }), + ); + } + + const individualLimit = rateLimits.individualLimit; + if (individualLimit) { + const resetsAt = codexEpochToIso(individualLimit.resetsAt); + windows.push({ + id: "spend", + label: "Spend limit", + kind: "other", + usedPercent: clampPercent(100 - individualLimit.remainingPercent), + ...(resetsAt ? { resetsAt } : {}), + }); + } + + const rawCredits = rateLimits.credits; + const credits: ProviderUsageCredits | undefined = + rawCredits && (rawCredits.unlimited || rawCredits.hasCredits || rawCredits.balance) + ? { + label: "Credits", + ...(rawCredits.balance ? { balance: rawCredits.balance } : {}), + ...(rawCredits.unlimited ? { unlimited: true } : {}), + } + : undefined; + + return { + windows, + credits, + resetCredits: mapCodexResetCredits(response.rateLimitResetCredits), + planLabel: codexPlanLabel(rateLimits.planType ?? coreRateLimits.planType), + }; +} + +function codexAccountEmail( + account: CodexSchema.V2GetAccountResponse["account"], +): string | undefined { + if (!account || account.type !== "chatgpt") return undefined; + return account.email ?? undefined; +} + +/** + * Build the usage capability for one Codex instance. Captures the spawner at + * create time so `fetchUsage` runs with `R = never`. + */ +export const makeCodexUsage = Effect.fn("makeCodexUsage")(function* ( + config: Pick, + meta: CodexUsageMeta, + environment?: NodeJS.ProcessEnv, +): Effect.fn.Return { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const resolvedEnvironment = environment ?? process.env; + + const fetchUsage = Effect.gen(function* () { + const fetchedAt = DateTime.formatIso(yield* DateTime.now); + const base = { + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + windows: [], + fetchedAt, + } satisfies Partial & { windows: ReadonlyArray }; + const failed = ( + status: "unauthenticated" | "error", + message: string, + ): ProviderUsageSnapshot => ({ ...base, status, message }); + + const result = yield* Effect.gen(function* () { + const resolvedHomePath = config.homePath ? expandHomePath(config.homePath) : undefined; + const spawnEnvironment = { + ...resolvedEnvironment, + ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}), + }; + const spawnCommand = yield* resolveSpawnCommand(config.binaryPath, ["app-server"], { + env: spawnEnvironment, + extendEnv: true, + }); + const child = yield* spawner.spawn( + ChildProcess.make(spawnCommand.command, spawnCommand.args, { + cwd: process.cwd(), + env: spawnEnvironment, + extendEnv: true, + forceKillAfter: CODEX_USAGE_FORCE_KILL_AFTER, + shell: spawnCommand.shell, + }), + ); + const clientContext = yield* Layer.build(CodexClient.layerChildProcess(child)); + const client = yield* Effect.service(CodexClient.CodexAppServerClient).pipe( + Effect.provide(clientContext), + ); + yield* client.request("initialize", buildCodexInitializeParams()); + yield* client.notify("initialized", undefined); + + const account = yield* client.request("account/read", {}); + if (!account.account && account.requiresOpenaiAuth) { + return { unauthenticated: true as const }; + } + const rateLimits = yield* client.request("account/rateLimits/read", undefined); + return { + unauthenticated: false as const, + rateLimits, + email: codexAccountEmail(account.account), + }; + }).pipe(Effect.scoped, Effect.timeoutOption(CODEX_USAGE_TIMEOUT), Effect.result); + + if (Result.isFailure(result)) { + const error = result.failure; + const message = error instanceof Error ? error.message : String(error); + return failed("error", `Codex usage probe failed: ${message}`); + } + if (Option.isNone(result.success)) { + return failed("error", "Codex usage probe timed out."); + } + const probe = result.success.value; + if (probe.unauthenticated) { + return failed("unauthenticated", LOGIN_MESSAGE); + } + + const { windows, credits, resetCredits, planLabel } = mapCodexRateLimits(probe.rateLimits); + return { + ...base, + ...(probe.email ? { account: probe.email } : {}), + status: "ok", + ...(planLabel ? { planLabel } : {}), + windows, + ...(credits ? { credits } : {}), + ...(resetCredits ? { resetCredits } : {}), + } satisfies ProviderUsageSnapshot; + }).pipe( + Effect.catchDefect((defect: unknown) => + Effect.map(DateTime.now, (now) => ({ + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + status: "error" as const, + windows: [], + message: `Codex usage fetch crashed: ${String(defect)}`, + fetchedAt: DateTime.formatIso(now), + })), + ), + ); + + return { fetchUsage } satisfies ProviderUsageShape; +}); diff --git a/apps/server/src/provider/Layers/GrokUsage.test.ts b/apps/server/src/provider/Layers/GrokUsage.test.ts new file mode 100644 index 00000000000..e86c80cd3d2 --- /dev/null +++ b/apps/server/src/provider/Layers/GrokUsage.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { mapGrokBillingResponse } from "./GrokUsage.ts"; + +describe("mapGrokBillingResponse", () => { + it("maps Grok's weekly shared pool, plan, and pay-as-you-go cap", () => { + const result = mapGrokBillingResponse({ + config: { + creditUsagePercent: 42, + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-07-14T12:00:00+00:00", + end: "2026-07-21T12:00:00+00:00", + }, + onDemandCap: { val: 2500 }, + subscription_tier: "supergrok_heavy", + }, + }); + + expect(result).toEqual({ + recognized: true, + windows: [ + { + id: "weekly", + label: "Weekly", + kind: "weekly", + usedPercent: 42, + resetsAt: "2026-07-21T12:00:00.000Z", + windowMinutes: 10_080, + }, + ], + credits: { label: "Extra usage", balance: "2500 cap" }, + planLabel: "SuperGrok Heavy", + }); + }); + + it("treats omitted proto zero values as zero and does not mislabel monthly accounts", () => { + expect( + mapGrokBillingResponse({ + config: { + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "2026-07-14T12:00:00Z", + end: "2026-07-21T12:00:00Z", + }, + }, + }), + ).toMatchObject({ + recognized: true, + windows: [{ usedPercent: 0 }], + credits: { balance: "Disabled" }, + }); + + expect( + mapGrokBillingResponse({ + config: { + creditUsagePercent: 50, + currentPeriod: { + type: "USAGE_PERIOD_TYPE_MONTHLY", + start: "2026-07-01T00:00:00Z", + end: "2026-08-01T00:00:00Z", + }, + }, + }).windows, + ).toEqual([]); + }); + + it("rejects unrecognized and malformed payloads", () => { + expect(mapGrokBillingResponse(null).recognized).toBe(false); + expect(mapGrokBillingResponse({ config: { unrelated: true } }).recognized).toBe(false); + expect( + mapGrokBillingResponse({ + config: { + creditUsagePercent: "bad", + currentPeriod: { + type: "USAGE_PERIOD_TYPE_WEEKLY", + start: "bad", + end: "also-bad", + }, + }, + }).windows, + ).toEqual([]); + }); +}); diff --git a/apps/server/src/provider/Layers/GrokUsage.ts b/apps/server/src/provider/Layers/GrokUsage.ts new file mode 100644 index 00000000000..580dc9f55a8 --- /dev/null +++ b/apps/server/src/provider/Layers/GrokUsage.ts @@ -0,0 +1,255 @@ +/** + * GrokUsage — account-level subscription usage through Grok Build's ACP + * billing extension. Authentication and token refresh remain owned by the + * Grok CLI; T3 never reads or writes `~/.grok/auth.json`. + * + * @module provider/Layers/GrokUsage + */ +import type { + GrokSettings, + ProviderDriverKind, + ProviderInstanceId, + ProviderUsageCredits, + ProviderUsageSnapshot, + ProviderUsageWindow, +} from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Result from "effect/Result"; +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; + +import { makeGrokAcpRuntime } from "../acp/GrokAcpSupport.ts"; +import type { ProviderUsageShape } from "../Services/ProviderUsage.ts"; + +const GROK_BILLING_METHOD = "x.ai/billing"; +const GROK_USAGE_TIMEOUT = Duration.seconds(15); +const WEEK_MINUTES = 7 * 24 * 60; + +export interface GrokUsageMeta { + readonly instanceId: ProviderInstanceId; + readonly driverKind: ProviderDriverKind; + readonly displayName: string | undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringField(record: Record, ...keys: ReadonlyArray) { + for (const key of keys) { + const value = record[key]; + if (typeof value === "string" && value.trim().length > 0) return value.trim(); + } + return undefined; +} + +function numberValue(value: unknown): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (!isRecord(value)) return undefined; + const nested = value["val"]; + return typeof nested === "number" && Number.isFinite(nested) ? nested : undefined; +} + +function normalizedIso(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + return DateTime.make(value).pipe( + Option.match({ + onNone: () => undefined, + onSome: DateTime.formatIso, + }), + ); +} + +function clampPercent(value: number): number { + return Math.max(0, Math.min(100, value)); +} + +function formatOpaqueNumber(value: number): string { + return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(2))); +} + +function formatPlanLabel(value: string | undefined): string | undefined { + if (!value) return undefined; + return value + .split(/[\s_-]+/g) + .filter((part) => part.length > 0) + .map((part) => + part.toLowerCase() === "supergrok" + ? "SuperGrok" + : part[0]!.toUpperCase() + part.slice(1).toLowerCase(), + ) + .join(" "); +} + +/** Map the proto-JSON payload returned by Grok's `x.ai/billing` extension. */ +export function mapGrokBillingResponse(raw: unknown): { + readonly recognized: boolean; + readonly windows: ReadonlyArray; + readonly credits: ProviderUsageCredits | undefined; + readonly planLabel: string | undefined; +} { + if (!isRecord(raw)) { + return { recognized: false, windows: [], credits: undefined, planLabel: undefined }; + } + const config = isRecord(raw["config"]) ? raw["config"] : raw; + const recognized = + "currentPeriod" in config || + "current_period" in config || + "creditUsagePercent" in config || + "credit_usage_percent" in config || + "onDemandCap" in config || + "on_demand_cap" in config; + if (!recognized) { + return { recognized: false, windows: [], credits: undefined, planLabel: undefined }; + } + + const windows: Array = []; + const periodValue = config["currentPeriod"] ?? config["current_period"]; + const period = isRecord(periodValue) ? periodValue : undefined; + const periodType = period ? stringField(period, "type") : undefined; + const periodStart = period ? normalizedIso(period["start"]) : undefined; + const periodEnd = period ? normalizedIso(period["end"]) : undefined; + const percentValue = config["creditUsagePercent"] ?? config["credit_usage_percent"]; + const usedPercent = percentValue === undefined ? 0 : numberValue(percentValue); + if ( + periodType === "USAGE_PERIOD_TYPE_WEEKLY" && + periodStart && + periodEnd && + Date.parse(periodEnd) > Date.parse(periodStart) && + usedPercent !== undefined + ) { + windows.push({ + id: "weekly", + label: "Weekly", + kind: "weekly", + usedPercent: clampPercent(usedPercent), + resetsAt: periodEnd, + windowMinutes: WEEK_MINUTES, + }); + } + + const cap = numberValue(config["onDemandCap"] ?? config["on_demand_cap"]) ?? 0; + const credits: ProviderUsageCredits = { + label: "Extra usage", + balance: cap > 0 ? `${formatOpaqueNumber(cap)} cap` : "Disabled", + }; + const planLabel = formatPlanLabel( + stringField(config, "subscriptionTier", "subscription_tier", "planName", "plan_name") ?? + stringField(raw, "subscriptionTier", "subscription_tier", "planName", "plan_name"), + ); + + return { recognized: true, windows, credits, planLabel }; +} + +function grokFailureStatus(message: string): "unauthenticated" | "unsupported" | "error" { + const normalized = message.toLowerCase(); + if ( + normalized.includes("authentication required") || + normalized.includes("unauthenticated") || + normalized.includes("not logged in") || + normalized.includes("grok login") + ) { + return "unauthenticated"; + } + if ( + normalized.includes("method not found") || + normalized.includes("unknown method") || + normalized.includes("unsupported method") + ) { + return "unsupported"; + } + return "error"; +} + +/** Build one Grok usage capability with all process services captured. */ +export const makeGrokUsage = Effect.fn("makeGrokUsage")(function* ( + config: Pick, + meta: GrokUsageMeta, + environment: NodeJS.ProcessEnv, + cwd: string, +): Effect.fn.Return< + ProviderUsageShape, + never, + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto +> { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const crypto = yield* Crypto.Crypto; + + const fetchUsage = Effect.gen(function* () { + const fetchedAt = DateTime.formatIso(yield* DateTime.now); + const base = { + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + windows: [], + fetchedAt, + } satisfies Partial & { windows: ReadonlyArray }; + const failed = ( + status: "unauthenticated" | "unsupported" | "error", + message: string, + ): ProviderUsageSnapshot => ({ ...base, status, message }); + + const result = yield* Effect.gen(function* () { + const acp = yield* makeGrokAcpRuntime({ + grokSettings: config, + environment, + childProcessSpawner, + cwd, + clientInfo: { name: "t3-code-usage", version: "0.0.0" }, + }); + yield* acp.start(); + return yield* acp.request(GROK_BILLING_METHOD, {}); + }).pipe( + Effect.scoped, + Effect.provideService(Crypto.Crypto, crypto), + Effect.timeoutOption(GROK_USAGE_TIMEOUT), + Effect.result, + ); + + if (Result.isFailure(result)) { + const error = result.failure; + const message = error instanceof Error ? error.message : String(error); + const status = grokFailureStatus(message); + return failed( + status, + status === "unauthenticated" + ? "Run `grok login` on the server machine." + : status === "unsupported" + ? "This Grok CLI does not expose billing usage. Update Grok and try again." + : `Grok usage probe failed: ${message}`, + ); + } + if (Option.isNone(result.success)) { + return failed("error", "Grok usage probe timed out."); + } + + const mapped = mapGrokBillingResponse(result.success.value); + if (!mapped.recognized) { + return failed("error", "Grok billing response changed or contained no usage data."); + } + return { + ...base, + status: "ok", + ...(mapped.planLabel ? { planLabel: mapped.planLabel } : {}), + windows: mapped.windows, + ...(mapped.credits ? { credits: mapped.credits } : {}), + } satisfies ProviderUsageSnapshot; + }).pipe( + Effect.catchDefect((defect: unknown) => + Effect.map(DateTime.now, (now) => ({ + instanceId: meta.instanceId, + driver: meta.driverKind, + ...(meta.displayName ? { displayName: meta.displayName } : {}), + status: "error" as const, + windows: [], + message: `Grok usage fetch crashed: ${String(defect)}`, + fetchedAt: DateTime.formatIso(now), + })), + ), + ); + + return { fetchUsage } satisfies ProviderUsageShape; +}); diff --git a/apps/server/src/provider/Layers/ProviderUsageService.test.ts b/apps/server/src/provider/Layers/ProviderUsageService.test.ts new file mode 100644 index 00000000000..bee8165c6cb --- /dev/null +++ b/apps/server/src/provider/Layers/ProviderUsageService.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, it } from "@effect/vitest"; +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderUsageSnapshot, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as PubSub from "effect/PubSub"; +import * as Stream from "effect/Stream"; + +import type { ProviderInstance } from "../ProviderDriver.ts"; +import type { ProviderUsageShape } from "../Services/ProviderUsage.ts"; +import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; +import { ProviderUsageService } from "../Services/ProviderUsage.ts"; +import { ProviderUsageServiceLive } from "./ProviderUsageService.ts"; + +const makeUsageSnapshot = ( + instanceId: string, + overrides: Partial = {}, +): ProviderUsageSnapshot => ({ + instanceId: ProviderInstanceId.make(instanceId), + driver: ProviderDriverKind.make("codex"), + status: "ok", + windows: [], + fetchedAt: "2025-01-15T10:00:00.000Z", + ...overrides, +}); + +const makeInstance = (input: { + readonly instanceId: string; + readonly enabled?: boolean; + readonly displayName?: string; + readonly accountEmail?: string; + readonly usage?: ProviderUsageShape; +}): ProviderInstance => { + const instanceId = ProviderInstanceId.make(input.instanceId); + const driverKind = ProviderDriverKind.make("codex"); + return { + instanceId, + driverKind, + continuationIdentity: { + driverKind, + continuationKey: `codex:instance:${instanceId}`, + }, + displayName: input.displayName, + enabled: input.enabled ?? true, + snapshot: { + getSnapshot: Effect.succeed({ + ...(input.accountEmail ? { auth: { email: input.accountEmail } } : {}), + }), + refresh: Effect.die("not used"), + streamChanges: Stream.empty, + maintenanceCapabilities: {}, + }, + usage: input.usage, + adapter: {}, + textGeneration: {}, + } as unknown as ProviderInstance; +}; + +const makeUsageLayer = (instances: ReadonlyArray) => { + const instanceRegistry = Layer.succeed( + ProviderInstanceRegistry, + ProviderInstanceRegistry.of({ + getInstance: (instanceId) => + Effect.succeed(instances.find((instance) => instance.instanceId === instanceId)), + listInstances: Effect.succeed(instances), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.flatMap(PubSub.unbounded(), (pubsub) => + PubSub.subscribe(pubsub), + ), + }), + ); + return ProviderUsageServiceLive.pipe(Layer.provide(instanceRegistry)); +}; + +describe("ProviderUsageServiceLive", () => { + it.effect("synthesizes unsupported snapshots for enabled instances without usage", () => { + const instance = makeInstance({ instanceId: "cursor" }); + return Effect.gen(function* () { + const service = yield* ProviderUsageService; + + const result = yield* service.getUsage(); + + expect(result.usage).toHaveLength(1); + expect(result.usage[0]).toMatchObject({ + instanceId: instance.instanceId, + driver: "codex", + status: "unsupported", + windows: [], + }); + }).pipe(Effect.provide(makeUsageLayer([instance]))); + }); + + it.effect("fills missing account identities and preserves fetched identities", () => { + const filled = makeInstance({ + instanceId: "codex_personal", + accountEmail: "snapshot@example.com", + usage: { + fetchUsage: Effect.succeed(makeUsageSnapshot("codex_personal")), + }, + }); + const preserved = makeInstance({ + instanceId: "codex_work", + accountEmail: "snapshot@example.com", + usage: { + fetchUsage: Effect.succeed( + makeUsageSnapshot("codex_work", { account: "fetcher@example.com" }), + ), + }, + }); + return Effect.gen(function* () { + const service = yield* ProviderUsageService; + + const result = yield* service.getUsage(); + + expect( + result.usage.find((snapshot) => snapshot.instanceId === filled.instanceId)?.account, + ).toBe("snapshot@example.com"); + expect( + result.usage.find((snapshot) => snapshot.instanceId === preserved.instanceId)?.account, + ).toBe("fetcher@example.com"); + }).pipe(Effect.provide(makeUsageLayer([filled, preserved]))); + }); + + it.effect("caches only successful snapshots", () => { + let okFetches = 0; + let errorFetches = 0; + const successful = makeInstance({ + instanceId: "codex_ok", + usage: { + fetchUsage: Effect.sync(() => { + okFetches += 1; + return makeUsageSnapshot("codex_ok"); + }), + }, + }); + const failing = makeInstance({ + instanceId: "codex_error", + usage: { + fetchUsage: Effect.sync(() => { + errorFetches += 1; + return makeUsageSnapshot("codex_error", { status: "error" }); + }), + }, + }); + return Effect.gen(function* () { + const service = yield* ProviderUsageService; + + yield* service.getUsage(successful.instanceId); + yield* service.getUsage(successful.instanceId); + yield* service.getUsage(failing.instanceId); + yield* service.getUsage(failing.instanceId); + + expect(okFetches).toBe(1); + expect(errorFetches).toBe(2); + }).pipe(Effect.provide(makeUsageLayer([successful, failing]))); + }); + + it.effect("filters to the requested enabled instance and excludes disabled instances", () => { + const requested = makeInstance({ + instanceId: "codex_personal", + usage: { fetchUsage: Effect.succeed(makeUsageSnapshot("codex_personal")) }, + }); + const other = makeInstance({ + instanceId: "codex_work", + usage: { fetchUsage: Effect.succeed(makeUsageSnapshot("codex_work")) }, + }); + const disabled = makeInstance({ + instanceId: "codex_disabled", + enabled: false, + usage: { fetchUsage: Effect.succeed(makeUsageSnapshot("codex_disabled")) }, + }); + return Effect.gen(function* () { + const service = yield* ProviderUsageService; + + const filtered = yield* service.getUsage(requested.instanceId); + const all = yield* service.getUsage(); + + expect(filtered.usage.map((snapshot) => snapshot.instanceId)).toEqual([requested.instanceId]); + expect(all.usage.map((snapshot) => snapshot.instanceId)).toEqual([ + requested.instanceId, + other.instanceId, + ]); + }).pipe(Effect.provide(makeUsageLayer([requested, other, disabled]))); + }); + + it.effect("isolates one provider usage error from healthy instances", () => { + const broken = makeInstance({ + instanceId: "codex_broken", + usage: { + fetchUsage: Effect.succeed( + makeUsageSnapshot("codex_broken", { status: "error", message: "upstream failed" }), + ), + }, + }); + const healthy = makeInstance({ + instanceId: "codex_healthy", + usage: { + fetchUsage: Effect.succeed( + makeUsageSnapshot("codex_healthy", { account: "healthy@example.com" }), + ), + }, + }); + return Effect.gen(function* () { + const service = yield* ProviderUsageService; + + const result = yield* service.getUsage(); + + expect(result.usage).toHaveLength(2); + expect( + result.usage.find((snapshot) => snapshot.instanceId === broken.instanceId), + ).toMatchObject({ + status: "error", + message: "upstream failed", + }); + expect( + result.usage.find((snapshot) => snapshot.instanceId === healthy.instanceId), + ).toMatchObject({ + status: "ok", + account: "healthy@example.com", + }); + }).pipe(Effect.provide(makeUsageLayer([broken, healthy]))); + }); +}); diff --git a/apps/server/src/provider/Layers/ProviderUsageService.ts b/apps/server/src/provider/Layers/ProviderUsageService.ts new file mode 100644 index 00000000000..f21b7345fd1 --- /dev/null +++ b/apps/server/src/provider/Layers/ProviderUsageService.ts @@ -0,0 +1,109 @@ +/** + * ProviderUsageServiceLive — aggregates per-instance usage capabilities into + * the `server.getProviderUsage` RPC result. + * + * Fresh `ok` snapshots are cached for a short TTL keyed by instance id. The + * cache exists to bound upstream traffic (Anthropic's usage endpoint, codex + * app-server spawns) under client pull-to-refresh, not to serve stale data: + * non-`ok` snapshots are never cached, so error/unauthenticated states are + * re-probed on every request. + * + * @module provider/Layers/ProviderUsageService + */ +import type { ProviderUsageSnapshot } from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; + +import type { ProviderInstance } from "../ProviderDriver.ts"; +import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; +import { ProviderUsageService } from "../Services/ProviderUsage.ts"; + +const USAGE_CACHE_TTL = Duration.seconds(60); + +interface CacheEntry { + readonly snapshot: ProviderUsageSnapshot; + readonly expiresAtMillis: number; +} + +const unsupportedSnapshot = ( + instance: ProviderInstance, + fetchedAt: string, +): ProviderUsageSnapshot => ({ + instanceId: instance.instanceId, + driver: instance.driverKind, + ...(instance.displayName ? { displayName: instance.displayName } : {}), + status: "unsupported", + windows: [], + fetchedAt, +}); + +/** + * Fill `account` (the cross-node dedupe identity) from the instance's + * provider snapshot when the fetcher didn't supply one. Only the auth email + * qualifies — auth labels ("Claude Max Subscription") are plan names, not + * account identities, and would wrongly merge different accounts on the + * same plan. + */ +const withAccountIdentity = ( + instance: ProviderInstance, + snapshot: ProviderUsageSnapshot, +): Effect.Effect => { + if (snapshot.account !== undefined) return Effect.succeed(snapshot); + return Effect.map(instance.snapshot.getSnapshot, (provider) => { + const email = provider.auth?.email; + return email ? { ...snapshot, account: email } : snapshot; + }); +}; + +export const ProviderUsageServiceLive = Layer.effect( + ProviderUsageService, + Effect.gen(function* () { + const instanceRegistry = yield* ProviderInstanceRegistry; + const cache = yield* Ref.make(new Map()); + + const fetchInstanceUsage = (instance: ProviderInstance) => + Effect.gen(function* () { + const now = yield* DateTime.now; + const nowMillis = DateTime.toEpochMillis(now); + if (!instance.usage) { + return unsupportedSnapshot(instance, DateTime.formatIso(now)); + } + const cached = (yield* Ref.get(cache)).get(instance.instanceId); + if (cached && cached.expiresAtMillis > nowMillis) { + return cached.snapshot; + } + const snapshot = yield* instance.usage.fetchUsage.pipe( + Effect.flatMap((fetched) => withAccountIdentity(instance, fetched)), + ); + if (snapshot.status === "ok") { + yield* Ref.update(cache, (entries) => { + const next = new Map(entries); + next.set(instance.instanceId, { + snapshot, + expiresAtMillis: nowMillis + Duration.toMillis(USAGE_CACHE_TTL), + }); + return next; + }); + } + return snapshot; + }); + + return { + getUsage: (instanceId) => + Effect.gen(function* () { + const instances = yield* instanceRegistry.listInstances; + const targets = instances.filter( + (instance) => + instance.enabled && (instanceId === undefined || instance.instanceId === instanceId), + ); + const usage = yield* Effect.all(targets.map(fetchInstanceUsage), { + concurrency: "unbounded", + }); + return { usage }; + }), + }; + }), +); diff --git a/apps/server/src/provider/ProviderDriver.ts b/apps/server/src/provider/ProviderDriver.ts index c738882c23a..9a9beafc151 100644 --- a/apps/server/src/provider/ProviderDriver.ts +++ b/apps/server/src/provider/ProviderDriver.ts @@ -33,6 +33,7 @@ import type * as Scope from "effect/Scope"; import type * as TextGeneration from "../textGeneration/TextGeneration.ts"; import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts"; import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts"; +import type { ProviderUsageShape } from "./Services/ProviderUsage.ts"; import type { ServerProviderShape } from "./Services/ServerProvider.ts"; /** @@ -71,6 +72,12 @@ export interface ProviderInstance { readonly snapshot: ServerProviderShape; readonly adapter: ProviderAdapterShape; readonly textGeneration: TextGeneration.TextGeneration["Service"]; + /** + * Optional account-usage capability. Absent when the driver cannot report + * subscription usage — the usage service surfaces those instances as + * `status: "unsupported"`. + */ + readonly usage?: ProviderUsageShape; } export interface ProviderContinuationIdentity { diff --git a/apps/server/src/provider/Services/ProviderUsage.ts b/apps/server/src/provider/Services/ProviderUsage.ts new file mode 100644 index 00000000000..115ea7c974b --- /dev/null +++ b/apps/server/src/provider/Services/ProviderUsage.ts @@ -0,0 +1,43 @@ +/** + * ProviderUsage — optional per-instance capability for account-level + * subscription usage (rate-limit windows, credits). + * + * Drivers that can report usage attach a `ProviderUsageShape` to their + * `ProviderInstance`; drivers that can't simply omit it and the aggregation + * service synthesizes an `unsupported` snapshot. Kept as a captured closure + * (not a Context tag) for the same reason as the other instance shapes — + * many instances of one driver, each with independent credentials. + * + * @module provider/Services/ProviderUsage + */ +import type { + ProviderInstanceId, + ProviderUsageResult, + ProviderUsageSnapshot, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; + +export interface ProviderUsageShape { + /** + * Fetch the current usage snapshot for this instance. Never fails — every + * failure mode (missing credentials, upstream errors, timeouts) is folded + * into the snapshot's `status`/`message` so one broken provider cannot + * fail an aggregate fetch. + */ + readonly fetchUsage: Effect.Effect; +} + +export interface ProviderUsageServiceShape { + /** + * Usage snapshots for every enabled instance (or just `instanceId` when + * supplied). Instances without the usage capability are included as + * `status: "unsupported"` so clients always see the full instance list. + */ + readonly getUsage: (instanceId?: ProviderInstanceId) => Effect.Effect; +} + +export class ProviderUsageService extends Context.Service< + ProviderUsageService, + ProviderUsageServiceShape +>()("t3/provider/Services/ProviderUsage/ProviderUsageService") {} diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index b1f6c1f7251..aebe0b6263f 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -82,6 +82,7 @@ import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSna import { SqlitePersistenceMemory } from "./persistence/Layers/Sqlite.ts"; import { PersistenceSqlError } from "./persistence/Errors.ts"; import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; +import * as ProviderUsage from "./provider/Services/ProviderUsage.ts"; import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; @@ -317,6 +318,7 @@ const buildAppUnderTest = (options?: { layers?: { keybindings?: Partial; providerRegistry?: Partial; + providerUsage?: Partial; serverSettings?: Partial; externalLauncher?: Partial; vcsDriver?: Partial; @@ -535,18 +537,27 @@ const buildAppUnderTest = (options?: { }), ), Layer.provide( - Layer.mock(ProviderRegistry.ProviderRegistry)({ - getProviders: Effect.succeed([]), - refresh: () => Effect.succeed([]), - refreshInstance: () => Effect.succeed([]), - getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) => - Effect.succeed( - makeManualOnlyProviderMaintenanceCapabilities({ provider, packageName: null }), - ), - setProviderMaintenanceActionState: () => Effect.succeed([]), - streamChanges: Stream.empty, - ...options?.layers?.providerRegistry, - }), + // Merged into one `Layer.provide` rather than a second chained call: + // this `.pipe(...)` sits at its typed 20-argument overload limit, so an + // extra provide collapses the whole layer's type to `unknown`. + Layer.mergeAll( + Layer.mock(ProviderRegistry.ProviderRegistry)({ + getProviders: Effect.succeed([]), + refresh: () => Effect.succeed([]), + refreshInstance: () => Effect.succeed([]), + getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) => + Effect.succeed( + makeManualOnlyProviderMaintenanceCapabilities({ provider, packageName: null }), + ), + setProviderMaintenanceActionState: () => Effect.succeed([]), + streamChanges: Stream.empty, + ...options?.layers?.providerRegistry, + }), + Layer.mock(ProviderUsage.ProviderUsageService)({ + getUsage: () => Effect.succeed({ usage: [] }), + ...options?.layers?.providerUsage, + }), + ), ), Layer.provide( Layer.mock(ServerSettings.ServerSettingsService)({ diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 0c632d8486c..a8a6b6e6ceb 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -52,6 +52,7 @@ import { ThreadDeletionReactorLive } from "./orchestration/Layers/ThreadDeletion import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import { ProviderRegistryLive } from "./provider/Layers/ProviderRegistry.ts"; +import { ProviderUsageServiceLive } from "./provider/Layers/ProviderUsageService.ts"; import * as ServerSettings from "./serverSettings.ts"; import * as ProjectFaviconResolver from "./project/ProjectFaviconResolver.ts"; import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; @@ -294,7 +295,10 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(Keybindings.layer), - Layer.provideMerge(ProviderRegistryLive), + // `ProviderUsageServiceLive` sits beside the snapshot registry — both read + // the instance registry provided further down this pipe. Merged into one + // `provideMerge` slot because `pipe` tops out at 20 arguments. + Layer.provideMerge(Layer.mergeAll(ProviderRegistryLive, ProviderUsageServiceLive)), // The instance registry is the new routing keystone — text generation, // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId` // through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 32dcb8a13d6..74e8f1b48e0 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -76,6 +76,7 @@ import { observeRpcStreamEffect as instrumentRpcStreamEffect, } from "./observability/RpcInstrumentation.ts"; import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; +import * as ProviderUsage from "./provider/Services/ProviderUsage.ts"; import * as ProviderMaintenanceRunner from "./provider/providerMaintenanceRunner.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; @@ -295,6 +296,7 @@ const RPC_REQUIRED_SCOPE = new Map([ [WS_METHODS.serverGetTraceDiagnostics, AuthOrchestrationReadScope], [WS_METHODS.serverGetProcessDiagnostics, AuthOrchestrationReadScope], [WS_METHODS.serverGetProcessResourceHistory, AuthOrchestrationReadScope], + [WS_METHODS.serverGetProviderUsage, AuthOrchestrationReadScope], [WS_METHODS.serverSignalProcess, AuthOrchestrationOperateScope], [WS_METHODS.cloudGetRelayClientStatus, AuthRelayWriteScope], [WS_METHODS.cloudInstallRelayClient, AuthRelayWriteScope], @@ -408,6 +410,7 @@ const makeWsRpcLayer = ( const previewManager = yield* PreviewManager.PreviewManager; const portDiscovery = yield* PortScanner.PortDiscovery; const providerRegistry = yield* ProviderRegistry.ProviderRegistry; + const providerUsage = yield* ProviderUsage.ProviderUsageService; const providerMaintenanceRunner = yield* ProviderMaintenanceRunner.ProviderMaintenanceRunner; const config = yield* ServerConfig.ServerConfig; const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents; @@ -1259,6 +1262,12 @@ const makeWsRpcLayer = ( "rpc.aggregate": "server", }, ), + [WS_METHODS.serverGetProviderUsage]: (input) => + observeRpcEffect( + WS_METHODS.serverGetProviderUsage, + providerUsage.getUsage(input.instanceId), + { "rpc.aggregate": "server" }, + ), [WS_METHODS.serverUpsertKeybinding]: (rule) => observeRpcEffect( WS_METHODS.serverUpsertKeybinding, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 6ec2e631d19..1ef074fd3cf 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -85,6 +85,7 @@ import { renderProviderTraitsPicker, } from "./composerProviderState"; import { ContextWindowMeter } from "./ContextWindowMeter"; +import { ProviderUsageAlert } from "./ProviderUsageControl"; import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview"; import { basenameOfPath } from "../../pierre-icons"; import { cn, randomUUID } from "~/lib/utils"; @@ -340,6 +341,8 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: { compact: boolean; + environmentId: EnvironmentId; + providerInstanceId: ProviderInstanceId; activeContextWindow: ReturnType; activeThreadProviderDisplayName: string | null; isPreparingWorktree: boolean; @@ -364,6 +367,11 @@ const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions( }) { return ( <> + {props.activeContextWindow ? ( 0; + const usageInstanceId = + selectedInstanceId === "favorites" ? props.activeInstanceId : selectedInstanceId; const instanceOrder = useMemo( () => instanceEntries.map((entry) => entry.instanceId), [instanceEntries], @@ -669,6 +674,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { No models found + {props.environmentId ? ( + + ) : null} diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index e3463631733..98059a7c73e 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -1,4 +1,5 @@ import { + type EnvironmentId, type ProviderInstanceId, type ProviderDriverKind, type ResolvedKeybindingsConfig, @@ -25,6 +26,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { * icon, label and the default-highlighted combobox row. */ activeInstanceId: ProviderInstanceId; + environmentId?: EnvironmentId; model: string; lockedProvider: ProviderDriverKind | null; lockedContinuationGroupKey?: string | null; @@ -192,6 +194,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { > = {}, +): ProviderUsagePresentation => ({ + account: "person@example.com", + credits: undefined, + displayName: "Codex", + driver: ProviderDriverKind.make("codex"), + fetchedAt: "2026-07-17T10:00:00.000Z", + instanceId: ProviderInstanceId.make("codex"), + message: undefined, + planLabel: "Pro", + resetCredits: undefined, + freshness: undefined, + sourceNodes: [], + status: "ok", + windows: [], + ...overrides, +}); + +const headline = (usedPercent: number | null): ProviderUsageHeadline => ({ + label: usedPercent === null ? "Unlimited" : `${100 - usedPercent}% left`, + usedPercent, +}); + +describe("shouldShowProviderUsageAlert", () => { + it("keeps healthy and nonnumeric usage out of the composer", () => { + expect(shouldShowProviderUsageAlert(makeUsage(), headline(3))).toBe(false); + expect(shouldShowProviderUsageAlert(makeUsage(), headline(74))).toBe(false); + expect(shouldShowProviderUsageAlert(makeUsage(), headline(null))).toBe(false); + }); + + it("shows low-capacity and unavailable usage", () => { + expect(shouldShowProviderUsageAlert(makeUsage(), headline(75))).toBe(true); + expect(shouldShowProviderUsageAlert(makeUsage(), headline(95))).toBe(true); + expect(shouldShowProviderUsageAlert(makeUsage({ status: "unauthenticated" }), null)).toBe(true); + expect(shouldShowProviderUsageAlert(makeUsage({ status: "error" }), null)).toBe(true); + expect( + shouldShowProviderUsageAlert(makeUsage({ freshness: { state: "stale" } }), headline(3)), + ).toBe(true); + }); + + it("alerts on nearly consumed credits only after the limits are exhausted", () => { + const healthySession = makeUsage({ + windows: [{ id: "session", label: "Session", kind: "session", usedPercent: 50 }], + credits: { label: "Credits", usedCredits: 95, monthlyLimit: 100 }, + }); + const healthyHeadline = deriveProviderUsageHeadline(healthySession); + expect(healthyHeadline).toEqual({ label: "50% left", usedPercent: 50 }); + expect(shouldShowProviderUsageAlert(healthySession, healthyHeadline)).toBe(false); + + const exhaustedLimits = makeUsage({ + windows: [ + { id: "session", label: "Session", kind: "session", usedPercent: 100 }, + { id: "weekly", label: "Weekly", kind: "weekly", usedPercent: 100 }, + ], + credits: { label: "Credits", usedCredits: 95, monthlyLimit: 100 }, + }); + const exhaustedHeadline = deriveProviderUsageHeadline(exhaustedLimits); + expect(exhaustedHeadline).toEqual({ label: "$5.00 left", usedPercent: 95 }); + expect(shouldShowProviderUsageAlert(exhaustedLimits, exhaustedHeadline)).toBe(true); + }); + + it("does not alert for unsupported providers", () => { + expect(shouldShowProviderUsageAlert(makeUsage({ status: "unsupported" }), null)).toBe(false); + }); +}); diff --git a/apps/web/src/components/chat/ProviderUsageControl.tsx b/apps/web/src/components/chat/ProviderUsageControl.tsx new file mode 100644 index 00000000000..663ec18047d --- /dev/null +++ b/apps/web/src/components/chat/ProviderUsageControl.tsx @@ -0,0 +1,236 @@ +import type { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts"; +import { Link } from "@tanstack/react-router"; +import { ChevronDownIcon, CircleAlertIcon, GaugeIcon, RefreshCwIcon } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; + +import { useEnvironmentQuery } from "../../state/query"; +import { serverEnvironment } from "../../state/server"; +import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; +import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; +import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; +import { + ProviderUsageDetails, + type ProviderUsageHeadline, + ProviderUsageIdentity, + type ProviderUsagePresentation, + deriveProviderUsageHeadline, + providerUsagePresentationFromSnapshot, +} from "../provider-usage/ProviderUsagePresentation"; + +const PROVIDER_USAGE_ALERT_THRESHOLD = 75; + +export function shouldShowProviderUsageAlert( + usage: ProviderUsagePresentation, + headline: ProviderUsageHeadline | null, +): boolean { + if (usage.status === "error" || usage.status === "unauthenticated") return true; + if (usage.freshness?.state === "stale") return true; + const usedPercent = headline?.usedPercent; + return ( + usage.status === "ok" && + typeof usedPercent === "number" && + usedPercent >= PROVIDER_USAGE_ALERT_THRESHOLD + ); +} + +function useProviderUsage({ + environmentId, + instanceId, +}: { + environmentId: EnvironmentId; + instanceId: ProviderInstanceId; +}) { + const query = useEnvironmentQuery( + serverEnvironment.providerUsage({ environmentId, input: { instanceId } }), + ); + const [nowMs, setNowMs] = useState(() => Date.now()); + useEffect(() => { + const interval = window.setInterval(() => setNowMs(Date.now()), 30_000); + return () => window.clearInterval(interval); + }, []); + + const snapshot = query.data?.usage.find((candidate) => candidate.instanceId === instanceId); + const usage = useMemo( + () => (snapshot ? providerUsagePresentationFromSnapshot(snapshot) : null), + [snapshot], + ); + const headline = usage ? deriveProviderUsageHeadline(usage) : null; + + return { usage, headline, isPending: query.isPending, refresh: query.refresh, nowMs }; +} + +function usageToneClass( + usage: ProviderUsagePresentation, + headline: ProviderUsageHeadline | null, +): string { + if (usage.status === "error") return "text-destructive"; + if (usage.status === "unauthenticated") return "text-amber-600 dark:text-amber-300"; + if (usage.freshness?.state === "stale") return "text-amber-600 dark:text-amber-300"; + const usedPercent = headline?.usedPercent ?? null; + if (usedPercent !== null && usedPercent >= 95) return "text-red-600 dark:text-red-400"; + if (usedPercent !== null && usedPercent >= 75) return "text-amber-600 dark:text-amber-300"; + return "text-muted-foreground/75"; +} + +function ProviderUsageExpandedContent({ + usage, + nowMs, + isPending, + onRefresh, +}: { + usage: ProviderUsagePresentation; + nowMs: number; + isPending: boolean; + onRefresh: () => void; +}) { + return ( +
+
+
+ +
+ +
+ +
+ Subscription limits + +
+
+ ); +} + +export function ProviderUsageAlert({ + environmentId, + instanceId, + compact, +}: { + environmentId: EnvironmentId; + instanceId: ProviderInstanceId; + compact: boolean; +}) { + const { usage, headline, isPending, refresh, nowMs } = useProviderUsage({ + environmentId, + instanceId, + }); + + if (!usage || !shouldShowProviderUsageAlert(usage, headline)) return null; + + const hasFailure = usage.status === "error" || usage.status === "unauthenticated"; + const ariaLabel = hasFailure + ? `${usage.displayName} subscription usage unavailable` + : `${usage.displayName} subscription usage: ${headline?.label ?? "available"}`; + + return ( + + + {hasFailure ? ( + + ) : ( + + )} + {!compact && headline ? ( + {headline.label} + ) : null} + + } + /> + + + + + ); +} + +export function ProviderUsageSelectorPanel({ + environmentId, + instanceId, +}: { + environmentId: EnvironmentId; + instanceId: ProviderInstanceId; +}) { + const [open, setOpen] = useState(false); + const { usage, headline, isPending, refresh, nowMs } = useProviderUsage({ + environmentId, + instanceId, + }); + + if (!usage || usage.status === "unsupported") return null; + if (usage.status === "ok" && headline === null) return null; + + const hasFailure = usage.status === "error" || usage.status === "unauthenticated"; + const summary = hasFailure ? "Unavailable" : (headline?.label ?? "Available"); + + return ( + + + {hasFailure ? ( + + ) : ( + + )} + + {usage.displayName} usage + + + {summary} + + + + +
+ +
+
+
+ ); +} diff --git a/apps/web/src/components/provider-usage/ProviderUsagePresentation.test.tsx b/apps/web/src/components/provider-usage/ProviderUsagePresentation.test.tsx new file mode 100644 index 00000000000..a1140eaf493 --- /dev/null +++ b/apps/web/src/components/provider-usage/ProviderUsagePresentation.test.tsx @@ -0,0 +1,204 @@ +import { describe, expect, it } from "@effect/vitest"; +import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { + buildProviderUsageSubtitle, + deriveProviderUsageHeadline, + ProviderUsageDetails, + type ProviderUsagePresentation, +} from "./ProviderUsagePresentation"; + +const makeUsage = ( + overrides: Partial = {}, +): ProviderUsagePresentation => ({ + account: "person@example.com", + credits: undefined, + displayName: "Codex", + driver: ProviderDriverKind.make("codex"), + fetchedAt: "2026-07-17T10:00:00.000Z", + instanceId: ProviderInstanceId.make("codex"), + message: undefined, + planLabel: "Pro", + resetCredits: undefined, + freshness: undefined, + sourceNodes: ["Local"], + status: "ok", + windows: [], + ...overrides, +}); + +describe("deriveProviderUsageHeadline", () => { + it("headlines the session limit while it has capacity, even when weekly is tighter", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "weekly", label: "Weekly", kind: "weekly", usedPercent: 85 }, + { id: "session", label: "Session", kind: "session", usedPercent: 20 }, + ], + }), + ); + + expect(headline).toEqual({ label: "80% left", usedPercent: 20 }); + }); + + it("moves to the weekly limit once the session limit is exhausted", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "session", label: "Session", kind: "session", usedPercent: 100 }, + { id: "weekly", label: "Weekly", kind: "weekly", usedPercent: 60 }, + ], + }), + ); + + expect(headline).toEqual({ label: "40% left", usedPercent: 60 }); + }); + + it("never lets credits pre-empt an unexhausted session limit", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [{ id: "session", label: "Session", kind: "session", usedPercent: 50 }], + credits: { label: "Extra usage", usedCredits: 95, monthlyLimit: 100 }, + }), + ); + + expect(headline).toEqual({ label: "50% left", usedPercent: 50 }); + }); + + it("never lets model windows pre-empt the session limit", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "session", label: "Session", kind: "session", usedPercent: 10 }, + { id: "model:opus", label: "Opus", kind: "model", usedPercent: 90 }, + ], + }), + ); + + expect(headline).toEqual({ label: "90% left", usedPercent: 10 }); + }); + + it("headlines consumed credits once session and weekly are exhausted", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "session", label: "Session", kind: "session", usedPercent: 100 }, + { id: "weekly", label: "Weekly", kind: "weekly", usedPercent: 100 }, + ], + credits: { label: "Extra usage", usedCredits: 12, monthlyLimit: 100 }, + }), + ); + + expect(headline).toEqual({ label: "$88.00 left", usedPercent: 12 }); + }); + + it("keeps showing the exhausted limit while metered credits are untouched", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "session", label: "Session", kind: "session", usedPercent: 100 }, + { id: "weekly", label: "Weekly", kind: "weekly", usedPercent: 100 }, + ], + credits: { label: "Extra usage", usedCredits: 0, monthlyLimit: 100 }, + }), + ); + + expect(headline).toEqual({ label: "0% left", usedPercent: 100 }); + }); + + it("falls back to a credit balance when no windows exist", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ credits: { label: "Credits", usedCredits: 12, monthlyLimit: 100 } }), + ); + + expect(headline).toEqual({ label: "$88.00 left", usedPercent: 12 }); + }); + + it("falls back to the most constrained remaining window without session/weekly limits", () => { + const headline = deriveProviderUsageHeadline( + makeUsage({ + windows: [ + { id: "model:opus", label: "Opus", kind: "model", usedPercent: 30 }, + { id: "spend", label: "Spend limit", kind: "other", usedPercent: 70 }, + ], + }), + ); + + expect(headline).toEqual({ label: "30% left", usedPercent: 70 }); + }); + + it("does not summarize non-success states", () => { + expect(deriveProviderUsageHeadline(makeUsage({ status: "unauthenticated" }))).toBeNull(); + }); +}); + +describe("buildProviderUsageSubtitle", () => { + it("combines account and source environments", () => { + expect(buildProviderUsageSubtitle("person@example.com", ["Local", "VPS"])).toBe( + "person@example.com · via Local, VPS", + ); + }); +}); + +describe("ProviderUsageDetails", () => { + it("renders meters only for limits with numeric usage data", () => { + const unlimitedMarkup = renderToStaticMarkup( + , + ); + const numericMarkup = renderToStaticMarkup( + , + ); + + expect(unlimitedMarkup).toContain("Unlimited"); + expect(unlimitedMarkup).not.toContain('role="progressbar"'); + expect(numericMarkup).toContain('role="progressbar"'); + }); + + it("renders stale-state guidance with the scheduled retry", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Showing cached usage after provider throttling."); + expect(markup).toContain("Retries in 5m"); + }); + + it("renders available rate-limit reset credits without a redemption action", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("1 available"); + expect(markup).toContain("Weekly reset"); + expect(markup).toContain("Expires in 1d 0h"); + expect(markup).not.toContain("Redeem"); + }); +}); diff --git a/apps/web/src/components/provider-usage/ProviderUsagePresentation.tsx b/apps/web/src/components/provider-usage/ProviderUsagePresentation.tsx new file mode 100644 index 00000000000..b8f82b842ec --- /dev/null +++ b/apps/web/src/components/provider-usage/ProviderUsagePresentation.tsx @@ -0,0 +1,349 @@ +import type { ProviderUsageSnapshot } from "@t3tools/contracts"; +import { + formatProviderUsageCredits, + formatProviderUsageExpiry, + formatProviderUsageReset, + formatProviderUsageRetry, + providerUsageCreditsHaveMeter, + providerUsageCreditsUsedPercent, + providerUsageDisplayName, + providerUsagePercentLeft, + type ProviderUsageCard, +} from "@t3tools/client-runtime/provider-usage"; +import { CircleAlertIcon, CircleUserRoundIcon } from "lucide-react"; + +import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; +import { cn } from "~/lib/utils"; + +export type ProviderUsagePresentation = Pick< + ProviderUsageCard, + | "account" + | "credits" + | "displayName" + | "driver" + | "fetchedAt" + | "instanceId" + | "message" + | "planLabel" + | "resetCredits" + | "freshness" + | "sourceNodes" + | "status" + | "windows" +>; + +export function providerUsagePresentationFromSnapshot( + snapshot: ProviderUsageSnapshot, + sourceNodes: ReadonlyArray = [], +): ProviderUsagePresentation { + return { + account: snapshot.account, + credits: snapshot.credits, + displayName: providerUsageDisplayName(snapshot.driver, snapshot.displayName), + driver: snapshot.driver, + fetchedAt: snapshot.fetchedAt, + instanceId: snapshot.instanceId, + message: snapshot.message, + planLabel: snapshot.planLabel, + resetCredits: snapshot.resetCredits, + freshness: snapshot.freshness, + sourceNodes, + status: snapshot.status, + windows: snapshot.windows, + }; +} + +export interface ProviderUsageHeadline { + readonly label: string; + readonly usedPercent: number | null; +} + +/** + * Pick the compact headline in strict priority order: the session limit while + * it has capacity, then the weekly limit, and only once both are exhausted + * the overflow credits ("extra usage") — and only while spend is actually + * flowing there. Model/other windows and credit balances never pre-empt a + * session/weekly limit that still has room. + */ +export function deriveProviderUsageHeadline( + usage: ProviderUsagePresentation, +): ProviderUsageHeadline | null { + if (usage.status !== "ok") return null; + + const toHeadline = (window: (typeof usage.windows)[number]): ProviderUsageHeadline => ({ + label: `${Math.round(providerUsagePercentLeft(window.usedPercent))}% left`, + usedPercent: window.usedPercent, + }); + const isExhausted = (window: (typeof usage.windows)[number]) => window.usedPercent >= 100; + + const session = usage.windows.find((window) => window.kind === "session"); + const weekly = usage.windows.find((window) => window.kind === "weekly"); + if (session && !isExhausted(session)) return toHeadline(session); + if (weekly && !isExhausted(weekly)) return toHeadline(weekly); + + const credits = usage.credits; + const creditLabel = credits + ? (formatProviderUsageCredits(credits)?.split(" · ")[0] ?? null) + : null; + const creditHeadline = + credits && creditLabel + ? { + label: creditLabel, + usedPercent: providerUsageCreditsHaveMeter(credits) + ? providerUsageCreditsUsedPercent(credits) + : null, + } + : null; + + const exhaustedLimit = session ?? weekly; + if (exhaustedLimit) { + // A metered credit balance with zero spend means overflow is not being + // consumed yet — keep showing the exhausted limit. An unmetered balance + // (e.g. Codex's opaque credits) cannot prove consumption, so surface it + // once the limits are dry. + const consumingCredits = + credits !== undefined && + (!providerUsageCreditsHaveMeter(credits) || providerUsageCreditsUsedPercent(credits) > 0); + return consumingCredits && creditHeadline ? creditHeadline : toHeadline(exhaustedLimit); + } + + // No session/weekly limits reported: fall back to the most constrained + // remaining window, then the credit balance. + const mostConstrainedWindow = usage.windows.reduce<(typeof usage.windows)[number] | null>( + (current, window) => + current === null || window.usedPercent > current.usedPercent ? window : current, + null, + ); + return mostConstrainedWindow ? toHeadline(mostConstrainedWindow) : creditHeadline; +} + +export function ProviderUsageIdentity({ + usage, + compact = false, +}: { + usage: ProviderUsagePresentation; + compact?: boolean; +}) { + const subtitle = buildProviderUsageSubtitle(usage.account, usage.sourceNodes); + return ( +
+ +
+
+ + {usage.displayName} + + {usage.planLabel ? ( + + {usage.planLabel} + + ) : null} +
+ {subtitle ? ( +
+ {subtitle} +
+ ) : null} +
+
+ ); +} + +export function ProviderUsageDetails({ + usage, + nowMs, + compact = false, +}: { + usage: ProviderUsagePresentation; + nowMs: number; + compact?: boolean; +}) { + if (usage.status === "unsupported") { + return ( +

Usage isn't available for this provider.

+ ); + } + + if (usage.status !== "ok") { + const isError = usage.status === "error"; + const Icon = isError ? CircleAlertIcon : CircleUserRoundIcon; + return ( +
+ + + {usage.message ?? + (isError ? "Couldn't load provider usage." : "Sign in to see provider usage.")} + +
+ ); + } + + const formattedCredits = usage.credits ? formatProviderUsageCredits(usage.credits) : null; + const resetCredits = usage.resetCredits; + const hasResetCredits = resetCredits !== undefined && resetCredits.availableCount > 0; + const isStale = usage.freshness?.state === "stale"; + const retry = formatProviderUsageRetry(usage.freshness?.retryAt, nowMs); + + return ( +
+ {isStale ? ( +
+ + + {usage.message ?? "Showing the most recent usage because live usage is unavailable."} + {retry ? ` ${retry}.` : null} + +
+ ) : null} + {usage.windows.length === 0 && !formattedCredits && !hasResetCredits ? ( +

No active limits reported.

+ ) : null} + {usage.windows.map((window) => ( + + ))} + {usage.credits && formattedCredits ? ( + + ) : null} + {resetCredits && resetCredits.availableCount > 0 ? ( + + ) : null} +
+ ); +} + +function ProviderUsageResetCreditsRow({ + resetCredits, + nowMs, +}: { + resetCredits: NonNullable; + nowMs: number; +}) { + return ( +
+
+ Rate limit resets + + {resetCredits.availableCount} available + +
+ {resetCredits.credits.length > 0 ? ( +
+ {resetCredits.credits.map((credit) => { + const expiry = formatProviderUsageExpiry(credit.expiresAt, nowMs); + return ( +
+
+ + {credit.title ?? "Rate limit reset"} + + {expiry ? ( + {expiry} + ) : null} +
+ {credit.description ? ( + {credit.description} + ) : null} +
+ ); + })} +
+ ) : null} +
+ ); +} + +export function ProviderUsageWindowRow({ + label, + usedPercent, + resetsAt, + nowMs, + valueText, + compact = false, +}: { + label: string; + usedPercent: number | null; + resetsAt?: string; + nowMs?: number; + valueText?: string; + compact?: boolean; +}) { + const resets = nowMs === undefined ? null : formatProviderUsageReset(resetsAt, nowMs); + const left = + valueText ?? + (usedPercent === null ? "" : `${Math.round(providerUsagePercentLeft(usedPercent))}% left`); + return ( +
+
+ {label} + {left} +
+ {usedPercent === null ? null : ( + + )} + {resets ? ( +
{resets}
+ ) : null} +
+ ); +} + +export function ProviderUsageMeter({ usedPercent, label }: { usedPercent: number; label: string }) { + const used = Math.max(0, Math.min(100, usedPercent)); + const fillWidth = Math.max(used, used > 0 ? 2 : 0); + const fillClass = used >= 95 ? "bg-red-500" : used >= 75 ? "bg-amber-500" : "bg-emerald-500"; + return ( +
+
+
+ ); +} + +export function buildProviderUsageSubtitle( + account: string | undefined, + sourceNodes: ReadonlyArray, +): string | null { + const parts: string[] = []; + if (account) parts.push(account); + if (sourceNodes.length > 1) parts.push(`via ${sourceNodes.join(", ")}`); + else if (sourceNodes.length === 1) parts.push(sourceNodes[0]!); + return parts.length > 0 ? parts.join(" · ") : null; +} diff --git a/apps/web/src/components/settings/ProviderUsageSettings.tsx b/apps/web/src/components/settings/ProviderUsageSettings.tsx new file mode 100644 index 00000000000..b87bb90b161 --- /dev/null +++ b/apps/web/src/components/settings/ProviderUsageSettings.tsx @@ -0,0 +1,255 @@ +import type { EnvironmentId } from "@t3tools/contracts"; +import { + aggregateProviderUsage, + areProviderUsageResultsComplete, + type EnvironmentUsageInput, + type ProviderUsageNodeStatus, +} from "@t3tools/client-runtime/provider-usage"; +import { GaugeIcon, LoaderCircleIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { useEnvironments } from "../../state/environments"; +import { useEnvironmentQuery } from "../../state/query"; +import { serverEnvironment } from "../../state/server"; +import { formatRelativeTimeLabel } from "../../timestampFormat"; +import { Button } from "../ui/button"; +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { + ProviderUsageDetails, + ProviderUsageIdentity, +} from "../provider-usage/ProviderUsagePresentation"; +import { SettingsPageContainer, SettingsSection, useRelativeTimeTick } from "./settingsLayout"; + +export function ProviderUsageSettings() { + const { isReady, environments } = useEnvironments(); + const sortedEnvironments = useMemo( + () => [...environments].sort((a, b) => a.label.localeCompare(b.label)), + [environments], + ); + const [results, setResults] = useState>({}); + const [refreshNonce, setRefreshNonce] = useState(0); + const [isRefreshing, setIsRefreshing] = useState(false); + const nowMs = useRelativeTimeTick(30_000); + + const handleResult = useCallback((next: EnvironmentUsageInput) => { + setResults((current) => { + const previous = current[next.environmentId]; + if ( + previous && + previous.snapshots === next.snapshots && + previous.isPending === next.isPending && + previous.error === next.error && + previous.environmentLabel === next.environmentLabel + ) { + return current; + } + return { ...current, [next.environmentId]: next }; + }); + }, []); + + const handleRemove = useCallback((environmentId: string) => { + setResults((current) => { + if (!(environmentId in current)) return current; + const next = { ...current }; + delete next[environmentId]; + return next; + }); + }, []); + + const aggregate = useMemo( + () => + aggregateProviderUsage( + sortedEnvironments.flatMap((environment) => { + const result = results[environment.environmentId]; + return result ? [result] : []; + }), + ), + [results, sortedEnvironments], + ); + + const handleRefresh = useCallback(() => { + setRefreshNonce((current) => current + 1); + }, []); + + useEffect(() => { + if (refreshNonce === 0) return; + setIsRefreshing(true); + const timeout = window.setTimeout(() => setIsRefreshing(false), 1_000); + return () => window.clearTimeout(timeout); + }, [refreshNonce]); + + const hasContent = + aggregate.cards.length > 0 || + aggregate.pendingNodes.length > 0 || + aggregate.failedNodes.length > 0; + const hasAllResults = areProviderUsageResultsComplete( + sortedEnvironments.map((environment) => environment.environmentId), + results, + ); + const isInitialLoading = + isReady && !hasContent && sortedEnvironments.length > 0 && !hasAllResults; + const newestFetchedAt = aggregate.cards.reduce( + (current, card) => (current === null || card.fetchedAt > current ? card.fetchedAt : current), + null, + ); + + return ( + + {sortedEnvironments.map((environment) => ( + + ))} + + + {newestFetchedAt ? ( + + Updated {formatRelativeTimeLabel(newestFetchedAt)} + + ) : null} + + + + + } + /> + Refresh provider usage + +
+ } + > + {!isReady || isInitialLoading ? ( +
+ + Loading usage… +
+ ) : !hasContent ? ( + + + + + + No usage to show + + Connect an environment signed in to a supported provider to see its subscription + limits here. + + + + ) : ( +
+ {aggregate.cards.map((card) => ( +
+ + +
+ Updated {formatRelativeTimeLabel(card.fetchedAt)} +
+
+ ))} + {aggregate.pendingNodes.map((node) => ( + + ))} + {aggregate.failedNodes.map((node) => ( + + ))} +
+ )} +
+ +

+ Usage reflects provider subscription rate-limit windows across all connected environments. + Identical accounts are shown once. Refreshes may use a server-cached snapshot for up to one + minute. +

+ + ); +} + +function EnvironmentUsageProbe({ + environmentId, + environmentLabel, + refreshNonce, + onResult, + onRemove, +}: { + environmentId: EnvironmentId; + environmentLabel: string; + refreshNonce: number; + onResult: (result: EnvironmentUsageInput) => void; + onRemove: (environmentId: string) => void; +}) { + const query = useEnvironmentQuery(serverEnvironment.providerUsage({ environmentId, input: {} })); + const snapshots = query.data?.usage ?? null; + + useEffect(() => { + onResult({ + environmentId, + environmentLabel, + snapshots, + isPending: query.isPending, + error: query.error, + }); + }, [environmentId, environmentLabel, onResult, query.error, query.isPending, snapshots]); + + useEffect(() => () => onRemove(environmentId), [environmentId, onRemove]); + + const refreshRef = useRef(query.refresh); + refreshRef.current = query.refresh; + const didMount = useRef(false); + useEffect(() => { + if (!didMount.current) { + didMount.current = true; + return; + } + refreshRef.current(); + }, [refreshNonce]); + + return null; +} + +function EnvironmentStatusRow({ + node, + failed = false, +}: { + node: ProviderUsageNodeStatus; + failed?: boolean; +}) { + return ( +
+ {failed ? ( + + ) : ( + + )} +
+
{node.environmentLabel}
+
+ {failed ? (node.error ?? "Unreachable") : "Loading usage…"} +
+
+
+ ); +} diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index 6774b6f333f..b7e321ef308 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -3,6 +3,7 @@ import { ArchiveIcon, ArrowLeftIcon, BotIcon, + GaugeIcon, GitBranchIcon, KeyboardIcon, Link2Icon, @@ -26,6 +27,7 @@ export type SettingsSectionPath = | "/settings/general" | "/settings/keybindings" | "/settings/providers" + | "/settings/usage" | "/settings/source-control" | "/settings/connections" | "/settings/archived"; @@ -38,6 +40,7 @@ export const SETTINGS_NAV_ITEMS: ReadonlyArray<{ { label: "General", to: "/settings/general", icon: Settings2Icon }, { label: "Keybindings", to: "/settings/keybindings", icon: KeyboardIcon }, { label: "Providers", to: "/settings/providers", icon: BotIcon }, + { label: "Usage & Limits", to: "/settings/usage", icon: GaugeIcon }, { label: "Source Control", to: "/settings/source-control", icon: GitBranchIcon }, { label: "Connections", to: "/settings/connections", icon: Link2Icon }, { label: "Archive", to: "/settings/archived", icon: ArchiveIcon }, diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 3a9140e278c..8d911041ee7 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as SettingsRouteImport } from './routes/settings' import { Route as PairRouteImport } from './routes/pair' import { Route as ChatRouteImport } from './routes/_chat' import { Route as ChatIndexRouteImport } from './routes/_chat.index' +import { Route as SettingsUsageRouteImport } from './routes/settings.usage' import { Route as SettingsSourceControlRouteImport } from './routes/settings.source-control' import { Route as SettingsProvidersRouteImport } from './routes/settings.providers' import { Route as SettingsKeybindingsRouteImport } from './routes/settings.keybindings' @@ -42,6 +43,11 @@ const ChatIndexRoute = ChatIndexRouteImport.update({ path: '/', getParentRoute: () => ChatRoute, } as any) +const SettingsUsageRoute = SettingsUsageRouteImport.update({ + id: '/usage', + path: '/usage', + getParentRoute: () => SettingsRoute, +} as any) const SettingsSourceControlRoute = SettingsSourceControlRouteImport.update({ id: '/source-control', path: '/source-control', @@ -100,6 +106,7 @@ export interface FileRoutesByFullPath { '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute + '/settings/usage': typeof SettingsUsageRoute '/$environmentId/$threadId': typeof ChatEnvironmentIdThreadIdRoute '/draft/$draftId': typeof ChatDraftDraftIdRoute } @@ -113,6 +120,7 @@ export interface FileRoutesByTo { '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute + '/settings/usage': typeof SettingsUsageRoute '/': typeof ChatIndexRoute '/$environmentId/$threadId': typeof ChatEnvironmentIdThreadIdRoute '/draft/$draftId': typeof ChatDraftDraftIdRoute @@ -129,6 +137,7 @@ export interface FileRoutesById { '/settings/keybindings': typeof SettingsKeybindingsRoute '/settings/providers': typeof SettingsProvidersRoute '/settings/source-control': typeof SettingsSourceControlRoute + '/settings/usage': typeof SettingsUsageRoute '/_chat/': typeof ChatIndexRoute '/_chat/$environmentId/$threadId': typeof ChatEnvironmentIdThreadIdRoute '/_chat/draft/$draftId': typeof ChatDraftDraftIdRoute @@ -146,6 +155,7 @@ export interface FileRouteTypes { | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' + | '/settings/usage' | '/$environmentId/$threadId' | '/draft/$draftId' fileRoutesByTo: FileRoutesByTo @@ -159,6 +169,7 @@ export interface FileRouteTypes { | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' + | '/settings/usage' | '/' | '/$environmentId/$threadId' | '/draft/$draftId' @@ -174,6 +185,7 @@ export interface FileRouteTypes { | '/settings/keybindings' | '/settings/providers' | '/settings/source-control' + | '/settings/usage' | '/_chat/' | '/_chat/$environmentId/$threadId' | '/_chat/draft/$draftId' @@ -215,6 +227,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ChatIndexRouteImport parentRoute: typeof ChatRoute } + '/settings/usage': { + id: '/settings/usage' + path: '/usage' + fullPath: '/settings/usage' + preLoaderRoute: typeof SettingsUsageRouteImport + parentRoute: typeof SettingsRoute + } '/settings/source-control': { id: '/settings/source-control' path: '/source-control' @@ -303,6 +322,7 @@ interface SettingsRouteChildren { SettingsKeybindingsRoute: typeof SettingsKeybindingsRoute SettingsProvidersRoute: typeof SettingsProvidersRoute SettingsSourceControlRoute: typeof SettingsSourceControlRoute + SettingsUsageRoute: typeof SettingsUsageRoute } const SettingsRouteChildren: SettingsRouteChildren = { @@ -313,6 +333,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsKeybindingsRoute: SettingsKeybindingsRoute, SettingsProvidersRoute: SettingsProvidersRoute, SettingsSourceControlRoute: SettingsSourceControlRoute, + SettingsUsageRoute: SettingsUsageRoute, } const SettingsRouteWithChildren = SettingsRoute._addFileChildren( diff --git a/apps/web/src/routes/settings.usage.tsx b/apps/web/src/routes/settings.usage.tsx new file mode 100644 index 00000000000..0b9586cba4c --- /dev/null +++ b/apps/web/src/routes/settings.usage.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { ProviderUsageSettings } from "../components/settings/ProviderUsageSettings"; + +export const Route = createFileRoute("/settings/usage")({ + component: ProviderUsageSettings, +}); diff --git a/docs/nodes.md b/docs/nodes.md new file mode 100644 index 00000000000..0812c9f9436 --- /dev/null +++ b/docs/nodes.md @@ -0,0 +1,198 @@ +# T3 Code nodes (jetblk fork) + +How our nodes run the fork's server instead of upstream's `npx t3@nightly`. + +## How it works + +``` +nightly cron ──► GitHub Actions (nightly-fork.yml) + gate: skip unless main gained a non-docs commit + stamp version → typecheck + usage tests + → build web client + server bundle → dist/client + → pnpm deploy (bundle + prod node_modules) + └► GitHub Releases (t3-server.tgz, ~125 MB): + • `nightly` rolling — what nodes pull + • `v` immutable — pin/rollback target + │ + systemctl --user restart t3code.service (on any node) + │ + ExecStartPre: curl | tar → ~/.local/share/t3-nightly + ExecStart: node dist/bin.mjs serve --host --port 3773 +``` + +**A restart is an update.** There is no separate update command — same contract the old +`npx t3@nightly` line gave us, just sourced from our fork. + +**When builds happen.** Every 3 hours, at :37 past the hour (06:37, 09:37, … UTC) — the +same cadence upstream's release.yml uses for its nightly channel, so our builds stay about +as fresh as theirs. A window's merges land in **one** build, not one per PR. + +A scheduled run first checks whether `main` gained a commit touching something other than +docs since the last published build; if not, it skips without building — so a quiet window, +or one of nothing but docs, costs a ~10s gate check and burns no version number. **Need a +build sooner?** `gh workflow run nightly-fork.yml --repo jetblk/t3code` skips that gate and +always builds. + +`nightly` is still a _channel_ (like an npm dist-tag) as well as a cadence: it is the name of +the rolling release nodes pull, whether it was produced by the cron or by you. + +**Each build publishes two releases:** + +| Release | Purpose | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `nightly` (rolling) | The stable URL nodes curl. Recreated every build, so its tag tracks the newest commit and the asset name never changes. | +| `v` (immutable) | Never deleted. The pin/rollback target — point a node's `ExecStartPre` at this URL to freeze it on a known-good build. | + +The repo is public, so nodes download the release **unauthenticated**. No tokens, no +registry, no `~/.npmrc`. + +The release asset is **self-contained**: `dist/` plus production `node_modules/`, +including the native deps (`node-pty`, sqlite). A node needs no build tooling. +`dist/client` holds the browser UI — the server answers "No static directory configured +and no dev URL set." without it, so the workflow builds `@t3tools/web` and copies it in. + +**Everything ships from one commit.** The web client, the server bundle and the version +stamp all come from the same CI run, so a node's browser UI can never drift from its +server. Settings → General → About shows that version. + +There is deliberately **no release-channel switcher** in the web UI: it is gated on the +`VITE_HOSTED_APP_CHANNEL` build env, which only upstream's hosted Vercel deploy sets +(to switch between `latest.app.t3.codes` / `nightly.app.t3.codes`). Self-hosted builds +have no channels to switch between — its absence confirms you are on our build. + +## Prerequisites + +| Requirement | Why | +| --------------------------------- | ------------------------------------------------------------------------------------ | +| **linux-x64** | The tarball ships natives compiled for it. See gotcha 3. | +| **Node ≥ 22.16** | Server `engines: ^22.16 \|\| ^23.11 \|\| >=24.10`; the bundle targets `node22.16.0`. | +| **tailscale**, joined | The unit waits for a tailnet IP and binds it. | +| **curl**, **tar** | The update step. | +| **systemd user session** + linger | See gotcha 2. | + +A node needs **none** of: a repo checkout, pnpm/vp, npm/npx, or Node 24. Node 24 is only +needed to _build_ the repo (its dev tooling pins `^24.13.1`); running the server is not building it. + +## Bootstrap a new node + +```bash +# 1. prerequisites (node >= 22.16, tailscale joined) +node -v && tailscale ip -4 + +# 2. scratch dir — keeps agent scratch off the /tmp RAM disk (gotcha 1) +mkdir -p ~/.cache/t3-scratch + +# 3. install the unit +mkdir -p ~/.config/systemd/user +curl -fsSL https://raw.githubusercontent.com/jetblk/t3code/main/docs/t3code.service \ + -o ~/.config/systemd/user/t3code.service + +# 4. let the user unit run headless / at boot (gotcha 2) +loginctl enable-linger "$USER" + +# 5. start it (this pulls the current nightly) +systemctl --user daemon-reload +systemctl --user enable --now t3code.service + +# 6. verify +journalctl --user -u t3code.service -n 30 --no-pager # expect: t3 nightly: 0.0.28-jetblk.. +ss -tlnp | grep 3773 # bound to the tailnet IP, not 0.0.0.0 +``` + +**Pair a client.** On first start the server prints pairing details to the journal: + +``` +Listening on http://100.x.y.z:3773 +Connection string: http://100.x.y.z:3773 +Token: XXXXXXXXXXXX +Pairing URL: http://100.x.y.z:3773/pair#token=XXXXXXXXXXXX +``` + +Open the Pairing URL (or scan the QR in the journal) from the mobile/desktop client while +on the tailnet. Pairing is stored in `~/.t3/userdata/secrets/` and survives restarts and +upgrades. + +## Switch an existing node off `npx t3@nightly` + +```bash +# keep a way back +cp ~/.config/systemd/user/t3code.service ~/.config/systemd/user/t3code.service.npx-bak + +systemctl --user stop t3code.service +curl -fsSL https://raw.githubusercontent.com/jetblk/t3code/main/docs/t3code.service \ + -o ~/.config/systemd/user/t3code.service +mkdir -p ~/.cache/t3-scratch +systemctl --user daemon-reload +systemctl --user start t3code.service +journalctl --user -u t3code.service -n 30 --no-pager +``` + +**Existing pairings survive.** The unit passes no `--base-dir`, so the server keeps using +the default `~/.t3` — the same data dir `npx t3@nightly` used. Its `secrets/` (server +signing key) and `state.sqlite` are untouched, so clients reconnect to the _same_ server +identity with no re-pairing. + +Nothing in the unit is node-specific: it resolves the tailnet IP itself, so the same file +works on every node. + +## Operations + +| Task | Command | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Update to latest nightly | `systemctl --user restart t3code.service` | +| Which build is running | `cat ~/.local/share/t3-nightly/dist/VERSION` | +| Logs (service) | `journalctl --user -u t3code.service -f` | +| Logs (server traces) | `~/.t3/userdata/logs/server.trace.ndjson*` | +| Roll back to upstream | `cp ~/.config/systemd/user/t3code.service.npx-bak ~/.config/systemd/user/t3code.service && systemctl --user daemon-reload && systemctl --user restart t3code.service` | +| Pin / roll back to a build | Point the unit's `curl` at an immutable tag: `/download/v/t3-server.tgz` instead of `/download/nightly/`. `gh release list --repo jetblk/t3code` lists them. | +| Freeze entirely (no updates) | Comment out the `curl` `ExecStartPre`; the node then keeps running whatever is in `~/.local/share/t3-nightly`. | +| Build without a code change | `gh workflow run nightly-fork.yml --repo jetblk/t3code` | + +## Gotchas + +These are all load-bearing — each one cost us an outage or an hour. + +1. **`/tmp` is a small RAM disk** (tmpfs, ~half of RAM), not real disk. The + `Environment=TMPDIR/TMP/TEMP=%h/.cache/t3-scratch` lines exist because an agent ran + `pnpm install` under `/tmp`, put a 2.4 GB pnpm store on the RAM disk, filled it, broke + the shell, and crashed the server. **Never checkout or install under `/tmp`** on these + boxes, and don't drop those env lines. The update step extracts to `~/.local/share` for + the same reason. +2. **`loginctl enable-linger`** — without it a systemd _user_ unit stops at logout and does + not start at boot. Easy to miss on a fresh node; the node looks fine until you log out. +3. **linux-x64 only.** The tarball contains natives (`node-pty`, sqlite) compiled on the + CI runner. A macOS or arm64 node needs its own build (add a matrix to + `nightly-fork.yml`) — the current single artifact will not run there. +4. **Runtime Node ≠ build Node.** The server runs on ≥ 22.16; only building the repo needs + Node 24. Don't install Node 24 on a node just to run the server. +5. **The update is best-effort.** `ExecStartPre=-` (leading dash) means a failed download is + ignored, so a node with no GitHub reachability still boots on its last-good install + rather than refusing to start. Check the journal for `t3 nightly: ` to confirm + an update actually landed. +6. **Desktop client shows "Client and server versions differ."** Expected and cosmetic. Our + builds report `0.0.28-jetblk..`; upstream's auto-updating desktop client + reports `0.0.29-nightly.<...>`, and the check is an exact string compare. The RPC + contract is what matters, and it is in sync as long as `main` is merged with upstream. + Upstream never commits nightly versions (only stable ones), which is why a source build + otherwise reports the last stable forever. +7. **This node no longer runs your working tree.** Once switched, the server runs the + published release, not `~/workspace/t3code`. Local rebuilds have no effect until pushed + and released. That is the point (every node runs the same reproducible build), but it is + a mental-model shift if you are used to `node apps/server/dist/bin.mjs`. +8. **Merging is not releasing.** Since builds went on a cron, a merge to `main` publishes + nothing on its own — restart a node right after merging and you will still get the last + published build, with no error to tell you why. The next scheduled check (≤3 h out) will + pick it up; to get it onto a node sooner, dispatch a build + (`gh workflow run nightly-fork.yml --repo jetblk/t3code`), wait for it to publish, then + restart. `cat ~/.local/share/t3-nightly/dist/VERSION` tells you which build you actually + have. + +## Fork CI notes + +- Only `nightly-fork.yml` is enabled. All 8 workflows inherited from upstream are + `disabled_manually` — they need pingdotgg's `blacksmith-*` runners and secrets, and would + queue forever on every push. Disabling is a repo setting, so upstream merges don't + re-enable them. +- The nightly gates on typecheck + the provider-usage tests only. The full server suite has + pre-existing ACP-transport failures unrelated to this fork; gating on it would block every + release. diff --git a/docs/provider-usage-plan.md b/docs/provider-usage-plan.md new file mode 100644 index 00000000000..c35cdeaa397 --- /dev/null +++ b/docs/provider-usage-plan.md @@ -0,0 +1,116 @@ +# Provider Usage in the T3 Code Mobile App + +## Context + +T3 Code users have no way to see how much of their AI-provider subscription limits they've consumed (Claude 5-hour/weekly windows, ChatGPT/Codex session/weekly windows). The macOS app OpenUsage (github.com/robinebers/openusage) solves this well on desktop; this plan brings equivalent, minimal v1 functionality to the T3 Code mobile app. + +**Decisions (confirmed with user):** + +- Subscription rate-limit windows (not API $ spend) · fetched via the T3 Code server (which owns provider credentials) · a dedicated settings screen · minimal v1 (on-demand + pull-to-refresh; no history, no push). Claude + OpenAI/Codex first; schema is provider-agnostic so Cursor/OpenCode/Grok plug in later. +- **Multi-node:** the user runs several T3 Code nodes (Linux VPS + local). The client queries **all connected environments** and **dedupes identical provider accounts** (same driver + account identity) into one card that lists its source nodes — no N duplicate Claude cards when nodes share one Max account. _Rationale (alternatives considered):_ usage is account-scoped, so an "active node only/with fallback" design would be simpler and sufficient for single-account fleets — but T3 Code ships to users who may run different provider accounts on different nodes, and fan-out + dedupe is the only design that renders that topology correctly while collapsing to one card for the single-account case. Redundant fetches are bounded by the 60s server cache + 30s client stale-time. +- **Meter orientation:** "% left" like OpenUsage ("92% left · resets in 3h 48m"). No "~X% left at reset" pace projection in v1 (needs burn-rate history). +- **Linux-first:** Claude credentials are read from the credentials file only. No macOS Keychain assumption/fallback in v1 (the user's server nodes are Linux). + +**Data sources (verified from OpenUsage source):** + +- **Claude:** `GET https://api.anthropic.com/api/oauth/usage` with `Authorization: Bearer ` + `anthropic-beta: oauth-2025-04-20` + a `claude-code/x.y` User-Agent. Token read from `${claudeHome}/.claude/.credentials.json` (needs `user:profile` scope — full `claude` login, not `setup-token`). Response: `five_hour` / `seven_day` each `{utilization, resets_at}`, model-scoped windows (`seven_day_sonnet` and/or `limits[]` entries with `scope.model.display_name` — e.g. currently "Fable"), `extra_usage` `{is_enabled, used_credits, monthly_limit}`. **Model-window labels must come from the response dynamically, never hardcoded** — Anthropic renames/reshuffles these (Sonnet → Fable already happened) and may drop them entirely. +- **Codex:** no HTTP needed — the vendored `packages/effect-codex-app-server` already exposes a typed `account/rateLimits/read` JSON-RPC request (`schema.gen.ts:29769`) returning `planType`, `primary`/`secondary` windows (`usedPercent`, `resetsAt`, `windowDurationMins`), `credits`. The codex CLI handles token refresh itself — avoids the token-rotation race entirely. + +## Architecture + +Effect RPC over WebSocket, following existing patterns end to end: +contract schema → standalone RPC → per-driver optional `usage` capability on `ProviderInstance` → aggregation service with 60s cache → shared client-runtime query atom per environment → mobile screen that merges all environments' results and dedupes by account. + +Standalone request/response RPC (not extending `ServerProvider`): provider snapshots are broadcast/persisted on a 5-min refresh loop; volatile usage data fits pull-to-refresh request semantics instead. + +## Phase 1 — Contracts (`packages/contracts`) + +**New `packages/contracts/src/providerUsage.ts`** (export from `index.ts`), effect-Schema style matching `server.ts`: + +- `ProviderUsageWindow`: `{ id, label, kind: "session"|"weekly"|"model"|"other", usedPercent (0–100, raw from API; clients render "left"), resetsAt?: IsoDateTime, windowMinutes? }` +- `ProviderUsageCredits`: `{ label, balance?, usedCredits?, monthlyLimit?, unlimited? }` +- `ProviderUsageSnapshot`: `{ instanceId: ProviderInstanceId, driver: ProviderDriverKind, displayName?, account?: TrimmedNonEmptyString, status: "ok"|"unauthenticated"|"unsupported"|"error", planLabel?, windows: Array (decoding default []), credits?, message?, fetchedAt: IsoDateTime }` — flat struct with a `status` discriminant, like `ServerProvider` folds availability/auth into one record. **`account`** is the cross-node dedupe identity (account email from the provider's auth snapshot when known). +- `ProviderUsageResult`: `{ usage: Array }` + +**`packages/contracts/src/rpc.ts`:** add `serverGetProviderUsage: "server.getProviderUsage"` to `WS_METHODS`; add `WsServerGetProviderUsageRpc = Rpc.make(...)` with payload `{ instanceId?: ProviderInstanceId }`, success `ProviderUsageResult`, error `EnvironmentAuthorizationError` (pattern: `WsServerGetProcessResourceHistoryRpc`, rpc.ts:305); register in `WsRpcGroup`. Per-provider failures fold into snapshot `status`/`message` — one broken provider never blanks the screen. + +## Phase 2 — Server (`apps/server`) + +**Capability:** `apps/server/src/provider/Services/ProviderUsage.ts` — `interface ProviderUsageShape { readonly fetchUsage: Effect.Effect }` (never fails; failures folded into status). Add optional `readonly usage?: ProviderUsageShape` to `ProviderInstance` in `apps/server/src/provider/ProviderDriver.ts` (alongside `snapshot`/`adapter`/`textGeneration`, line 71). Drivers without it surface as `"unsupported"` with zero changes. + +**`apps/server/src/provider/Layers/CodexUsage.ts`** — `makeCodexUsage(effectiveConfig, meta)`: + +1. Spawn `codex app-server` with `CODEX_HOME` from the driver's resolved home layout; `initialize` (with `capabilities: { experimentalApi: true }`) → `client.request("account/rateLimits/read", {})` — mirror/extract the spawn+init flow of `probeCodexAppServerProvider` in `Layers/CodexProvider.ts` (~line 289). +2. Pure exported mapper `mapCodexRateLimitsSnapshot(response, now)`: `primary` → session window (label derived from `windowDurationMins`), `secondary` → weekly, `individualLimit` → "Spend limit" (`100 - remainingPercent`), `credits` → credits, `planType` → `planLabel` (title-case map like `codexAccountAuthLabel`, CodexProvider.ts:69). Verify `resetsAt` epoch unit (s vs ms) against a live response. +3. Auth errors → `"unauthenticated"` ("Run `codex login` on the server machine"); `Effect.timeout("15 seconds")` + catch-all → `"error"`. +4. Wire `usage:` into the `ProviderInstance` returned by `Drivers/CodexDriver.ts` `create` (~line 202). + +**`apps/server/src/provider/Layers/ClaudeUsage.ts`** — `makeClaudeUsage(claudeSettings, meta)` using `FileSystem`/`Path`/`HttpClient` (already in `ClaudeDriverEnv`): + +1. `resolveClaudeHomePath` (`Drivers/ClaudeHome.ts:9`) → read + schema-parse `${home}/.claude/.credentials.json` (`claudeAiOauth: { accessToken, expiresAt?, subscriptionType?, scopes? }`). **File-only in v1** — missing/unparseable → `"unauthenticated"` ("Sign in with the `claude` CLI on the server machine"). Do NOT add macOS Keychain fallback; if a Mac-hosted node lacks the file, the unauthenticated card + message is the correct v1 behavior (possible v2 nicety). `expiresAt < now` → `"unauthenticated"` ("token expired — refreshes next time Claude runs"). **No token refresh in v1** — writing rotated tokens races Claude Code's own refresh. +2. `GET https://api.anthropic.com/api/oauth/usage` via effect `HttpClient` with the headers above; parse with a _lenient_ all-optional schema so upstream drift degrades instead of erroring. +3. Pure exported mapper `mapClaudeUsageResponse(json, meta, now)`: `five_hour` → "Session", `seven_day` → "Weekly"; **model-scoped windows get their labels from the response** — `limits[]` entries use `scope.model.display_name` verbatim (e.g. "Fable"), and any `seven_day_` key is labeled from the `` suffix, title-cased, never a hardcoded model name; `extra_usage` → credits. `planLabel` from credentials `subscriptionType` — reuse/export `claudeSubscriptionLabel` from `Layers/ClaudeProvider.ts` (~line 444). +4. 401/403 → `"unauthenticated"` (403 ≈ missing `user:profile` scope → "re-login with a recent Claude Code version"); else `"error"`. Wire into `Drivers/ClaudeDriver.ts` `create`. + +**`apps/server/src/provider/Layers/ProviderUsageService.ts`** — Context.Service over `ProviderInstanceRegistry`: + +- `getUsage(instanceId?)`: list enabled instances (filtered by `instanceId` if given); no `usage` capability → synthesize `"unsupported"` snapshot; fetch capable ones concurrently. +- Populate `snapshot.account` from the instance's provider snapshot auth (`ServerProviderAuth.email`, falling back to its `label`) so clients can dedupe across nodes. +- 60s TTL cache (`Ref>`); cache only `"ok"` results, always retry errors. Bounds pull-to-refresh hammering and Codex process spawns. +- Compose in `apps/server/src/server.ts` near `ProviderRegistryLive` (~line 297). + +**`apps/server/src/ws.ts`:** add scope entry `[WS_METHODS.serverGetProviderUsage, AuthOrchestrationReadScope]` (~line 279); handler next to `serverRefreshProviders` (~line 1245) calling `providerUsage.getUsage(input.instanceId)` wrapped in `observeRpcEffect`. Update `server.test.ts` layer if the group addition makes handlers a compile error. + +Leave the existing unconsumed `account.rate-limits.updated` event (`providerRuntime.ts`) as-is — it's the v2 hook for live-push updates into this same schema (note in a comment). + +## Phase 3 — Shared client runtime (`packages/client-runtime`) + +In `createServerEnvironmentAtoms` (`src/state/server.ts`, return object ~line 288), add alongside `traceDiagnostics`: + +```ts +providerUsage: createEnvironmentRpcQueryAtomFamily(runtime, { + label: "environment-data:server:provider-usage", + tag: WS_METHODS.serverGetProviderUsage, + staleTimeMs: 30_000, + idleTtlMs: 5 * 60_000, +}), +``` + +No refresh command needed — refresh via `useAtomRefresh`; the server 60s cache dedupes. Mobile picks it up automatically through `apps/mobile/src/state/server.ts`. + +## Phase 4 — Mobile UI (`apps/mobile`) + +**Navigation:** add `"SettingsProviderUsage"` to the union in `src/features/settings/components/settings-sheet-targets.ts`; register in `Stack.tsx` `SettingsSheetStack` (after `SettingsClientStorage`, ~line 151) with `linking: "provider-usage"`, title "Usage & Limits"; add `` in `SettingsRouteScreen.tsx` — **both return branches** (signed-out ~line 103 and signed-in ~line 451). + +**New `src/features/settings/SettingsProviderUsageRouteScreen.tsx`** (uniwind classes, `AppText`, `AsyncResult` branches per `SettingsClientStorageRouteScreen.tsx`), **merged multi-node view**: + +- **Aggregation (new mobile state, e.g. `src/state/providerUsage.ts`):** a derived atom (or `Atom.family` keyed by nothing/catalog) that reads the environment catalog (`environmentCatalog.catalogValueAtom`) and each environment's `serverEnvironment.providerUsage({ environmentId, input: {} })` atom, combining into: + - `cards`: snapshots grouped by dedupe key — `"${driver}:${account}"` when `account` is present; otherwise no dedupe (key = `"${environmentId}:${instanceId}"`). Each card keeps the freshest snapshot (`fetchedAt`) plus the list of source node labels (from `useSavedRemoteConnections()` / catalog names). + - `pendingEnvironments` / `failedEnvironments` for partial-state rendering. + Deriving in an atom (atoms compose in `@effect/atom`) avoids hooks-in-loops over the environment list. +- **Screen rendering:** show cards as environments resolve; a slim inline row per still-loading node (`ActivityIndicator` + node name) and per unreachable node ("vps-2 unreachable"). One section "Providers"; a card's subtitle shows its account + source nodes (e.g. `nick@… · via vps-1, local`) when deduped from >1 node. +- **`ProviderUsageCard({ card })`:** header = existing `ProviderIcon` (`src/components/ProviderIcon.tsx`) + display name + `planLabel` pill (e.g. "Max 20x"). Body by `status`: `ok` → `UsageWindowRow` per window + credits line; `unauthenticated` → SF symbol + `message` (+ node name so the user knows which box to log in on); `unsupported` → muted note; `error` → danger-tinted `message`. +- **`UsageWindowRow` — "% left" orientation (OpenUsage-style):** label left; meter; below it left-aligned `"92% left"` (`100 - usedPercent`, `tabular-nums`) and right-aligned `"Resets in 3h 48m"` from a pure `formatResetsIn(resetsAtIso, now)` helper. No pace projection in v1. New ~20-line `UsageMeter` (no existing mobile progress bar — `LoadingStrip` is indeterminate): rounded track `View` + inner fill `View` at `width: ${clamped}%` of _used_, color stepping at 75% used (warning) / 95% used (danger) via `useThemeColor`. +- **Credits line** mirrors the same orientation: "$89.09 left · $200 limit" (`monthlyLimit - usedCredits`), with a small meter when `monthlyLimit` is set. +- **Pull-to-refresh:** screen-level `ScrollView refreshControl={}` triggering `useAtomRefresh` on each environment's usage atom (iterate the same stable sorted environment list the aggregate atom uses). + +## Phase 5 — Tests & verification + +- **Mappers (main coverage):** `ClaudeUsage.test.ts` / `CodexUsage.test.ts` — fixture JSON → expected snapshot; cases: full response, model-scoped windows with dynamic display names (assert no hardcoded model labels — fixture uses an unseen name), missing model windows entirely, disabled `extra_usage`, Codex null `secondary`, unknown `planType`, expired/malformed/missing credentials file → `"unauthenticated"`. +- **Service:** `ProviderUsageService.test.ts` with stubbed `ProviderInstanceRegistry` (pattern: `ProviderInstanceRegistryLive.test.ts`): unsupported synthesis, `account` population from snapshot auth, cache TTL, instance filtering, one failing provider doesn't affect others. +- **Client aggregation:** unit-test the pure dedupe/merge function (same account on 2 nodes → 1 card with 2 node labels + freshest snapshot; missing `account` → separate cards; mixed ok/unreachable environments). +- **Contracts:** decode round-trip + `windows` default in `providerUsage.test.ts`. +- Run `pnpm --filter @t3tools/contracts test`, server package tests, repo typecheck/lint. +- **Manual E2E:** run ≥2 server nodes logged into the same Claude account → Expo app → Settings → Usage & Limits: one Claude card listing both nodes; "% left" values match OpenUsage/`claude /usage`; pull-to-refresh ≤1 upstream fetch per node per 60s (RPC trace spans); negative paths: rename `.credentials.json` on one node → unauthenticated card naming that node; empty Codex home → "Run codex login"; take one node offline → "unreachable" row while other cards remain. + +## Sequencing + +1. Contracts → 2. Claude/Codex usage implementations + mapper tests → 3. `ProviderUsageService` + `server.ts`/`ws.ts` wiring → 4. client-runtime atom → 5. mobile aggregation atom + screen + navigation → 6. manual E2E. + +## Risks + +- `api.anthropic.com/api/oauth/usage` is undocumented and can change/break silently (model windows already renamed Sonnet → Fable; could move to API-spend-only) — lenient schema + dynamic labels + `"error"` degradation contain it. Old tokens without `user:profile` → 403. +- No Claude token refresh in v1: expired token shows "unauthenticated" until the CLI next runs on that node (deliberate — avoids rotation races). +- Dedupe depends on `account` identity from provider auth snapshots; if a provider reports no email/label, cards fall back to per-node (duplicates possible but data still correct). +- Codex fetch spawns a short-lived `codex app-server` (~1–2s) — fine with the 60s cache; v2 can reuse the adapter's long-lived connection and the already-emitted `account/rateLimits/updated` events for live push. +- Older codex CLIs without `account/rateLimits/read` fold into `"error"` with the JSON-RPC message. diff --git a/docs/t3code.service b/docs/t3code.service new file mode 100644 index 00000000000..48f7552716d --- /dev/null +++ b/docs/t3code.service @@ -0,0 +1,55 @@ +[Unit] +Description=T3 Code server (jetblk fork nightly, tailnet-only, port 3773) +Wants=network-online.target +After=network-online.target +# A node without a working tailscale is broken anyway — fail hard after +# 3 attempts instead of crash-looping forever (e.g. --skip-tailscale VMs). +StartLimitIntervalSec=600 +StartLimitBurst=3 + +[Service] +# /tmp is a small box-wide tmpfs (~half of RAM). A spawned agent that runs +# pnpm install under /tmp put a 2.4G store on it and filled the RAM disk, +# breaking the shell and crashing the server. Point all scratch at the 57G +# root disk instead so checkouts/installs can't exhaust the tmpfs. +Environment=TMPDIR=%h/.cache/t3-scratch +Environment=TMP=%h/.cache/t3-scratch +Environment=TEMP=%h/.cache/t3-scratch + +# User units can't order after the system tailscaled unit; wait (bounded) +# for a tailnet IP so the bind below never races the interface. +ExecStartPre=/bin/sh -c 'i=0; until tailscale ip -4 >/dev/null 2>&1; do i=$((i+1)); if [ "$i" -ge 60 ]; then echo "no tailnet IP after 120s" >&2; exit 1; fi; sleep 2; done' + +# Pull the latest fork nightly before starting, so a restart IS an update — +# the same contract the old `npx t3@nightly` line gave us. +# +# Staged into .new and swapped, so a half-downloaded archive can never +# replace a working install, and stale files from the previous version don't +# linger. Extracted under ~/.local/share, deliberately NOT /tmp (see above). +# +# The leading `-` makes the update best-effort: if GitHub is unreachable the +# node still starts on the build it already has, rather than refusing to boot. +ExecStartPre=-/bin/sh -c 'set -e; \ + d=%h/.local/share/t3-nightly; \ + rm -rf "$d.new"; mkdir -p "$d.new"; \ + curl -fsSL --retry 3 --retry-delay 2 -o "$d.new/t3.tgz" \ + https://github.com/jetblk/t3code/releases/download/nightly/t3-server.tgz; \ + tar xzf "$d.new/t3.tgz" -C "$d.new"; rm -f "$d.new/t3.tgz"; \ + test -f "$d.new/dist/bin.mjs"; \ + rm -rf "$d.old"; if [ -d "$d" ]; then mv "$d" "$d.old"; fi; \ + mv "$d.new" "$d"; rm -rf "$d.old"; \ + echo "t3 nightly: $(cat "$d/dist/VERSION" 2>/dev/null || echo unknown)"' + +# Binding the Tailscale IP (not 0.0.0.0) keeps this tailnet-only even if UFW +# changes, and satisfies T3's non-loopback check for remote pairing. Refuse +# to start with an empty IP rather than fall back to t3's default bind. +ExecStart=/bin/sh -c 'ip="$(tailscale ip -4 | head -1)"; [ -n "$ip" ] || exit 1; \ + exec /usr/bin/node %h/.local/share/t3-nightly/dist/bin.mjs serve --host "$ip" --port 3773' + +WorkingDirectory=%h +Restart=on-failure +RestartSec=15 +TimeoutStartSec=300 + +[Install] +WantedBy=default.target diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index d9e19889721..1c52ea3e3a7 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -35,6 +35,10 @@ "types": "./src/platform/index.ts", "default": "./src/platform/index.ts" }, + "./provider-usage": { + "types": "./src/providerUsage.ts", + "default": "./src/providerUsage.ts" + }, "./relay": { "types": "./src/relay/index.ts", "default": "./src/relay/index.ts" diff --git a/packages/client-runtime/src/providerUsage.test.ts b/packages/client-runtime/src/providerUsage.test.ts new file mode 100644 index 00000000000..301f0055c24 --- /dev/null +++ b/packages/client-runtime/src/providerUsage.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from "@effect/vitest"; +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderUsageSnapshot, +} from "@t3tools/contracts"; + +import { + aggregateProviderUsage, + areProviderUsageResultsComplete, + formatProviderUsageCredits, + formatProviderUsageExpiry, + formatProviderUsageReset, + formatProviderUsageRetry, + providerUsageCreditsHaveMeter, + providerUsagePercentLeft, + type EnvironmentUsageInput, +} from "./providerUsage.ts"; + +const makeSnapshot = (overrides: Partial = {}): ProviderUsageSnapshot => ({ + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + status: "ok", + windows: [], + fetchedAt: "2026-07-17T10:00:00.000Z", + ...overrides, +}); + +describe("aggregateProviderUsage", () => { + it("dedupes provider accounts across environments and keeps the freshest values", () => { + const result = aggregateProviderUsage([ + { + environmentId: "vps", + environmentLabel: "VPS", + snapshots: [ + makeSnapshot({ + account: "person@example.com", + planLabel: "Older plan", + fetchedAt: "2026-07-17T10:00:00.000Z", + }), + ], + isPending: false, + error: null, + }, + { + environmentId: "local", + environmentLabel: "Local", + snapshots: [ + makeSnapshot({ + account: "PERSON@example.com", + planLabel: "Current plan", + freshness: { state: "stale", retryAt: "2026-07-17T11:05:00.000Z" }, + resetCredits: { + availableCount: 1, + credits: [{ id: "reset-1", expiresAt: "2026-07-20T11:00:00.000Z" }], + }, + fetchedAt: "2026-07-17T11:00:00.000Z", + }), + ], + isPending: false, + error: null, + }, + ]); + + expect(result.cards).toHaveLength(1); + expect(result.cards[0]).toMatchObject({ + instanceId: "codex", + planLabel: "Current plan", + freshness: { state: "stale", retryAt: "2026-07-17T11:05:00.000Z" }, + resetCredits: { + availableCount: 1, + credits: [{ id: "reset-1", expiresAt: "2026-07-20T11:00:00.000Z" }], + }, + sourceNodes: ["Local", "VPS"], + }); + }); + + it("keeps account-less instances separate and preserves partial failures", () => { + const result = aggregateProviderUsage([ + { + environmentId: "local", + environmentLabel: "Local", + snapshots: [makeSnapshot()], + isPending: false, + error: null, + }, + { + environmentId: "vps", + environmentLabel: "VPS", + snapshots: [makeSnapshot()], + isPending: false, + error: null, + }, + { + environmentId: "offline", + environmentLabel: "Offline", + snapshots: null, + isPending: false, + error: "unreachable", + }, + ]); + + expect(result.cards.map((card) => card.key).sort()).toEqual([ + "instance:local:codex", + "instance:vps:codex", + ]); + expect(result.failedNodes).toEqual([ + { environmentId: "offline", environmentLabel: "Offline", error: "unreachable" }, + ]); + }); + + it("reports completion only after every active environment has returned a result", () => { + const localResult = { + environmentId: "local", + environmentLabel: "Local", + snapshots: [], + isPending: false, + error: null, + } satisfies EnvironmentUsageInput; + const staleResult = { + ...localResult, + environmentId: "stale", + environmentLabel: "Stale", + } satisfies EnvironmentUsageInput; + + expect( + areProviderUsageResultsComplete(["local", "vps"], { + local: localResult, + stale: staleResult, + }), + ).toBe(false); + expect( + areProviderUsageResultsComplete(["local"], { + local: localResult, + stale: staleResult, + }), + ).toBe(true); + }); +}); + +describe("provider usage formatting", () => { + const nowMs = Date.parse("2026-07-17T10:00:00.000Z"); + + it("formats reset times, percentages, and credits", () => { + expect(formatProviderUsageReset("2026-07-17T13:48:00.000Z", nowMs)).toBe("Resets in 3h 48m"); + expect(formatProviderUsageExpiry("2026-07-17T13:48:00.000Z", nowMs)).toBe("Expires in 3h 48m"); + expect(formatProviderUsageRetry("2026-07-17T10:05:00.000Z", nowMs)).toBe("Retries in 5m"); + expect(providerUsagePercentLeft(130)).toBe(0); + expect( + formatProviderUsageCredits({ label: "Credits", usedCredits: 57.5, monthlyLimit: 200 }), + ).toBe("$142.50 left · $200.00 limit"); + expect( + providerUsageCreditsHaveMeter({ label: "Credits", usedCredits: 57.5, monthlyLimit: 200 }), + ).toBe(true); + expect(providerUsageCreditsHaveMeter({ label: "Credits", unlimited: true })).toBe(false); + expect(providerUsageCreditsHaveMeter({ label: "Credits", balance: "42 credits" })).toBe(false); + expect( + providerUsageCreditsHaveMeter({ label: "Credits", usedCredits: 0, monthlyLimit: 0 }), + ).toBe(false); + }); +}); diff --git a/packages/client-runtime/src/providerUsage.ts b/packages/client-runtime/src/providerUsage.ts new file mode 100644 index 00000000000..b15d3243350 --- /dev/null +++ b/packages/client-runtime/src/providerUsage.ts @@ -0,0 +1,245 @@ +/** + * Client-side aggregation and presentation helpers for provider usage. + * + * Provider limits are account-scoped, while T3 Code can connect to several + * environments backed by the same account. These helpers collapse duplicate + * account snapshots without coupling the logic to React or a platform UI. + * + * @module providerUsage + */ +import type { + ProviderUsageCredits, + ProviderUsageSnapshot, + ProviderUsageWindow, +} from "@t3tools/contracts"; + +/** One environment's provider-usage query state. */ +export interface EnvironmentUsageInput { + readonly environmentId: string; + readonly environmentLabel: string; + /** `null` until the first snapshot list arrives (loading/error). */ + readonly snapshots: ReadonlyArray | null; + readonly isPending: boolean; + readonly error: string | null; +} + +/** A provider account card, possibly merged across several environments. */ +export interface ProviderUsageCard { + /** Dedupe key, stable across renders. */ + readonly key: string; + readonly driver: ProviderUsageSnapshot["driver"]; + readonly instanceId: ProviderUsageSnapshot["instanceId"]; + readonly displayName: string; + readonly account: string | undefined; + readonly planLabel: string | undefined; + readonly status: ProviderUsageSnapshot["status"]; + readonly windows: ReadonlyArray; + readonly credits: ProviderUsageCredits | undefined; + readonly resetCredits: ProviderUsageSnapshot["resetCredits"]; + readonly freshness: ProviderUsageSnapshot["freshness"]; + readonly message: string | undefined; + /** Environment labels that reported this account (deduped and sorted). */ + readonly sourceNodes: ReadonlyArray; + readonly fetchedAt: string; +} + +export interface ProviderUsageNodeStatus { + readonly environmentId: string; + readonly environmentLabel: string; + readonly error?: string; +} + +export interface AggregatedProviderUsage { + readonly cards: ReadonlyArray; + /** Environments still loading their first result. */ + readonly pendingNodes: ReadonlyArray; + /** Environments that errored before returning any usage. */ + readonly failedNodes: ReadonlyArray; +} + +/** Whether every currently active environment has reported its first query state. */ +export function areProviderUsageResultsComplete( + environmentIds: ReadonlyArray, + results: Readonly>, +): boolean { + return environmentIds.every((environmentId) => results[environmentId] !== undefined); +} + +const PROVIDER_DISPLAY_NAMES: Record = { + claudeAgent: "Claude", + codex: "Codex", + cursor: "Cursor", + opencode: "OpenCode", + grok: "Grok", +}; + +export function providerUsageDisplayName(driver: string, snapshotName?: string): string { + if (snapshotName && snapshotName.length > 0) return snapshotName; + return PROVIDER_DISPLAY_NAMES[driver] ?? driver; +} + +const dedupeKey = (environmentId: string, snapshot: ProviderUsageSnapshot): string => + snapshot.account + ? `account:${snapshot.driver}:${snapshot.account.toLowerCase()}` + : `instance:${environmentId}:${snapshot.instanceId}`; + +/** + * Merge every environment's snapshots into deduped account cards plus + * pending/failed environment lists. The freshest duplicate supplies displayed + * values; every reporting environment remains visible in `sourceNodes`. + */ +export function aggregateProviderUsage( + inputs: ReadonlyArray, +): AggregatedProviderUsage { + const cards = new Map }>(); + const pendingNodes: ProviderUsageNodeStatus[] = []; + const failedNodes: ProviderUsageNodeStatus[] = []; + + for (const input of inputs) { + if (input.snapshots === null) { + if (input.error !== null) { + failedNodes.push({ + environmentId: input.environmentId, + environmentLabel: input.environmentLabel, + error: input.error, + }); + } else if (input.isPending) { + pendingNodes.push({ + environmentId: input.environmentId, + environmentLabel: input.environmentLabel, + }); + } + continue; + } + + for (const snapshot of input.snapshots) { + const key = dedupeKey(input.environmentId, snapshot); + const existing = cards.get(key); + const nextCard: ProviderUsageCard = { + key, + driver: snapshot.driver, + instanceId: snapshot.instanceId, + displayName: providerUsageDisplayName(snapshot.driver, snapshot.displayName), + account: snapshot.account, + planLabel: snapshot.planLabel, + status: snapshot.status, + windows: snapshot.windows, + credits: snapshot.credits, + resetCredits: snapshot.resetCredits, + freshness: snapshot.freshness, + message: snapshot.message, + sourceNodes: [input.environmentLabel], + fetchedAt: snapshot.fetchedAt, + }; + + if (!existing) { + cards.set(key, { card: nextCard, nodes: new Set([input.environmentLabel]) }); + continue; + } + + existing.nodes.add(input.environmentLabel); + const winner = snapshot.fetchedAt > existing.card.fetchedAt ? nextCard : existing.card; + existing.card = { ...winner, key, sourceNodes: existing.card.sourceNodes }; + } + } + + const finalized = [...cards.values()].map(({ card, nodes }) => ({ + ...card, + sourceNodes: [...nodes].sort((a, b) => a.localeCompare(b)), + })); + + finalized.sort( + (a, b) => a.displayName.localeCompare(b.displayName) || a.key.localeCompare(b.key), + ); + + return { cards: finalized, pendingNodes, failedNodes }; +} + +/** `100 - usedPercent`, clamped to the range rendered by usage meters. */ +export function providerUsagePercentLeft(usedPercent: number): number { + return Math.max(0, Math.min(100, 100 - usedPercent)); +} + +function formatProviderUsageFutureTime( + atIso: string | undefined, + nowMs: number, + prefix: "Resets" | "Expires" | "Retries", +): string | null { + if (!atIso) return null; + const atMs = Date.parse(atIso); + if (Number.isNaN(atMs)) return null; + const diffMs = atMs - nowMs; + if (diffMs <= 0) return null; + + const totalMinutes = Math.floor(diffMs / 60_000); + const minutes = totalMinutes % 60; + const totalHours = Math.floor(totalMinutes / 60); + const hours = totalHours % 24; + const days = Math.floor(totalHours / 24); + + if (days > 0) return `${prefix} in ${days}d ${hours}h`; + if (totalHours > 0) { + return minutes > 0 ? `${prefix} in ${hours}h ${minutes}m` : `${prefix} in ${hours}h`; + } + return `${prefix} in ${Math.max(1, minutes)}m`; +} + +/** Format a future reset timestamp as a compact relative duration. */ +export function formatProviderUsageReset( + resetsAtIso: string | undefined, + nowMs: number, +): string | null { + return formatProviderUsageFutureTime(resetsAtIso, nowMs, "Resets"); +} + +/** Format a future credit expiry as a compact relative duration. */ +export function formatProviderUsageExpiry( + expiresAtIso: string | undefined, + nowMs: number, +): string | null { + return formatProviderUsageFutureTime(expiresAtIso, nowMs, "Expires"); +} + +/** Format the retry time attached to a stale provider snapshot. */ +export function formatProviderUsageRetry( + retryAtIso: string | undefined, + nowMs: number, +): string | null { + return formatProviderUsageFutureTime(retryAtIso, nowMs, "Retries"); +} + +/** Format a credit balance, preferring numeric limit data when available. */ +export function formatProviderUsageCredits(credits: ProviderUsageCredits): string | null { + if (credits.unlimited) return "Unlimited"; + if (credits.usedCredits !== undefined && credits.monthlyLimit !== undefined) { + const left = Math.max(0, credits.monthlyLimit - credits.usedCredits); + return `${formatDollars(left)} left · ${formatDollars(credits.monthlyLimit)} limit`; + } + if (credits.balance) return credits.balance; + return null; +} + +/** Percentage consumed for a numeric credit limit. */ +export function providerUsageCreditsUsedPercent(credits: ProviderUsageCredits): number { + if ( + credits.usedCredits === undefined || + credits.monthlyLimit === undefined || + credits.monthlyLimit <= 0 + ) { + return 0; + } + return Math.max(0, Math.min(100, (credits.usedCredits / credits.monthlyLimit) * 100)); +} + +/** Whether a credit balance has enough numeric data for a meaningful usage meter. */ +export function providerUsageCreditsHaveMeter(credits: ProviderUsageCredits): boolean { + return ( + credits.usedCredits !== undefined && + credits.monthlyLimit !== undefined && + credits.monthlyLimit > 0 + ); +} + +function formatDollars(amount: number): string { + return `$${amount.toFixed(2)}`; +} diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index 7306a0a1071..6f84e3672c0 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -298,6 +298,15 @@ export function createServerEnvironmentAtoms( label: "environment-data:server:process-resource-history", tag: WS_METHODS.serverGetProcessResourceHistory, }), + // Usage is volatile and refreshed on demand (pull-to-refresh); a short + // stale time keeps the meters current while the server's own 60s cache + // bounds upstream provider traffic. Idle-evict after 5min offscreen. + providerUsage: createEnvironmentRpcQueryAtomFamily(runtime, { + label: "environment-data:server:provider-usage", + tag: WS_METHODS.serverGetProviderUsage, + staleTimeMs: 30_000, + idleTtlMs: 5 * 60_000, + }), configProjection, welcome: createEnvironmentRpcSubscriptionAtomFamily(runtime, { label: "environment-data:server:welcome", diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 43270efdec7..d5ec668deab 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -10,6 +10,7 @@ export * from "./terminal.ts"; export * from "./provider.ts"; export * from "./providerInstance.ts"; export * from "./providerRuntime.ts"; +export * from "./providerUsage.ts"; export * from "./model.ts"; export * from "./keybindings.ts"; export * from "./server.ts"; diff --git a/packages/contracts/src/providerUsage.test.ts b/packages/contracts/src/providerUsage.test.ts new file mode 100644 index 00000000000..b5600607aab --- /dev/null +++ b/packages/contracts/src/providerUsage.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "@effect/vitest"; +import * as Schema from "effect/Schema"; + +import { ProviderUsageResult, ProviderUsageSnapshot } from "./providerUsage.ts"; + +const decodeProviderUsageSnapshot = Schema.decodeUnknownSync(ProviderUsageSnapshot); +const encodeProviderUsageSnapshot = Schema.encodeSync(ProviderUsageSnapshot); +const decodeProviderUsageResult = Schema.decodeUnknownSync(ProviderUsageResult); + +describe("ProviderUsageSnapshot", () => { + it("decodes and round-trips a complete usage snapshot", () => { + const input = { + instanceId: "claude_personal", + driver: "claudeAgent", + displayName: "Claude Personal", + account: "person@example.com", + status: "ok", + planLabel: "Claude Max", + windows: [ + { + id: "five_hour", + label: "Session", + kind: "session", + usedPercent: 42, + resetsAt: "2025-01-15T12:00:00.000Z", + windowMinutes: 300, + }, + ], + credits: { + label: "Extra usage", + usedCredits: 12.5, + monthlyLimit: 100, + unlimited: false, + }, + resetCredits: { + availableCount: 1, + credits: [ + { + id: "reset-1", + title: "Usage reset", + expiresAt: "2025-01-22T10:00:00.000Z", + }, + ], + }, + freshness: { + state: "stale", + retryAt: "2025-01-15T10:05:00.000Z", + }, + message: "Live usage is temporarily rate limited.", + fetchedAt: "2025-01-15T10:00:00.000Z", + }; + + expect(encodeProviderUsageSnapshot(decodeProviderUsageSnapshot(input))).toEqual(input); + }); + + it("defaults omitted windows to an empty array", () => { + const decoded = decodeProviderUsageSnapshot({ + instanceId: "codex", + driver: "codex", + status: "unsupported", + fetchedAt: "2025-01-15T10:00:00.000Z", + }); + + expect(decoded.windows).toEqual([]); + }); +}); + +describe("ProviderUsageResult", () => { + it("decodes a usage result", () => { + const decoded = decodeProviderUsageResult({ + usage: [ + { + instanceId: "codex", + driver: "codex", + status: "ok", + windows: [], + fetchedAt: "2025-01-15T10:00:00.000Z", + }, + ], + }); + + expect(decoded.usage).toHaveLength(1); + expect(decoded.usage[0]).toMatchObject({ + instanceId: "codex", + driver: "codex", + status: "ok", + }); + }); +}); + +describe("ProviderUsageSnapshot validation", () => { + it("rejects invalid statuses and missing required identity fields", () => { + expect(() => + decodeProviderUsageSnapshot({ + instanceId: "codex", + driver: "codex", + status: "pending", + windows: [], + fetchedAt: "2025-01-15T10:00:00.000Z", + }), + ).toThrow(); + expect(() => + decodeProviderUsageSnapshot({ + driver: "codex", + status: "ok", + windows: [], + fetchedAt: "2025-01-15T10:00:00.000Z", + }), + ).toThrow(); + expect(() => + decodeProviderUsageSnapshot({ + instanceId: "codex", + driver: "codex", + status: "ok", + windows: [], + }), + ).toThrow(); + }); +}); diff --git a/packages/contracts/src/providerUsage.ts b/packages/contracts/src/providerUsage.ts new file mode 100644 index 00000000000..2f909616950 --- /dev/null +++ b/packages/contracts/src/providerUsage.ts @@ -0,0 +1,131 @@ +/** + * Provider account-level usage contracts. + * + * Normalized subscription rate-limit usage (session/weekly windows, credits) + * reported per provider instance. Providers whose drivers cannot report usage + * surface as `status: "unsupported"` rather than being omitted, so clients can + * render a complete picture of every configured instance. + * + * Snapshots are request/response data fetched on demand (not part of the + * broadcast `ServerProvider` config stream) — usage changes continuously and + * would churn the persisted config cache. The `account.rate-limits.updated` + * runtime event remains the future hook for live-push updates into this same + * shape. + * + * @module providerUsage + */ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts"; + +export const ProviderUsageWindowKind = Schema.Literals(["session", "weekly", "model", "other"]); +export type ProviderUsageWindowKind = typeof ProviderUsageWindowKind.Type; + +/** + * One rate-limit window (e.g. Claude's 5-hour session window). `usedPercent` + * is the raw consumed percentage from the provider; clients decide the + * presentation orientation ("92% left"). Labels for `kind: "model"` windows + * come from the provider response verbatim — model names are never hardcoded + * because providers rename them without notice. + */ +export const ProviderUsageWindow = Schema.Struct({ + /** Stable identifier within a snapshot, e.g. `five_hour`, `primary`, `model:opus`. */ + id: TrimmedNonEmptyString, + label: TrimmedNonEmptyString, + kind: ProviderUsageWindowKind, + /** Consumed percentage of the window, clamped to 0–100 by producers. */ + usedPercent: Schema.Number, + resetsAt: Schema.optional(IsoDateTime), + windowMinutes: Schema.optional(Schema.Number), +}); +export type ProviderUsageWindow = typeof ProviderUsageWindow.Type; + +/** Overflow/credit balances (Claude "extra usage", Codex credits). */ +export const ProviderUsageCredits = Schema.Struct({ + label: TrimmedNonEmptyString, + /** Preformatted balance when the provider reports an opaque amount. */ + balance: Schema.optional(TrimmedNonEmptyString), + /** Consumed amount in the account currency (dollars). */ + usedCredits: Schema.optional(Schema.Number), + /** Cap in the account currency (dollars); absent means no configured cap. */ + monthlyLimit: Schema.optional(Schema.Number), + unlimited: Schema.optional(Schema.Boolean), +}); +export type ProviderUsageCredits = typeof ProviderUsageCredits.Type; + +/** One available credit that can reset provider rate-limit windows. */ +export const ProviderUsageResetCredit = Schema.Struct({ + /** Opaque provider identifier. Read-only until a separate redemption capability is approved. */ + id: TrimmedNonEmptyString, + title: Schema.optional(TrimmedNonEmptyString), + description: Schema.optional(TrimmedNonEmptyString), + expiresAt: Schema.optional(IsoDateTime), +}); +export type ProviderUsageResetCredit = typeof ProviderUsageResetCredit.Type; + +/** Read-only inventory of on-demand rate-limit reset credits. */ +export const ProviderUsageResetCredits = Schema.Struct({ + availableCount: Schema.Number, + /** Absent details mean only the aggregate count was available upstream. */ + credits: Schema.Array(ProviderUsageResetCredit).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), +}); +export type ProviderUsageResetCredits = typeof ProviderUsageResetCredits.Type; + +/** Optional freshness metadata. Absence means the snapshot is live. */ +export const ProviderUsageFreshness = Schema.Struct({ + state: Schema.Literals(["fresh", "stale"]), + /** Earliest time the collector intends to retry a throttled live request. */ + retryAt: Schema.optional(IsoDateTime), +}); +export type ProviderUsageFreshness = typeof ProviderUsageFreshness.Type; + +export const ProviderUsageStatus = Schema.Literals([ + "ok", + "unauthenticated", + "unsupported", + "error", +]); +export type ProviderUsageStatus = typeof ProviderUsageStatus.Type; + +/** + * Usage for one provider instance. Flat struct with a `status` discriminant + * (mirroring how `ServerProvider` folds availability/auth into one record): + * per-instance failures become `unauthenticated`/`error` snapshots so a single + * broken provider never fails the whole result. + */ +export const ProviderUsageSnapshot = Schema.Struct({ + instanceId: ProviderInstanceId, + driver: ProviderDriverKind, + displayName: Schema.optional(TrimmedNonEmptyString), + /** + * Account identity (email or auth label) used by clients to dedupe the same + * provider account reported by multiple server nodes. Absent when the + * driver does not expose one — those snapshots are never deduped. + */ + account: Schema.optional(TrimmedNonEmptyString), + status: ProviderUsageStatus, + /** Subscription tier, e.g. "Max 20x", "ChatGPT Pro". */ + planLabel: Schema.optional(TrimmedNonEmptyString), + windows: Schema.Array(ProviderUsageWindow).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + credits: Schema.optional(ProviderUsageCredits), + resetCredits: Schema.optional(ProviderUsageResetCredits), + freshness: Schema.optional(ProviderUsageFreshness), + /** Human guidance or a freshness notice associated with this snapshot. */ + message: Schema.optional(TrimmedNonEmptyString), + fetchedAt: IsoDateTime, +}); +export type ProviderUsageSnapshot = typeof ProviderUsageSnapshot.Type; + +export const ProviderUsageInput = Schema.Struct({ + instanceId: Schema.optional(ProviderInstanceId), +}); +export type ProviderUsageInput = typeof ProviderUsageInput.Type; + +export const ProviderUsageResult = Schema.Struct({ + usage: Schema.Array(ProviderUsageSnapshot), +}); +export type ProviderUsageResult = typeof ProviderUsageResult.Type; diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 48c5d9a774d..7ac18376702 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -59,6 +59,7 @@ import { OrchestrationRpcSchemas, } from "./orchestration.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; +import { ProviderUsageInput, ProviderUsageResult } from "./providerUsage.ts"; import { RelayClientInstallFailedError, RelayClientInstallProgressEventSchema, @@ -213,6 +214,7 @@ export const WS_METHODS = { serverGetProcessDiagnostics: "server.getProcessDiagnostics", serverGetProcessResourceHistory: "server.getProcessResourceHistory", serverSignalProcess: "server.signalProcess", + serverGetProviderUsage: "server.getProviderUsage", // Cloud environment methods cloudGetRelayClientStatus: "cloud.getRelayClientStatus", @@ -317,6 +319,12 @@ export const WsServerSignalProcessRpc = Rpc.make(WS_METHODS.serverSignalProcess, error: EnvironmentAuthorizationError, }); +export const WsServerGetProviderUsageRpc = Rpc.make(WS_METHODS.serverGetProviderUsage, { + payload: ProviderUsageInput, + success: ProviderUsageResult, + error: EnvironmentAuthorizationError, +}); + export const WsCloudGetRelayClientStatusRpc = Rpc.make(WS_METHODS.cloudGetRelayClientStatus, { payload: Schema.Struct({}), success: RelayClientStatusSchema, @@ -694,6 +702,7 @@ export const WsRpcGroup = RpcGroup.make( WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, + WsServerGetProviderUsageRpc, WsCloudGetRelayClientStatusRpc, WsCloudInstallRelayClientRpc, WsSourceControlLookupRepositoryRpc, diff --git a/packages/effect-codex-app-server/scripts/generate.ts b/packages/effect-codex-app-server/scripts/generate.ts index 3deb4514292..5683d86acb1 100644 --- a/packages/effect-codex-app-server/scripts/generate.ts +++ b/packages/effect-codex-app-server/scripts/generate.ts @@ -17,7 +17,7 @@ import { } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -const UPSTREAM_REF = "b39f943a634a6e7ba86c3d6e8cf6d5f35e612566"; +const UPSTREAM_REF = "58ec5283156c3fce6421d38684afef0a656129d4"; const USER_AGENT = "effect-codex-app-server-generator"; const GITHUB_API_BASE = "https://api.github.com/repos/openai/codex/contents/codex-rs/app-server-protocol"; @@ -349,10 +349,12 @@ function resolveResponseTypeName( "account/logout": "LogoutAccountResponse", "account/rateLimits/read": "GetAccountRateLimitsResponse", "account/usage/read": "GetAccountTokenUsageResponse", + "account/workspaceMessages/read": "GetWorkspaceMessagesResponse", "config/batchWrite": "ConfigWriteResponse", "config/mcpServer/reload": "McpServerRefreshResponse", "config/value/write": "ConfigWriteResponse", "configRequirements/read": "ConfigRequirementsReadResponse", + "externalAgentConfig/import/readHistories": "ExternalAgentConfigImportHistoriesReadResponse", }; const override = overrides[method]; diff --git a/packages/effect-codex-app-server/src/_generated/meta.gen.ts b/packages/effect-codex-app-server/src/_generated/meta.gen.ts index ed39896bdb9..7824543e450 100644 --- a/packages/effect-codex-app-server/src/_generated/meta.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/meta.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 58ec5283156c3fce6421d38684afef0a656129d4 import * as CodexSchema from "./schema.gen.ts"; @@ -73,7 +73,9 @@ export const CLIENT_REQUEST_METHODS = { "account/login/cancel": "account/login/cancel", "account/logout": "account/logout", "account/rateLimits/read": "account/rateLimits/read", + "account/rateLimitResetCredit/consume": "account/rateLimitResetCredit/consume", "account/usage/read": "account/usage/read", + "account/workspaceMessages/read": "account/workspaceMessages/read", "account/sendAddCreditsNudgeEmail": "account/sendAddCreditsNudgeEmail", "feedback/upload": "feedback/upload", "command/exec": "command/exec", @@ -83,6 +85,7 @@ export const CLIENT_REQUEST_METHODS = { "config/read": "config/read", "externalAgentConfig/detect": "externalAgentConfig/detect", "externalAgentConfig/import": "externalAgentConfig/import", + "externalAgentConfig/import/readHistories": "externalAgentConfig/import/readHistories", "config/value/write": "config/value/write", "config/batchWrite": "config/batchWrite", "configRequirements/read": "configRequirements/read", @@ -152,6 +155,7 @@ export const SERVER_NOTIFICATION_METHODS = { "account/rateLimits/updated": "account/rateLimits/updated", "app/list/updated": "app/list/updated", "remoteControl/status/changed": "remoteControl/status/changed", + "externalAgentConfig/import/progress": "externalAgentConfig/import/progress", "externalAgentConfig/import/completed": "externalAgentConfig/import/completed", "fs/changed": "fs/changed", "item/reasoning/summaryTextDelta": "item/reasoning/summaryTextDelta", @@ -161,6 +165,7 @@ export const SERVER_NOTIFICATION_METHODS = { "model/rerouted": "model/rerouted", "model/verification": "model/verification", "turn/moderationMetadata": "turn/moderationMetadata", + "model/safetyBuffering/updated": "model/safetyBuffering/updated", warning: "warning", guardianWarning: "guardianWarning", deprecationNotice: "deprecationNotice", @@ -255,7 +260,9 @@ export interface ClientRequestParamsByMethod { readonly "account/login/cancel": CodexSchema.V2CancelLoginAccountParams; readonly "account/logout": undefined; readonly "account/rateLimits/read": undefined; + readonly "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditParams; readonly "account/usage/read": undefined; + readonly "account/workspaceMessages/read": undefined; readonly "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailParams; readonly "feedback/upload": CodexSchema.V2FeedbackUploadParams; readonly "command/exec": CodexSchema.V2CommandExecParams; @@ -265,6 +272,7 @@ export interface ClientRequestParamsByMethod { readonly "config/read": CodexSchema.V2ConfigReadParams; readonly "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectParams; readonly "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportParams; + readonly "externalAgentConfig/import/readHistories": undefined; readonly "config/value/write": CodexSchema.V2ConfigValueWriteParams; readonly "config/batchWrite": CodexSchema.V2ConfigBatchWriteParams; readonly "configRequirements/read": undefined; @@ -345,7 +353,9 @@ export interface ClientRequestResponsesByMethod { readonly "account/login/cancel": CodexSchema.V2CancelLoginAccountResponse; readonly "account/logout": CodexSchema.V2LogoutAccountResponse; readonly "account/rateLimits/read": CodexSchema.V2GetAccountRateLimitsResponse; + readonly "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse; readonly "account/usage/read": CodexSchema.V2GetAccountTokenUsageResponse; + readonly "account/workspaceMessages/read": CodexSchema.V2GetWorkspaceMessagesResponse; readonly "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailResponse; readonly "feedback/upload": CodexSchema.V2FeedbackUploadResponse; readonly "command/exec": CodexSchema.V2CommandExecResponse; @@ -355,6 +365,7 @@ export interface ClientRequestResponsesByMethod { readonly "config/read": CodexSchema.V2ConfigReadResponse; readonly "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectResponse; readonly "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportResponse; + readonly "externalAgentConfig/import/readHistories": CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse; readonly "config/value/write": CodexSchema.V2ConfigWriteResponse; readonly "config/batchWrite": CodexSchema.V2ConfigWriteResponse; readonly "configRequirements/read": CodexSchema.V2ConfigRequirementsReadResponse; @@ -437,6 +448,7 @@ export interface ServerNotificationParamsByMethod { readonly "account/rateLimits/updated": CodexSchema.V2AccountRateLimitsUpdatedNotification; readonly "app/list/updated": CodexSchema.V2AppListUpdatedNotification; readonly "remoteControl/status/changed": CodexSchema.V2RemoteControlStatusChangedNotification; + readonly "externalAgentConfig/import/progress": CodexSchema.V2ExternalAgentConfigImportProgressNotification; readonly "externalAgentConfig/import/completed": CodexSchema.V2ExternalAgentConfigImportCompletedNotification; readonly "fs/changed": CodexSchema.V2FsChangedNotification; readonly "item/reasoning/summaryTextDelta": CodexSchema.V2ReasoningSummaryTextDeltaNotification; @@ -446,6 +458,7 @@ export interface ServerNotificationParamsByMethod { readonly "model/rerouted": CodexSchema.V2ModelReroutedNotification; readonly "model/verification": CodexSchema.V2ModelVerificationNotification; readonly "turn/moderationMetadata": CodexSchema.V2TurnModerationMetadataNotification; + readonly "model/safetyBuffering/updated": CodexSchema.V2ModelSafetyBufferingUpdatedNotification; readonly warning: CodexSchema.V2WarningNotification; readonly guardianWarning: CodexSchema.V2GuardianWarningNotification; readonly deprecationNotice: CodexSchema.V2DeprecationNoticeNotification; @@ -535,7 +548,9 @@ export const CLIENT_REQUEST_PARAMS = { "account/login/cancel": CodexSchema.V2CancelLoginAccountParams, "account/logout": undefined, "account/rateLimits/read": undefined, + "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditParams, "account/usage/read": undefined, + "account/workspaceMessages/read": undefined, "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailParams, "feedback/upload": CodexSchema.V2FeedbackUploadParams, "command/exec": CodexSchema.V2CommandExecParams, @@ -545,6 +560,7 @@ export const CLIENT_REQUEST_PARAMS = { "config/read": CodexSchema.V2ConfigReadParams, "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectParams, "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportParams, + "externalAgentConfig/import/readHistories": undefined, "config/value/write": CodexSchema.V2ConfigValueWriteParams, "config/batchWrite": CodexSchema.V2ConfigBatchWriteParams, "configRequirements/read": undefined, @@ -625,7 +641,9 @@ export const CLIENT_REQUEST_RESPONSES = { "account/login/cancel": CodexSchema.V2CancelLoginAccountResponse, "account/logout": CodexSchema.V2LogoutAccountResponse, "account/rateLimits/read": CodexSchema.V2GetAccountRateLimitsResponse, + "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse, "account/usage/read": CodexSchema.V2GetAccountTokenUsageResponse, + "account/workspaceMessages/read": CodexSchema.V2GetWorkspaceMessagesResponse, "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailResponse, "feedback/upload": CodexSchema.V2FeedbackUploadResponse, "command/exec": CodexSchema.V2CommandExecResponse, @@ -635,6 +653,8 @@ export const CLIENT_REQUEST_RESPONSES = { "config/read": CodexSchema.V2ConfigReadResponse, "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectResponse, "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportResponse, + "externalAgentConfig/import/readHistories": + CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse, "config/value/write": CodexSchema.V2ConfigWriteResponse, "config/batchWrite": CodexSchema.V2ConfigWriteResponse, "configRequirements/read": CodexSchema.V2ConfigRequirementsReadResponse, @@ -718,6 +738,8 @@ export const SERVER_NOTIFICATION_PARAMS = { "account/rateLimits/updated": CodexSchema.V2AccountRateLimitsUpdatedNotification, "app/list/updated": CodexSchema.V2AppListUpdatedNotification, "remoteControl/status/changed": CodexSchema.V2RemoteControlStatusChangedNotification, + "externalAgentConfig/import/progress": + CodexSchema.V2ExternalAgentConfigImportProgressNotification, "externalAgentConfig/import/completed": CodexSchema.V2ExternalAgentConfigImportCompletedNotification, "fs/changed": CodexSchema.V2FsChangedNotification, @@ -728,6 +750,7 @@ export const SERVER_NOTIFICATION_PARAMS = { "model/rerouted": CodexSchema.V2ModelReroutedNotification, "model/verification": CodexSchema.V2ModelVerificationNotification, "turn/moderationMetadata": CodexSchema.V2TurnModerationMetadataNotification, + "model/safetyBuffering/updated": CodexSchema.V2ModelSafetyBufferingUpdatedNotification, warning: CodexSchema.V2WarningNotification, guardianWarning: CodexSchema.V2GuardianWarningNotification, deprecationNotice: CodexSchema.V2DeprecationNoticeNotification, diff --git a/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts b/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts index 74154a2769f..7efdf8d34ad 100644 --- a/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 58ec5283156c3fce6421d38684afef0a656129d4 import * as CodexSchema from "./schema.gen.ts"; @@ -35,6 +35,9 @@ export const v2 = { ConfigValueWriteParams: CodexSchema.V2ConfigValueWriteParams, ConfigWarningNotification: CodexSchema.V2ConfigWarningNotification, ConfigWriteResponse: CodexSchema.V2ConfigWriteResponse, + ConsumeAccountRateLimitResetCreditParams: CodexSchema.V2ConsumeAccountRateLimitResetCreditParams, + ConsumeAccountRateLimitResetCreditResponse: + CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse, ContextCompactedNotification: CodexSchema.V2ContextCompactedNotification, DeprecationNoticeNotification: CodexSchema.V2DeprecationNoticeNotification, ErrorNotification: CodexSchema.V2ErrorNotification, @@ -46,7 +49,11 @@ export const v2 = { ExternalAgentConfigDetectResponse: CodexSchema.V2ExternalAgentConfigDetectResponse, ExternalAgentConfigImportCompletedNotification: CodexSchema.V2ExternalAgentConfigImportCompletedNotification, + ExternalAgentConfigImportHistoriesReadResponse: + CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse, ExternalAgentConfigImportParams: CodexSchema.V2ExternalAgentConfigImportParams, + ExternalAgentConfigImportProgressNotification: + CodexSchema.V2ExternalAgentConfigImportProgressNotification, ExternalAgentConfigImportResponse: CodexSchema.V2ExternalAgentConfigImportResponse, FeedbackUploadParams: CodexSchema.V2FeedbackUploadParams, FeedbackUploadResponse: CodexSchema.V2FeedbackUploadResponse, @@ -75,6 +82,7 @@ export const v2 = { GetAccountRateLimitsResponse: CodexSchema.V2GetAccountRateLimitsResponse, GetAccountResponse: CodexSchema.V2GetAccountResponse, GetAccountTokenUsageResponse: CodexSchema.V2GetAccountTokenUsageResponse, + GetWorkspaceMessagesResponse: CodexSchema.V2GetWorkspaceMessagesResponse, GuardianWarningNotification: CodexSchema.V2GuardianWarningNotification, HookCompletedNotification: CodexSchema.V2HookCompletedNotification, HooksListParams: CodexSchema.V2HooksListParams, @@ -112,6 +120,7 @@ export const v2 = { ModelProviderCapabilitiesReadParams: CodexSchema.V2ModelProviderCapabilitiesReadParams, ModelProviderCapabilitiesReadResponse: CodexSchema.V2ModelProviderCapabilitiesReadResponse, ModelReroutedNotification: CodexSchema.V2ModelReroutedNotification, + ModelSafetyBufferingUpdatedNotification: CodexSchema.V2ModelSafetyBufferingUpdatedNotification, ModelVerificationNotification: CodexSchema.V2ModelVerificationNotification, PermissionProfileListParams: CodexSchema.V2PermissionProfileListParams, PermissionProfileListResponse: CodexSchema.V2PermissionProfileListResponse, diff --git a/packages/effect-codex-app-server/src/_generated/schema.gen.ts b/packages/effect-codex-app-server/src/_generated/schema.gen.ts index fb93292384d..fe88c3200a9 100644 --- a/packages/effect-codex-app-server/src/_generated/schema.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/schema.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 58ec5283156c3fce6421d38684afef0a656129d4 import * as Schema from "effect/Schema"; @@ -51,12 +51,17 @@ export const ClientRequest__AddCreditsNudgeCreditType = Schema.Literals(["credit export type ClientRequest__AdditionalContextKind = "untrusted" | "application"; export const ClientRequest__AdditionalContextKind = Schema.Literals(["untrusted", "application"]); -export type ClientRequest__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; -}; +export type ClientRequest__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const ClientRequest__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -121,7 +126,6 @@ export const ClientRequest__AppsListParams = Schema.Struct({ export type ClientRequest__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -135,7 +139,7 @@ export type ClientRequest__AskForApproval = }; export const ClientRequest__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -260,6 +264,46 @@ export const ClientRequest__ConfigReadParams = Schema.Struct({ includeLayers: Schema.optionalKey(Schema.Boolean), }); +export type ClientRequest__ConsumeAccountRateLimitResetCreditParams = { + readonly creditId?: string | null; + readonly idempotencyKey: string; +}; +export const ClientRequest__ConsumeAccountRateLimitResetCreditParams = Schema.Struct({ + creditId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + }), + Schema.Null, + ]), + ), + idempotencyKey: Schema.String.annotate({ + description: + "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + }), +}); + +export type ClientRequest__DynamicToolNamespaceTool = { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; +}; +export const ClientRequest__DynamicToolNamespaceTool = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolNamespaceToolType" }), + }).annotate({ title: "FunctionDynamicToolNamespaceTool" }), + ], + { mode: "oneOf" }, +); + export type ClientRequest__ExperimentalFeatureEnablementSetParams = { readonly enablement: { readonly [x: string]: boolean }; }; @@ -321,7 +365,7 @@ export const ClientRequest__ExternalAgentConfigDetectParams = Schema.Struct({ ), includeHome: Schema.optionalKey( Schema.Boolean.annotate({ - description: "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + description: "If true, include detection under the user's home directory.", }), ), }); @@ -527,6 +571,7 @@ export const ClientRequest__ImageDetail = Schema.Literals(["auto", "low", "high" export type ClientRequest__InitializeCapabilities = { readonly experimentalApi?: boolean; + readonly mcpServerOpenaiFormElicitation?: boolean; readonly optOutNotificationMethods?: ReadonlyArray | null; readonly requestAttestation?: boolean; }; @@ -537,6 +582,11 @@ export const ClientRequest__InitializeCapabilities = Schema.Struct({ default: false, }), ), + mcpServerOpenaiFormElicitation: Schema.optionalKey( + Schema.Boolean.annotate({ + description: "Allow downstream MCP servers to request OpenAI extended form elicitations.", + }), + ), optOutNotificationMethods: Schema.optionalKey( Schema.Union([ Schema.Array(Schema.String).annotate({ @@ -554,6 +604,19 @@ export const ClientRequest__InitializeCapabilities = Schema.Struct({ ), }).annotate({ description: "Client-declared capabilities negotiated during initialize." }); +export type ClientRequest__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const ClientRequest__InternalChatMessageMetadataPassthrough = Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", +}); + +export type ClientRequest__LegacyAppPathString = string; +export const ClientRequest__LegacyAppPathString = Schema.String; + export type ClientRequest__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -684,11 +747,13 @@ export const ClientRequest__McpServerMigration = Schema.Struct({ name: Schema.St export type ClientRequest__McpServerOauthLoginParams = { readonly name: string; readonly scopes?: ReadonlyArray | null; + readonly threadId?: string | null; readonly timeoutSecs?: number | null; }; export const ClientRequest__McpServerOauthLoginParams = Schema.Struct({ name: Schema.String, scopes: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), timeoutSecs: Schema.optionalKey( Schema.Union([Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), Schema.Null]), ), @@ -808,12 +873,14 @@ export type ClientRequest__PluginListMarketplaceKind = | "local" | "vertical" | "workspace-directory" - | "shared-with-me"; + | "shared-with-me" + | "created-by-me-remote"; export const ClientRequest__PluginListMarketplaceKind = Schema.Literals([ "local", "vertical", "workspace-directory", "shared-with-me", + "created-by-me-remote", ]); export type ClientRequest__PluginShareCheckoutParams = { readonly remotePluginId: string }; @@ -1042,6 +1109,9 @@ export const ClientRequest__SessionMigration = Schema.Struct({ title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ClientRequest__SkillMigration = { readonly name: string }; +export const ClientRequest__SkillMigration = Schema.Struct({ name: Schema.String }); + export type ClientRequest__SkillsListParams = { readonly cwds?: ReadonlyArray; readonly forceReload?: boolean; @@ -1236,7 +1306,7 @@ export const ClientRequest__ThreadRollbackParams = Schema.Struct({ .check(Schema.isInt()) .check(Schema.isGreaterThanOrEqualTo(0)), threadId: Schema.String, -}); +}).annotate({ description: "DEPRECATED: `thread/rollback` will be removed soon." }); export type ClientRequest__ThreadSetNameParams = { readonly name: string; @@ -1259,8 +1329,12 @@ export const ClientRequest__ThreadShellCommandParams = Schema.Struct({ threadId: Schema.String, }); -export type ClientRequest__ThreadSortKey = "created_at" | "updated_at"; -export const ClientRequest__ThreadSortKey = Schema.Literals(["created_at", "updated_at"]); +export type ClientRequest__ThreadSortKey = "created_at" | "updated_at" | "recency_at"; +export const ClientRequest__ThreadSortKey = Schema.Literals([ + "created_at", + "updated_at", + "recency_at", +]); export type ClientRequest__ThreadSource = string; export const ClientRequest__ThreadSource = Schema.String; @@ -1367,6 +1441,9 @@ export const CommandExecutionRequestApprovalParams__FileSystemSpecialPath = Sche { mode: "oneOf" }, ); +export type CommandExecutionRequestApprovalParams__LegacyAppPathString = string; +export const CommandExecutionRequestApprovalParams__LegacyAppPathString = Schema.String; + export type CommandExecutionRequestApprovalParams__NetworkApprovalProtocol = | "http" | "https" @@ -1664,11 +1741,8 @@ export const PermissionsRequestApprovalParams__FileSystemSpecialPath = Schema.Un { mode: "oneOf" }, ); -export type PermissionsRequestApprovalResponse__AbsolutePathBuf = string; -export const PermissionsRequestApprovalResponse__AbsolutePathBuf = Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", -}); +export type PermissionsRequestApprovalParams__LegacyAppPathString = string; +export const PermissionsRequestApprovalParams__LegacyAppPathString = Schema.String; export type PermissionsRequestApprovalResponse__AdditionalNetworkPermissions = { readonly enabled?: boolean | null; @@ -1718,6 +1792,9 @@ export const PermissionsRequestApprovalResponse__FileSystemSpecialPath = Schema. { mode: "oneOf" }, ); +export type PermissionsRequestApprovalResponse__LegacyAppPathString = string; +export const PermissionsRequestApprovalResponse__LegacyAppPathString = Schema.String; + export type ServerNotification__AbsolutePathBuf = string; export const ServerNotification__AbsolutePathBuf = Schema.String.annotate({ description: @@ -1821,7 +1898,6 @@ export const ServerNotification__ApprovalsReviewer = Schema.Literals([ export type ServerNotification__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -1835,7 +1911,7 @@ export type ServerNotification__AskForApproval = }; export const ServerNotification__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -1854,13 +1930,15 @@ export type ServerNotification__AuthMode = | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" - | "personalAccessToken"; + | "personalAccessToken" + | "bedrockApiKey"; export const ServerNotification__AuthMode = Schema.Literals([ "apikey", "chatgpt", "chatgptAuthTokens", "agentIdentity", "personalAccessToken", + "bedrockApiKey", ]).annotate({ description: "Authentication mode for OpenAI-backed providers." }); export type ServerNotification__AutoReviewDecisionSource = "agent"; @@ -1999,8 +2077,27 @@ export const ServerNotification__DynamicToolCallStatus = Schema.Literals([ "failed", ]); -export type ServerNotification__ExternalAgentConfigImportCompletedNotification = {}; -export const ServerNotification__ExternalAgentConfigImportCompletedNotification = Schema.Struct({}); +export type ServerNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "SESSIONS"; +export const ServerNotification__ExternalAgentConfigMigrationItemType = Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", +]); export type ServerNotification__FileChangeOutputDeltaNotification = { readonly delta: string; @@ -2193,17 +2290,27 @@ export const ServerNotification__HookScope = Schema.Literals(["thread", "turn"]) export type ServerNotification__ImageDetail = "auto" | "low" | "high" | "original"; export const ServerNotification__ImageDetail = Schema.Literals(["auto", "low", "high", "original"]); +export type ServerNotification__LegacyAppPathString = string; +export const ServerNotification__LegacyAppPathString = Schema.String; + export type ServerNotification__McpServerOauthLoginCompletedNotification = { readonly error?: string | null; readonly name: string; readonly success: boolean; + readonly threadId?: string | null; }; export const ServerNotification__McpServerOauthLoginCompletedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, success: Schema.Boolean, + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ServerNotification__McpServerStartupFailureReason = "reauthenticationRequired"; +export const ServerNotification__McpServerStartupFailureReason = Schema.Literal( + "reauthenticationRequired", +); + export type ServerNotification__McpServerStartupState = | "starting" | "ready" @@ -2216,6 +2323,23 @@ export const ServerNotification__McpServerStartupState = Schema.Literals([ "cancelled", ]); +export type ServerNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const ServerNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type ServerNotification__McpToolCallError = { readonly message: string }; export const ServerNotification__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -2284,6 +2408,25 @@ export const ServerNotification__ModeKind = Schema.Literals(["plan", "default"]) export type ServerNotification__ModelRerouteReason = "highRiskCyberActivity"; export const ServerNotification__ModelRerouteReason = Schema.Literal("highRiskCyberActivity"); +export type ServerNotification__ModelSafetyBufferingUpdatedNotification = { + readonly fasterModel?: string | null; + readonly model: string; + readonly reasons: ReadonlyArray; + readonly showBufferingUi: boolean; + readonly threadId: string; + readonly turnId: string; + readonly useCases: ReadonlyArray; +}; +export const ServerNotification__ModelSafetyBufferingUpdatedNotification = Schema.Struct({ + fasterModel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + model: Schema.String, + reasons: Schema.Array(Schema.String), + showBufferingUi: Schema.Boolean, + threadId: Schema.String, + turnId: Schema.String, + useCases: Schema.Array(Schema.String), +}); + export type ServerNotification__ModelVerification = "trustedAccessForCyber"; export const ServerNotification__ModelVerification = Schema.Literal("trustedAccessForCyber"); @@ -3032,6 +3175,9 @@ export const ServerRequest__FileSystemSpecialPath = Schema.Union( { mode: "oneOf" }, ); +export type ServerRequest__LegacyAppPathString = string; +export const ServerRequest__LegacyAppPathString = Schema.String; + export type ServerRequest__McpElicitationArrayType = "array"; export const ServerRequest__McpElicitationArrayType = Schema.Literal("array"); @@ -3168,6 +3314,7 @@ export const V1InitializeParams__ClientInfo = Schema.Struct({ export type V1InitializeParams__InitializeCapabilities = { readonly experimentalApi?: boolean; + readonly mcpServerOpenaiFormElicitation?: boolean; readonly optOutNotificationMethods?: ReadonlyArray | null; readonly requestAttestation?: boolean; }; @@ -3178,6 +3325,11 @@ export const V1InitializeParams__InitializeCapabilities = Schema.Struct({ default: false, }), ), + mcpServerOpenaiFormElicitation: Schema.optionalKey( + Schema.Boolean.annotate({ + description: "Allow downstream MCP servers to request OpenAI extended form elicitations.", + }), + ), optOutNotificationMethods: Schema.optionalKey( Schema.Union([ Schema.Array(Schema.String).annotate({ @@ -3281,13 +3433,15 @@ export type V2AccountUpdatedNotification__AuthMode = | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" - | "personalAccessToken"; + | "personalAccessToken" + | "bedrockApiKey"; export const V2AccountUpdatedNotification__AuthMode = Schema.Literals([ "apikey", "chatgpt", "chatgptAuthTokens", "agentIdentity", "personalAccessToken", + "bedrockApiKey", ]).annotate({ description: "Authentication mode for OpenAI-backed providers." }); export type V2AccountUpdatedNotification__PlanType = @@ -3445,20 +3599,8 @@ export const V2ConfigReadResponse__ApprovalsReviewer = Schema.Literals([ "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", }); -export type V2ConfigReadResponse__AppsDefaultConfig = { - readonly destructive_enabled?: boolean; - readonly enabled?: boolean; - readonly open_world_enabled?: boolean; -}; -export const V2ConfigReadResponse__AppsDefaultConfig = Schema.Struct({ - destructive_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), - enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), - open_world_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), -}); - export type V2ConfigReadResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -3472,7 +3614,7 @@ export type V2ConfigReadResponse__AskForApproval = }; export const V2ConfigReadResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -3572,12 +3714,16 @@ export const V2ConfigReadResponse__WebSearchLocation = Schema.Struct({ timezone: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); -export type V2ConfigReadResponse__WebSearchMode = "disabled" | "cached" | "live"; -export const V2ConfigReadResponse__WebSearchMode = Schema.Literals(["disabled", "cached", "live"]); +export type V2ConfigReadResponse__WebSearchMode = "disabled" | "cached" | "indexed" | "live"; +export const V2ConfigReadResponse__WebSearchMode = Schema.Literals([ + "disabled", + "cached", + "indexed", + "live", +]); export type V2ConfigRequirementsReadResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -3591,7 +3737,7 @@ export type V2ConfigRequirementsReadResponse__AskForApproval = }; export const V2ConfigRequirementsReadResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -3662,6 +3808,11 @@ export const V2ConfigRequirementsReadResponse__NetworkUnixSocketPermission = Sch "deny", ]); +export type V2ConfigRequirementsReadResponse__ReasoningEffort = string; +export const V2ConfigRequirementsReadResponse__ReasoningEffort = Schema.String.annotate({ + description: "A non-empty reasoning effort value advertised by the model.", +}).check(Schema.isMinLength(1)); + export type V2ConfigRequirementsReadResponse__ResidencyRequirement = "us"; export const V2ConfigRequirementsReadResponse__ResidencyRequirement = Schema.Literal("us"); @@ -3675,10 +3826,15 @@ export const V2ConfigRequirementsReadResponse__SandboxMode = Schema.Literals([ "danger-full-access", ]); -export type V2ConfigRequirementsReadResponse__WebSearchMode = "disabled" | "cached" | "live"; +export type V2ConfigRequirementsReadResponse__WebSearchMode = + | "disabled" + | "cached" + | "indexed" + | "live"; export const V2ConfigRequirementsReadResponse__WebSearchMode = Schema.Literals([ "disabled", "cached", + "indexed", "live", ]); @@ -3716,6 +3872,11 @@ export const V2ConfigWriteResponse__AbsolutePathBuf = Schema.String.annotate({ export type V2ConfigWriteResponse__WriteStatus = "ok" | "okOverridden"; export const V2ConfigWriteResponse__WriteStatus = Schema.Literals(["ok", "okOverridden"]); +export type V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = + "reset" | "nothingToReset" | "noCredit" | "alreadyRedeemed"; +export const V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = + Schema.Literals(["reset", "nothingToReset", "noCredit", "alreadyRedeemed"]); + export type V2ErrorNotification__NonSteerableTurnKind = "review" | "compact"; export const V2ErrorNotification__NonSteerableTurnKind = Schema.Literals(["review", "compact"]); @@ -3828,11 +3989,62 @@ export const V2ExternalAgentConfigDetectResponse__SessionMigration = Schema.Stru title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2ExternalAgentConfigDetectResponse__SkillMigration = { readonly name: string }; +export const V2ExternalAgentConfigDetectResponse__SkillMigration = Schema.Struct({ + name: Schema.String, +}); + export type V2ExternalAgentConfigDetectResponse__SubagentMigration = { readonly name: string }; export const V2ExternalAgentConfigDetectResponse__SubagentMigration = Schema.Struct({ name: Schema.String, }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "SESSIONS"; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", + ]); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "SESSIONS"; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", + ]); + export type V2ExternalAgentConfigImportParams__CommandMigration = { readonly name: string }; export const V2ExternalAgentConfigImportParams__CommandMigration = Schema.Struct({ name: Schema.String, @@ -3891,11 +4103,39 @@ export const V2ExternalAgentConfigImportParams__SessionMigration = Schema.Struct title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2ExternalAgentConfigImportParams__SkillMigration = { readonly name: string }; +export const V2ExternalAgentConfigImportParams__SkillMigration = Schema.Struct({ + name: Schema.String, +}); + export type V2ExternalAgentConfigImportParams__SubagentMigration = { readonly name: string }; export const V2ExternalAgentConfigImportParams__SubagentMigration = Schema.Struct({ name: Schema.String, }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "SESSIONS"; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", + ]); + export type V2FileChangePatchUpdatedNotification__PatchChangeKind = | { readonly type: "add" } | { readonly type: "delete" } @@ -3992,6 +4232,24 @@ export const V2GetAccountRateLimitsResponse__RateLimitReachedType = Schema.Liter "workspace_member_usage_limit_reached", ]); +export type V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus = + | "available" + | "redeeming" + | "redeemed" + | "unknown"; +export const V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus = Schema.Literals([ + "available", + "redeeming", + "redeemed", + "unknown", +]); + +export type V2GetAccountRateLimitsResponse__RateLimitResetType = "codexRateLimits" | "unknown"; +export const V2GetAccountRateLimitsResponse__RateLimitResetType = Schema.Literals([ + "codexRateLimits", + "unknown", +]); + export type V2GetAccountRateLimitsResponse__RateLimitWindow = { readonly resetsAt?: number | null; readonly usedPercent: number; @@ -4082,6 +4340,16 @@ export const V2GetAccountTokenUsageResponse__AccountTokenUsageSummary = Schema.S ), }); +export type V2GetWorkspaceMessagesResponse__WorkspaceMessageType = + | "headline" + | "announcement" + | "unknown"; +export const V2GetWorkspaceMessagesResponse__WorkspaceMessageType = Schema.Literals([ + "headline", + "announcement", + "unknown", +]); + export type V2HookCompletedNotification__AbsolutePathBuf = string; export const V2HookCompletedNotification__AbsolutePathBuf = Schema.String.annotate({ description: @@ -4384,6 +4652,26 @@ export const V2ItemCompletedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ItemCompletedNotification__LegacyAppPathString = string; +export const V2ItemCompletedNotification__LegacyAppPathString = Schema.String; + +export type V2ItemCompletedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ItemCompletedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ItemCompletedNotification__McpToolCallError = { readonly message: string }; export const V2ItemCompletedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -4634,6 +4922,9 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__GuardianUserAuth description: "[UNSTABLE] Authorization level assigned by approval auto-review.", }); +export type V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString = string; +export const V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString = Schema.String; + export type V2ItemGuardianApprovalReviewCompletedNotification__NetworkApprovalProtocol = | "http" | "https" @@ -4735,6 +5026,9 @@ export const V2ItemGuardianApprovalReviewStartedNotification__GuardianUserAuthor description: "[UNSTABLE] Authorization level assigned by approval auto-review.", }); +export type V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString = string; +export const V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString = Schema.String; + export type V2ItemGuardianApprovalReviewStartedNotification__NetworkApprovalProtocol = | "http" | "https" @@ -4827,6 +5121,26 @@ export const V2ItemStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ItemStartedNotification__LegacyAppPathString = string; +export const V2ItemStartedNotification__LegacyAppPathString = Schema.String; + +export type V2ItemStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ItemStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ItemStartedNotification__McpToolCallError = { readonly message: string }; export const V2ItemStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -5133,6 +5447,12 @@ export const V2McpResourceReadResponse__ResourceContent = Schema.Union([ }), ]).annotate({ description: "Contents returned when reading a resource from an MCP server." }); +export type V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason = + "reauthenticationRequired"; +export const V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason = Schema.Literal( + "reauthenticationRequired", +); + export type V2McpServerStatusUpdatedNotification__McpServerStartupState = | "starting" | "ready" @@ -5191,10 +5511,14 @@ export const V2ModelVerificationNotification__ModelVerification = Schema.Literal("trustedAccessForCyber"); export type V2PermissionProfileListResponse__PermissionProfileSummary = { + readonly allowed: boolean; readonly description?: string | null; readonly id: string; }; export const V2PermissionProfileListResponse__PermissionProfileSummary = Schema.Struct({ + allowed: Schema.Boolean.annotate({ + description: "Whether the effective requirements allow selecting this profile.", + }), description: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -5272,18 +5596,18 @@ export const V2PluginInstallParams__AbsolutePathBuf = Schema.String.annotate({ }); export type V2PluginInstallResponse__AppSummary = { + readonly category?: string | null; readonly description?: string | null; readonly id: string; readonly installUrl?: string | null; readonly name: string; - readonly needsAuth: boolean; }; export const V2PluginInstallResponse__AppSummary = Schema.Struct({ + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, - needsAuth: Schema.Boolean, }).annotate({ description: "EXPERIMENTAL - app metadata summary for plugin responses." }); export type V2PluginInstallResponse__PluginAuthPolicy = "ON_INSTALL" | "ON_USE"; @@ -5299,12 +5623,14 @@ export type V2PluginListParams__PluginListMarketplaceKind = | "local" | "vertical" | "workspace-directory" - | "shared-with-me"; + | "shared-with-me" + | "created-by-me-remote"; export const V2PluginListParams__PluginListMarketplaceKind = Schema.Literals([ "local", "vertical", "workspace-directory", "shared-with-me", + "created-by-me-remote", ]); export type V2PluginListResponse__AbsolutePathBuf = string; @@ -5365,18 +5691,18 @@ export const V2PluginReadResponse__AbsolutePathBuf = Schema.String.annotate({ }); export type V2PluginReadResponse__AppSummary = { + readonly category?: string | null; readonly description?: string | null; readonly id: string; readonly installUrl?: string | null; readonly name: string; - readonly needsAuth: boolean; }; export const V2PluginReadResponse__AppSummary = Schema.Struct({ + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, - needsAuth: Schema.Boolean, }).annotate({ description: "EXPERIMENTAL - app metadata summary for plugin responses." }); export type V2PluginReadResponse__AppTemplateUnavailableReason = @@ -5574,12 +5900,17 @@ export const V2PluginShareUpdateTargetsResponse__PluginSharePrincipalType = Sche "workspace", ]); -export type V2RawResponseItemCompletedNotification__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; -}; +export type V2RawResponseItemCompletedNotification__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2RawResponseItemCompletedNotification__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -5602,6 +5933,17 @@ export const V2RawResponseItemCompletedNotification__ImageDetail = Schema.Litera "original", ]); +export type V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough = + Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", + }); + export type V2RawResponseItemCompletedNotification__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -5867,6 +6209,26 @@ export const V2ReviewStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ReviewStartResponse__LegacyAppPathString = string; +export const V2ReviewStartResponse__LegacyAppPathString = Schema.String; + +export type V2ReviewStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ReviewStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ReviewStartResponse__McpToolCallError = { readonly message: string }; export const V2ReviewStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6114,7 +6476,6 @@ export const V2ThreadForkParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadForkParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -6128,7 +6489,7 @@ export type V2ThreadForkParams__AskForApproval = }; export const V2ThreadForkParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -6166,7 +6527,6 @@ export const V2ThreadForkResponse__AgentPath = Schema.String; export type V2ThreadForkResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -6180,7 +6540,7 @@ export type V2ThreadForkResponse__AskForApproval = }; export const V2ThreadForkResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -6280,6 +6640,26 @@ export const V2ThreadForkResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadForkResponse__LegacyAppPathString = string; +export const V2ThreadForkResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadForkResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadForkResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadForkResponse__McpToolCallError = { readonly message: string }; export const V2ThreadForkResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6528,8 +6908,12 @@ export const V2ThreadListParams__ThreadListCwdFilter = Schema.Union([ Schema.Array(Schema.String), ]); -export type V2ThreadListParams__ThreadSortKey = "created_at" | "updated_at"; -export const V2ThreadListParams__ThreadSortKey = Schema.Literals(["created_at", "updated_at"]); +export type V2ThreadListParams__ThreadSortKey = "created_at" | "updated_at" | "recency_at"; +export const V2ThreadListParams__ThreadSortKey = Schema.Literals([ + "created_at", + "updated_at", + "recency_at", +]); export type V2ThreadListParams__ThreadSourceKind = | "cli" @@ -6650,6 +7034,26 @@ export const V2ThreadListResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadListResponse__LegacyAppPathString = string; +export const V2ThreadListResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadListResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadListResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadListResponse__McpToolCallError = { readonly message: string }; export const V2ThreadListResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6958,6 +7362,26 @@ export const V2ThreadMetadataUpdateResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadMetadataUpdateResponse__LegacyAppPathString = string; +export const V2ThreadMetadataUpdateResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadMetadataUpdateResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadMetadataUpdateResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadMetadataUpdateResponse__McpToolCallError = { readonly message: string }; export const V2ThreadMetadataUpdateResponse__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -7241,6 +7665,26 @@ export const V2ThreadReadResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadReadResponse__LegacyAppPathString = string; +export const V2ThreadReadResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadReadResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadReadResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadReadResponse__McpToolCallError = { readonly message: string }; export const V2ThreadReadResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -7450,12 +7894,17 @@ export const V2ThreadRealtimeStartedNotification__RealtimeConversationVersion = "v2", ]); -export type V2ThreadResumeParams__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; -}; +export type V2ThreadResumeParams__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2ThreadResumeParams__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -7478,7 +7927,6 @@ export const V2ThreadResumeParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadResumeParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -7492,7 +7940,7 @@ export type V2ThreadResumeParams__AskForApproval = }; export const V2ThreadResumeParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -7514,6 +7962,16 @@ export const V2ThreadResumeParams__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadResumeParams__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const V2ThreadResumeParams__InternalChatMessageMetadataPassthrough = Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", +}); + export type V2ThreadResumeParams__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -7670,7 +8128,6 @@ export const V2ThreadResumeResponse__AgentPath = Schema.String; export type V2ThreadResumeResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -7684,7 +8141,7 @@ export type V2ThreadResumeResponse__AskForApproval = }; export const V2ThreadResumeResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -7784,6 +8241,26 @@ export const V2ThreadResumeResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadResumeResponse__LegacyAppPathString = string; +export const V2ThreadResumeResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadResumeResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadResumeResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadResumeResponse__McpToolCallError = { readonly message: string }; export const V2ThreadResumeResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -8054,6 +8531,26 @@ export const V2ThreadRollbackResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadRollbackResponse__LegacyAppPathString = string; +export const V2ThreadRollbackResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadRollbackResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadRollbackResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadRollbackResponse__McpToolCallError = { readonly message: string }; export const V2ThreadRollbackResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -8276,7 +8773,6 @@ export const V2ThreadSettingsUpdatedNotification__ApprovalsReviewer = Schema.Lit export type V2ThreadSettingsUpdatedNotification__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8290,7 +8786,7 @@ export type V2ThreadSettingsUpdatedNotification__AskForApproval = }; export const V2ThreadSettingsUpdatedNotification__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8436,6 +8932,26 @@ export const V2ThreadStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadStartedNotification__LegacyAppPathString = string; +export const V2ThreadStartedNotification__LegacyAppPathString = Schema.String; + +export type V2ThreadStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadStartedNotification__McpToolCallError = { readonly message: string }; export const V2ThreadStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -8621,12 +9137,6 @@ export const V2ThreadStartedNotification__WebSearchAction = Schema.Union( { mode: "oneOf" }, ); -export type V2ThreadStartParams__AbsolutePathBuf = string; -export const V2ThreadStartParams__AbsolutePathBuf = Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", -}); - export type V2ThreadStartParams__ApprovalsReviewer = "user" | "auto_review" | "guardian_subagent"; export const V2ThreadStartParams__ApprovalsReviewer = Schema.Literals([ "user", @@ -8639,7 +9149,6 @@ export const V2ThreadStartParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadStartParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8653,7 +9162,7 @@ export type V2ThreadStartParams__AskForApproval = }; export const V2ThreadStartParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8667,6 +9176,29 @@ export const V2ThreadStartParams__AskForApproval = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartParams__DynamicToolNamespaceTool = { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; +}; +export const V2ThreadStartParams__DynamicToolNamespaceTool = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolNamespaceToolType" }), + }).annotate({ title: "FunctionDynamicToolNamespaceTool" }), + ], + { mode: "oneOf" }, +); + +export type V2ThreadStartParams__LegacyAppPathString = string; +export const V2ThreadStartParams__LegacyAppPathString = Schema.String; + export type V2ThreadStartParams__Personality = "none" | "friendly" | "pragmatic"; export const V2ThreadStartParams__Personality = Schema.Literals(["none", "friendly", "pragmatic"]); @@ -8697,7 +9229,6 @@ export const V2ThreadStartResponse__AgentPath = Schema.String; export type V2ThreadStartResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8711,7 +9242,7 @@ export type V2ThreadStartResponse__AskForApproval = }; export const V2ThreadStartResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8811,6 +9342,26 @@ export const V2ThreadStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadStartResponse__LegacyAppPathString = string; +export const V2ThreadStartResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadStartResponse__McpToolCallError = { readonly message: string }; export const V2ThreadStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -9107,6 +9658,26 @@ export const V2ThreadUnarchiveResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadUnarchiveResponse__LegacyAppPathString = string; +export const V2ThreadUnarchiveResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadUnarchiveResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2ThreadUnarchiveResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadUnarchiveResponse__McpToolCallError = { readonly message: string }; export const V2ThreadUnarchiveResponse__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9386,6 +9957,26 @@ export const V2TurnCompletedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnCompletedNotification__LegacyAppPathString = string; +export const V2TurnCompletedNotification__LegacyAppPathString = Schema.String; + +export type V2TurnCompletedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2TurnCompletedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnCompletedNotification__McpToolCallError = { readonly message: string }; export const V2TurnCompletedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9651,6 +10242,26 @@ export const V2TurnStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnStartedNotification__LegacyAppPathString = string; +export const V2TurnStartedNotification__LegacyAppPathString = Schema.String; + +export type V2TurnStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2TurnStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnStartedNotification__McpToolCallError = { readonly message: string }; export const V2TurnStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9846,7 +10457,6 @@ export const V2TurnStartParams__ApprovalsReviewer = Schema.Literals([ export type V2TurnStartParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -9860,7 +10470,7 @@ export type V2TurnStartParams__AskForApproval = }; export const V2TurnStartParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -9877,6 +10487,9 @@ export const V2TurnStartParams__AskForApproval = Schema.Union( export type V2TurnStartParams__ImageDetail = "auto" | "low" | "high" | "original"; export const V2TurnStartParams__ImageDetail = Schema.Literals(["auto", "low", "high", "original"]); +export type V2TurnStartParams__LegacyAppPathString = string; +export const V2TurnStartParams__LegacyAppPathString = Schema.String; + export type V2TurnStartParams__ModeKind = "plan" | "default"; export const V2TurnStartParams__ModeKind = Schema.Literals(["plan", "default"]).annotate({ description: "Initial collaboration mode to use when the TUI starts.", @@ -10008,6 +10621,26 @@ export const V2TurnStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnStartResponse__LegacyAppPathString = string; +export const V2TurnStartResponse__LegacyAppPathString = Schema.String; + +export type V2TurnStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; + readonly templateId?: string | null; +}; +export const V2TurnStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + templateId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnStartResponse__McpToolCallError = { readonly message: string }; export const V2TurnStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -10618,6 +11251,7 @@ export type ClientRequest__MigrationDetails = { readonly mcpServers?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const ClientRequest__MigrationDetails = Schema.Struct({ @@ -10634,6 +11268,7 @@ export const ClientRequest__MigrationDetails = Schema.Struct({ sessions: Schema.optionalKey( Schema.Array(ClientRequest__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey(Schema.Array(ClientRequest__SkillMigration).annotate({ default: [] })), subagents: Schema.optionalKey( Schema.Array(ClientRequest__SubagentMigration).annotate({ default: [] }), ), @@ -10730,6 +11365,7 @@ export type ClientRequest__ThreadForkParams = { readonly cwd?: string | null; readonly developerInstructions?: string | null; readonly ephemeral?: boolean; + readonly lastTurnId?: string | null; readonly model?: string | null; readonly modelProvider?: string | null; readonly sandbox?: ClientRequest__SandboxMode | null; @@ -10752,6 +11388,15 @@ export const ClientRequest__ThreadForkParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), developerInstructions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), ephemeral: Schema.optionalKey(Schema.Boolean), + lastTurnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.", + }), + Schema.Null, + ]), + ), model: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -10966,7 +11611,10 @@ export const CommandExecutionRequestApprovalParams__CommandAction = Schema.Union ); export type CommandExecutionRequestApprovalParams__FileSystemPath = - | { readonly path: CommandExecutionRequestApprovalParams__AbsolutePathBuf; readonly type: "path" } + | { + readonly path: CommandExecutionRequestApprovalParams__LegacyAppPathString; + readonly type: "path"; + } | { readonly pattern: string; readonly type: "glob_pattern" } | { readonly type: "special"; @@ -10975,7 +11623,7 @@ export type CommandExecutionRequestApprovalParams__FileSystemPath = export const CommandExecutionRequestApprovalParams__FileSystemPath = Schema.Union( [ Schema.Struct({ - path: CommandExecutionRequestApprovalParams__AbsolutePathBuf, + path: CommandExecutionRequestApprovalParams__LegacyAppPathString, type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), }).annotate({ title: "PathFileSystemPath" }), Schema.Struct({ @@ -11264,7 +11912,7 @@ export const McpServerElicitationRequestParams__McpElicitationUntitledSingleSele }); export type PermissionsRequestApprovalParams__FileSystemPath = - | { readonly path: PermissionsRequestApprovalParams__AbsolutePathBuf; readonly type: "path" } + | { readonly path: PermissionsRequestApprovalParams__LegacyAppPathString; readonly type: "path" } | { readonly pattern: string; readonly type: "glob_pattern" } | { readonly type: "special"; @@ -11273,7 +11921,7 @@ export type PermissionsRequestApprovalParams__FileSystemPath = export const PermissionsRequestApprovalParams__FileSystemPath = Schema.Union( [ Schema.Struct({ - path: PermissionsRequestApprovalParams__AbsolutePathBuf, + path: PermissionsRequestApprovalParams__LegacyAppPathString, type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), }).annotate({ title: "PathFileSystemPath" }), Schema.Struct({ @@ -11289,7 +11937,10 @@ export const PermissionsRequestApprovalParams__FileSystemPath = Schema.Union( ); export type PermissionsRequestApprovalResponse__FileSystemPath = - | { readonly path: PermissionsRequestApprovalResponse__AbsolutePathBuf; readonly type: "path" } + | { + readonly path: PermissionsRequestApprovalResponse__LegacyAppPathString; + readonly type: "path"; + } | { readonly pattern: string; readonly type: "glob_pattern" } | { readonly type: "special"; @@ -11298,7 +11949,7 @@ export type PermissionsRequestApprovalResponse__FileSystemPath = export const PermissionsRequestApprovalResponse__FileSystemPath = Schema.Union( [ Schema.Struct({ - path: PermissionsRequestApprovalResponse__AbsolutePathBuf, + path: PermissionsRequestApprovalResponse__LegacyAppPathString, type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), }).annotate({ title: "PathFileSystemPath" }), Schema.Struct({ @@ -11451,27 +12102,35 @@ export const ServerNotification__CollabAgentState = Schema.Struct({ status: ServerNotification__CollabAgentStatus, }); -export type ServerNotification__FileSystemPath = - | { readonly path: ServerNotification__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } - | { readonly type: "special"; readonly value: ServerNotification__FileSystemSpecialPath }; -export const ServerNotification__FileSystemPath = Schema.Union( - [ - Schema.Struct({ - path: ServerNotification__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), - Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: ServerNotification__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), - ], - { mode: "oneOf" }, -); +export type ServerNotification__ExternalAgentConfigImportItemTypeFailure = { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; +}; +export const ServerNotification__ExternalAgentConfigImportItemTypeFailure = Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + +export type ServerNotification__ExternalAgentConfigImportItemTypeSuccess = { + readonly cwd?: string | null; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; +}; +export const ServerNotification__ExternalAgentConfigImportItemTypeSuccess = Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); export type ServerNotification__FuzzyFileSearchResult = { readonly file_name: string; @@ -11528,14 +12187,40 @@ export const ServerNotification__HookOutputEntry = Schema.Struct({ text: Schema.String, }); +export type ServerNotification__FileSystemPath = + | { readonly path: ServerNotification__LegacyAppPathString; readonly type: "path" } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { readonly type: "special"; readonly value: ServerNotification__FileSystemSpecialPath }; +export const ServerNotification__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: ServerNotification__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: ServerNotification__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); + export type ServerNotification__McpServerStatusUpdatedNotification = { readonly error?: string | null; + readonly failureReason?: ServerNotification__McpServerStartupFailureReason | null; readonly name: string; readonly status: ServerNotification__McpServerStartupState; readonly threadId?: string | null; }; export const ServerNotification__McpServerStatusUpdatedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureReason: Schema.optionalKey( + Schema.Union([ServerNotification__McpServerStartupFailureReason, Schema.Null]), + ), name: Schema.String, status: ServerNotification__McpServerStartupState, threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -11578,6 +12263,7 @@ export const ServerNotification__ModelVerificationNotification = Schema.Struct({ export type ServerNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -11600,6 +12286,7 @@ export const ServerNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -12021,13 +12708,13 @@ export const ServerRequest__ChatgptAuthTokensRefreshParams = Schema.Struct({ }); export type ServerRequest__FileSystemPath = - | { readonly path: ServerRequest__AbsolutePathBuf; readonly type: "path" } + | { readonly path: ServerRequest__LegacyAppPathString; readonly type: "path" } | { readonly pattern: string; readonly type: "glob_pattern" } | { readonly type: "special"; readonly value: ServerRequest__FileSystemSpecialPath }; export const ServerRequest__FileSystemPath = Schema.Union( [ Schema.Struct({ - path: ServerRequest__AbsolutePathBuf, + path: ServerRequest__LegacyAppPathString, type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), }).annotate({ title: "PathFileSystemPath" }), Schema.Struct({ @@ -12555,6 +13242,25 @@ export const V2ConfigReadResponse__ConfigLayerSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ConfigReadResponse__AppsDefaultConfig = { + readonly approvals_reviewer?: V2ConfigReadResponse__ApprovalsReviewer | null; + readonly default_tools_approval_mode?: V2ConfigReadResponse__AppToolApproval | null; + readonly destructive_enabled?: boolean; + readonly enabled?: boolean; + readonly open_world_enabled?: boolean; +}; +export const V2ConfigReadResponse__AppsDefaultConfig = Schema.Struct({ + approvals_reviewer: Schema.optionalKey( + Schema.Union([V2ConfigReadResponse__ApprovalsReviewer, Schema.Null]), + ), + default_tools_approval_mode: Schema.optionalKey( + Schema.Union([V2ConfigReadResponse__AppToolApproval, Schema.Null]), + ), + destructive_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), + enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), + open_world_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), +}); + export type V2ConfigReadResponse__WebSearchToolConfig = { readonly allowed_domains?: ReadonlyArray | null; readonly context_size?: V2ConfigReadResponse__WebSearchContextSize | null; @@ -12579,50 +13285,17 @@ export const V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup = Sche matcher: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); -export type V2ConfigRequirementsReadResponse__ConfigRequirements = { - readonly allowAppshots?: boolean | null; - readonly allowManagedHooksOnly?: boolean | null; - readonly allowedApprovalPolicies?: ReadonlyArray | null; - readonly allowedPermissionProfiles?: { readonly [x: string]: boolean } | null; - readonly allowedSandboxModes?: ReadonlyArray | null; - readonly allowedWebSearchModes?: ReadonlyArray | null; - readonly allowedWindowsSandboxImplementations?: ReadonlyArray | null; - readonly computerUse?: V2ConfigRequirementsReadResponse__ComputerUseRequirements | null; - readonly defaultPermissions?: string | null; - readonly enforceResidency?: V2ConfigRequirementsReadResponse__ResidencyRequirement | null; - readonly featureRequirements?: { readonly [x: string]: boolean } | null; +export type V2ConfigRequirementsReadResponse__NewThreadModelDefaults = { + readonly model?: string | null; + readonly modelReasoningEffort?: V2ConfigRequirementsReadResponse__ReasoningEffort | null; + readonly serviceTier?: string | null; }; -export const V2ConfigRequirementsReadResponse__ConfigRequirements = Schema.Struct({ - allowAppshots: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - allowManagedHooksOnly: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - allowedApprovalPolicies: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__AskForApproval), Schema.Null]), - ), - allowedPermissionProfiles: Schema.optionalKey( - Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), - ), - allowedSandboxModes: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__SandboxMode), Schema.Null]), - ), - allowedWebSearchModes: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__WebSearchMode), Schema.Null]), - ), - allowedWindowsSandboxImplementations: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ConfigRequirementsReadResponse__WindowsSandboxSetupMode), - Schema.Null, - ]), - ), - computerUse: Schema.optionalKey( - Schema.Union([V2ConfigRequirementsReadResponse__ComputerUseRequirements, Schema.Null]), - ), - defaultPermissions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - enforceResidency: Schema.optionalKey( - Schema.Union([V2ConfigRequirementsReadResponse__ResidencyRequirement, Schema.Null]), - ), - featureRequirements: Schema.optionalKey( - Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), +export const V2ConfigRequirementsReadResponse__NewThreadModelDefaults = Schema.Struct({ + model: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + modelReasoningEffort: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ReasoningEffort, Schema.Null]), ), + serviceTier: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); export type V2ConfigWarningNotification__TextRange = { @@ -12734,6 +13407,7 @@ export const V2ConfigWriteResponse__ConfigLayerSource = Schema.Union( export type V2ErrorNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -12756,6 +13430,7 @@ export const V2ErrorNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -12846,6 +13521,7 @@ export type V2ExternalAgentConfigDetectResponse__MigrationDetails = { readonly mcpServers?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const V2ExternalAgentConfigDetectResponse__MigrationDetails = Schema.Struct({ @@ -12864,17 +13540,93 @@ export const V2ExternalAgentConfigDetectResponse__MigrationDetails = Schema.Stru sessions: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey( + Schema.Array(V2ExternalAgentConfigDetectResponse__SkillMigration).annotate({ default: [] }), + ), subagents: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__SubagentMigration).annotate({ default: [] }), ), }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + }; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + export type V2ExternalAgentConfigImportParams__MigrationDetails = { readonly commands?: ReadonlyArray; readonly hooks?: ReadonlyArray; readonly mcpServers?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const V2ExternalAgentConfigImportParams__MigrationDetails = Schema.Struct({ @@ -12893,11 +13645,48 @@ export const V2ExternalAgentConfigImportParams__MigrationDetails = Schema.Struct sessions: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey( + Schema.Array(V2ExternalAgentConfigImportParams__SkillMigration).annotate({ default: [] }), + ), subagents: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__SubagentMigration).annotate({ default: [] }), ), }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + }; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + export type V2FileChangePatchUpdatedNotification__FileUpdateChange = { readonly diff: string; readonly kind: V2FileChangePatchUpdatedNotification__PatchChangeKind; @@ -12909,6 +13698,52 @@ export const V2FileChangePatchUpdatedNotification__FileUpdateChange = Schema.Str path: Schema.String, }); +export type V2GetAccountRateLimitsResponse__RateLimitResetCredit = { + readonly description?: string | null; + readonly expiresAt?: number | null; + readonly grantedAt: number; + readonly id: string; + readonly resetType: V2GetAccountRateLimitsResponse__RateLimitResetType; + readonly status: V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus; + readonly title?: string | null; +}; +export const V2GetAccountRateLimitsResponse__RateLimitResetCredit = Schema.Struct({ + description: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Backend-provided display description for this credit, or `null` when unavailable.", + }), + Schema.Null, + ]), + ), + expiresAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: + "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + grantedAt: Schema.Number.annotate({ + description: "Unix timestamp in seconds when the credit was granted.", + format: "int64", + }).check(Schema.isInt()), + id: Schema.String.annotate({ description: "Opaque backend identifier for this reset credit." }), + resetType: V2GetAccountRateLimitsResponse__RateLimitResetType, + status: V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus, + title: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Backend-provided display title for this credit, or `null` when unavailable.", + }), + Schema.Null, + ]), + ), +}); + export type V2GetAccountRateLimitsResponse__RateLimitSnapshot = { readonly credits?: V2GetAccountRateLimitsResponse__CreditsSnapshot | null; readonly individualLimit?: V2GetAccountRateLimitsResponse__SpendControlLimitSnapshot | null; @@ -12945,28 +13780,62 @@ export const V2GetAccountRateLimitsResponse__RateLimitSnapshot = Schema.Struct({ export type V2GetAccountResponse__Account = | { readonly type: "apiKey" } | { - readonly email: string; + readonly email: string | null; readonly planType: V2GetAccountResponse__PlanType; readonly type: "chatgpt"; } - | { readonly type: "amazonBedrock" }; + | { readonly credentialSource?: "codexManaged" | "awsManaged"; readonly type: "amazonBedrock" }; export const V2GetAccountResponse__Account = Schema.Union( [ Schema.Struct({ type: Schema.Literal("apiKey").annotate({ title: "ApiKeyAccountType" }), }).annotate({ title: "ApiKeyAccount" }), Schema.Struct({ - email: Schema.String, + email: Schema.Union([Schema.String, Schema.Null]), planType: V2GetAccountResponse__PlanType, type: Schema.Literal("chatgpt").annotate({ title: "ChatgptAccountType" }), }).annotate({ title: "ChatgptAccount" }), Schema.Struct({ + credentialSource: Schema.optionalKey( + Schema.Literals(["codexManaged", "awsManaged"]).annotate({ default: "awsManaged" }), + ), type: Schema.Literal("amazonBedrock").annotate({ title: "AmazonBedrockAccountType" }), }).annotate({ title: "AmazonBedrockAccount" }), ], { mode: "oneOf" }, ); +export type V2GetWorkspaceMessagesResponse__WorkspaceMessage = { + readonly archivedAt?: number | null; + readonly createdAt?: number | null; + readonly messageBody: string; + readonly messageId: string; + readonly messageType: V2GetWorkspaceMessagesResponse__WorkspaceMessageType; +}; +export const V2GetWorkspaceMessagesResponse__WorkspaceMessage = Schema.Struct({ + archivedAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) when the message was archived.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + createdAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) when the message was created.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + messageBody: Schema.String, + messageId: Schema.String, + messageType: V2GetWorkspaceMessagesResponse__WorkspaceMessageType, +}); + export type V2HookCompletedNotification__HookOutputEntry = { readonly kind: V2HookCompletedNotification__HookOutputEntryKind; readonly text: string; @@ -13151,34 +14020,6 @@ export const V2ItemCompletedNotification__UserInput = Schema.Union( { mode: "oneOf" }, ); -export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = - | { - readonly path: V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf; - readonly type: "path"; - } - | { readonly pattern: string; readonly type: "glob_pattern" } - | { - readonly type: "special"; - readonly value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath; - }; -export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = Schema.Union( - [ - Schema.Struct({ - path: V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), - Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), - ], - { mode: "oneOf" }, -); - export type V2ItemGuardianApprovalReviewCompletedNotification__GuardianApprovalReview = { readonly rationale?: string | null; readonly riskLevel?: V2ItemGuardianApprovalReviewCompletedNotification__GuardianRiskLevel | null; @@ -13206,20 +14047,20 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__GuardianApproval "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", }); -export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = +export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = | { - readonly path: V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf; + readonly path: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString; readonly type: "path"; } | { readonly pattern: string; readonly type: "glob_pattern" } | { readonly type: "special"; - readonly value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath; + readonly value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath; }; -export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = Schema.Union( +export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = Schema.Union( [ Schema.Struct({ - path: V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf, + path: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), }).annotate({ title: "PathFileSystemPath" }), Schema.Struct({ @@ -13228,7 +14069,7 @@ export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = S }).annotate({ title: "GlobPatternFileSystemPath" }), Schema.Struct({ type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath, + value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath, }).annotate({ title: "SpecialFileSystemPath" }), ], { mode: "oneOf" }, @@ -13261,6 +14102,34 @@ export const V2ItemGuardianApprovalReviewStartedNotification__GuardianApprovalRe "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", }); +export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = + | { + readonly path: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString; + readonly type: "path"; + } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath; + }; +export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); + export type V2ItemStartedNotification__CommandAction = | { readonly command: string; @@ -13437,7 +14306,9 @@ export type V2PluginInstalledResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginInstalledResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginInstalledResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13477,12 +14348,23 @@ export const V2PluginInstalledResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginInstalledResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13505,6 +14387,12 @@ export type V2PluginInstalledResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginInstalledResponse__PluginSource = Schema.Union( [ @@ -13519,6 +14407,25 @@ export const V2PluginInstalledResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13562,7 +14469,9 @@ export type V2PluginListResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginListResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginListResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13602,12 +14511,23 @@ export const V2PluginListResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginListResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13630,6 +14550,12 @@ export type V2PluginListResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginListResponse__PluginSource = Schema.Union( [ @@ -13644,6 +14570,25 @@ export const V2PluginListResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13678,7 +14623,9 @@ export type V2PluginReadResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginReadResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginReadResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13718,12 +14665,23 @@ export const V2PluginReadResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginReadResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13746,6 +14704,12 @@ export type V2PluginReadResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginReadResponse__PluginSource = Schema.Union( [ @@ -13760,6 +14724,25 @@ export const V2PluginReadResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13790,6 +14773,7 @@ export const V2PluginReadResponse__SkillInterface = Schema.Struct({ export type V2PluginReadResponse__AppTemplateSummary = { readonly canonicalConnectorId?: string | null; + readonly category?: string | null; readonly description?: string | null; readonly logoUrl?: string | null; readonly logoUrlDark?: string | null; @@ -13800,6 +14784,7 @@ export type V2PluginReadResponse__AppTemplateSummary = { }; export const V2PluginReadResponse__AppTemplateSummary = Schema.Struct({ canonicalConnectorId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), logoUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), logoUrlDark: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -13843,7 +14828,9 @@ export type V2PluginShareListResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginShareListResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginShareListResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13883,12 +14870,23 @@ export const V2PluginShareListResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginShareListResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13911,6 +14909,12 @@ export type V2PluginShareListResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginShareListResponse__PluginSource = Schema.Union( [ @@ -13925,6 +14929,25 @@ export const V2PluginShareListResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -14113,6 +15136,7 @@ export const V2ReviewStartResponse__MemoryCitation = Schema.Struct({ export type V2ReviewStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14135,6 +15159,7 @@ export const V2ReviewStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14370,6 +15395,7 @@ export const V2ThreadForkResponse__MemoryCitation = Schema.Struct({ export type V2ThreadForkResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14392,6 +15418,7 @@ export const V2ThreadForkResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14705,6 +15732,7 @@ export const V2ThreadListResponse__MemoryCitation = Schema.Struct({ export type V2ThreadListResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14727,6 +15755,7 @@ export const V2ThreadListResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14971,6 +16000,7 @@ export const V2ThreadMetadataUpdateResponse__MemoryCitation = Schema.Struct({ export type V2ThreadMetadataUpdateResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14993,6 +16023,7 @@ export const V2ThreadMetadataUpdateResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15241,6 +16272,7 @@ export const V2ThreadReadResponse__MemoryCitation = Schema.Struct({ export type V2ThreadReadResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15263,6 +16295,7 @@ export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15570,6 +16603,7 @@ export const V2ThreadResumeResponse__MemoryCitation = Schema.Struct({ export type V2ThreadResumeResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15592,6 +16626,7 @@ export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15836,6 +16871,7 @@ export const V2ThreadRollbackResponse__MemoryCitation = Schema.Struct({ export type V2ThreadRollbackResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15858,6 +16894,7 @@ export const V2ThreadRollbackResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16164,6 +17201,7 @@ export const V2ThreadStartedNotification__MemoryCitation = Schema.Struct({ export type V2ThreadStartedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16186,6 +17224,7 @@ export const V2ThreadStartedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16434,6 +17473,7 @@ export const V2ThreadStartResponse__MemoryCitation = Schema.Struct({ export type V2ThreadStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16456,6 +17496,7 @@ export const V2ThreadStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16740,6 +17781,7 @@ export const V2ThreadUnarchiveResponse__MemoryCitation = Schema.Struct({ export type V2ThreadUnarchiveResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16762,6 +17804,7 @@ export const V2ThreadUnarchiveResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17010,6 +18053,7 @@ export const V2TurnCompletedNotification__MemoryCitation = Schema.Struct({ export type V2TurnCompletedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17032,6 +18076,7 @@ export const V2TurnCompletedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17256,6 +18301,7 @@ export const V2TurnStartedNotification__MemoryCitation = Schema.Struct({ export type V2TurnStartedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17278,6 +18324,7 @@ export const V2TurnStartedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17603,6 +18650,7 @@ export const V2TurnStartResponse__MemoryCitation = Schema.Struct({ export type V2TurnStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17625,6 +18673,7 @@ export const V2TurnStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -18397,6 +19446,8 @@ export type ServerNotification__AppInfo = { readonly branding?: ServerNotification__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -18412,6 +19463,12 @@ export const ServerNotification__AppInfo = Schema.Struct({ branding: Schema.optionalKey(Schema.Union([ServerNotification__AppBranding, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -18431,13 +19488,15 @@ export const ServerNotification__AppInfo = Schema.Struct({ pluginDisplayNames: Schema.optionalKey(Schema.Array(Schema.String).annotate({ default: [] })), }).annotate({ description: "EXPERIMENTAL - app metadata returned by app-list APIs." }); -export type ServerNotification__FileSystemSandboxEntry = { - readonly access: ServerNotification__FileSystemAccessMode; - readonly path: ServerNotification__FileSystemPath; +export type ServerNotification__ExternalAgentConfigImportTypeResult = { + readonly failures: ReadonlyArray; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; }; -export const ServerNotification__FileSystemSandboxEntry = Schema.Struct({ - access: ServerNotification__FileSystemAccessMode, - path: ServerNotification__FileSystemPath, +export const ServerNotification__ExternalAgentConfigImportTypeResult = Schema.Struct({ + failures: Schema.Array(ServerNotification__ExternalAgentConfigImportItemTypeFailure), + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array(ServerNotification__ExternalAgentConfigImportItemTypeSuccess), }); export type ServerNotification__FuzzyFileSearchSessionUpdatedNotification = { @@ -18513,6 +19572,15 @@ export const ServerNotification__HookRunSummary = Schema.Struct({ statusMessage: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ServerNotification__FileSystemSandboxEntry = { + readonly access: ServerNotification__FileSystemAccessMode; + readonly path: ServerNotification__FileSystemPath; +}; +export const ServerNotification__FileSystemSandboxEntry = Schema.Struct({ + access: ServerNotification__FileSystemAccessMode, + path: ServerNotification__FileSystemPath, +}); + export type ServerNotification__TurnError = { readonly additionalDetails?: string | null; readonly codexErrorInfo?: ServerNotification__CodexErrorInfo | null; @@ -18604,6 +19672,7 @@ export type ServerNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: ServerNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: ServerNotification__McpToolCallError | null; @@ -18654,9 +19723,10 @@ export type ServerNotification__ThreadItem = } | { readonly id: string; - readonly path: ServerNotification__AbsolutePathBuf; + readonly path: ServerNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -18719,10 +19789,7 @@ export const ServerNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -18770,6 +19837,9 @@ export const ServerNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([ServerNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -18782,7 +19852,14 @@ export const ServerNotification__ThreadItem = Schema.Union( ), error: Schema.optionalKey(Schema.Union([ServerNotification__McpToolCallError, Schema.Null])), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([ServerNotification__McpToolCallResult, Schema.Null]), @@ -18880,9 +19957,16 @@ export const ServerNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: ServerNotification__AbsolutePathBuf, + path: ServerNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -19077,7 +20161,8 @@ export type ServerRequest__CommandExecutionRequestApprovalParams = { readonly approvalId?: string | null; readonly command?: string | null; readonly commandActions?: ReadonlyArray | null; - readonly cwd?: ServerRequest__AbsolutePathBuf | null; + readonly cwd?: ServerRequest__LegacyAppPathString | null; + readonly environmentId?: string | null; readonly itemId: string; readonly networkApprovalContext?: ServerRequest__NetworkApprovalContext | null; readonly proposedExecpolicyAmendment?: ReadonlyArray | null; @@ -19112,10 +20197,16 @@ export const ServerRequest__CommandExecutionRequestApprovalParams = Schema.Struc ]), ), cwd: Schema.optionalKey( - Schema.Union([ServerRequest__AbsolutePathBuf, Schema.Null]).annotate({ + Schema.Union([ServerRequest__LegacyAppPathString, Schema.Null]).annotate({ description: "The command's working directory.", }), ), + environmentId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Environment in which the command will run." }), + Schema.Null, + ]), + ), itemId: Schema.String, networkApprovalContext: Schema.optionalKey( Schema.Union([ServerRequest__NetworkApprovalContext, Schema.Null]).annotate({ @@ -19157,12 +20248,21 @@ export const ServerRequest__CommandExecutionRequestApprovalParams = Schema.Struc }); export type ServerRequest__ToolRequestUserInputParams = { + readonly autoResolutionMs?: number | null; readonly itemId: string; readonly questions: ReadonlyArray; readonly threadId: string; readonly turnId: string; }; export const ServerRequest__ToolRequestUserInputParams = Schema.Struct({ + autoResolutionMs: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null, + ]), + ), itemId: Schema.String, questions: Schema.Array(ServerRequest__ToolRequestUserInputQuestion), threadId: Schema.String, @@ -19174,6 +20274,8 @@ export type V2AppListUpdatedNotification__AppInfo = { readonly branding?: V2AppListUpdatedNotification__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -19193,6 +20295,12 @@ export const V2AppListUpdatedNotification__AppInfo = Schema.Struct({ ), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -19217,6 +20325,8 @@ export type V2AppsListResponse__AppInfo = { readonly branding?: V2AppsListResponse__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -19232,6 +20342,12 @@ export const V2AppsListResponse__AppInfo = Schema.Struct({ branding: Schema.optionalKey(Schema.Union([V2AppsListResponse__AppBranding, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -19282,6 +20398,15 @@ export const V2ConfigReadResponse__ToolsV2 = Schema.Struct({ ), }); +export type V2ConfigRequirementsReadResponse__ModelsRequirements = { + readonly newThread?: V2ConfigRequirementsReadResponse__NewThreadModelDefaults | null; +}; +export const V2ConfigRequirementsReadResponse__ModelsRequirements = Schema.Struct({ + newThread: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__NewThreadModelDefaults, Schema.Null]), + ), +}); + export type V2ConfigWriteResponse__ConfigLayerMetadata = { readonly name: V2ConfigWriteResponse__ConfigLayerSource; readonly version: string; @@ -19327,6 +20452,42 @@ export const V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationIt itemType: V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationItemType, }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult = + { + readonly failures: ReadonlyArray; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult = + Schema.Struct({ + failures: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure, + ), + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory = { + readonly completedAtMs: number; + readonly failures: ReadonlyArray; + readonly importId: string; + readonly successes: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory = + Schema.Struct({ + completedAtMs: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + failures: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure, + ), + importId: Schema.String, + successes: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + export type V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem = { readonly cwd?: string | null; readonly description: string; @@ -19350,6 +20511,39 @@ export const V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem itemType: V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItemType, }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult = { + readonly failures: ReadonlyArray; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult = + Schema.Struct({ + failures: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure, + ), + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + +export type V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary = { + readonly availableCount: number; + readonly credits?: ReadonlyArray | null; +}; +export const V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary = Schema.Struct({ + availableCount: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + credits: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2GetAccountRateLimitsResponse__RateLimitResetCredit).annotate({ + description: + "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + }), + Schema.Null, + ]), + ), +}); + export type V2HookCompletedNotification__HookRunSummary = { readonly completedAt?: number | null; readonly displayOrder: number; @@ -19533,6 +20727,7 @@ export type V2ItemCompletedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ItemCompletedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ItemCompletedNotification__McpToolCallError | null; @@ -19585,9 +20780,10 @@ export type V2ItemCompletedNotification__ThreadItem = } | { readonly id: string; - readonly path: V2ItemCompletedNotification__AbsolutePathBuf; + readonly path: V2ItemCompletedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -19652,10 +20848,7 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -19703,6 +20896,9 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ItemCompletedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -19717,7 +20913,14 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( Schema.Union([V2ItemCompletedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ItemCompletedNotification__McpToolCallResult, Schema.Null]), @@ -19818,9 +21021,16 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ItemCompletedNotification__AbsolutePathBuf, + path: V2ItemCompletedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -19921,6 +21131,7 @@ export type V2ItemStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ItemStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ItemStartedNotification__McpToolCallError | null; @@ -19971,9 +21182,10 @@ export type V2ItemStartedNotification__ThreadItem = } | { readonly id: string; - readonly path: V2ItemStartedNotification__AbsolutePathBuf; + readonly path: V2ItemStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -20038,10 +21250,7 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -20089,6 +21298,9 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ItemStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -20103,7 +21315,14 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2ItemStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ItemStartedNotification__McpToolCallResult, Schema.Null]), @@ -20204,9 +21423,16 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ItemStartedNotification__AbsolutePathBuf, + path: V2ItemStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -20502,6 +21728,7 @@ export type V2ReviewStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ReviewStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ReviewStartResponse__McpToolCallError | null; @@ -20552,9 +21779,10 @@ export type V2ReviewStartResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ReviewStartResponse__AbsolutePathBuf; + readonly path: V2ReviewStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -20617,10 +21845,7 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -20668,6 +21893,9 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ReviewStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -20682,7 +21910,14 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( Schema.Union([V2ReviewStartResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ReviewStartResponse__McpToolCallResult, Schema.Null]), @@ -20782,9 +22017,16 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ReviewStartResponse__AbsolutePathBuf, + path: V2ReviewStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -20909,6 +22151,7 @@ export type V2ThreadForkResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadForkResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadForkResponse__McpToolCallError | null; @@ -20959,9 +22202,10 @@ export type V2ThreadForkResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadForkResponse__AbsolutePathBuf; + readonly path: V2ThreadForkResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21024,10 +22268,7 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21075,6 +22316,9 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadForkResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21089,7 +22333,14 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadForkResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadForkResponse__McpToolCallResult, Schema.Null]), @@ -21189,9 +22440,16 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadForkResponse__AbsolutePathBuf, + path: V2ThreadForkResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -21285,6 +22543,7 @@ export type V2ThreadListResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadListResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadListResponse__McpToolCallError | null; @@ -21335,9 +22594,10 @@ export type V2ThreadListResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadListResponse__AbsolutePathBuf; + readonly path: V2ThreadListResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21400,10 +22660,7 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21451,6 +22708,9 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadListResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21465,7 +22725,14 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadListResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadListResponse__McpToolCallResult, Schema.Null]), @@ -21565,9 +22832,16 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadListResponse__AbsolutePathBuf, + path: V2ThreadListResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -21661,6 +22935,7 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadMetadataUpdateResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadMetadataUpdateResponse__McpToolCallError | null; @@ -21713,9 +22988,10 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadMetadataUpdateResponse__AbsolutePathBuf; + readonly path: V2ThreadMetadataUpdateResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21780,10 +23056,7 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21831,6 +23104,9 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21845,7 +23121,14 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallResult, Schema.Null]), @@ -21946,9 +23229,16 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadMetadataUpdateResponse__AbsolutePathBuf, + path: V2ThreadMetadataUpdateResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22042,6 +23332,7 @@ export type V2ThreadReadResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadReadResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadReadResponse__McpToolCallError | null; @@ -22092,9 +23383,10 @@ export type V2ThreadReadResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadReadResponse__AbsolutePathBuf; + readonly path: V2ThreadReadResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22157,10 +23449,7 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22208,6 +23497,9 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadReadResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22222,7 +23514,14 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadReadResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadReadResponse__McpToolCallResult, Schema.Null]), @@ -22322,9 +23621,16 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadReadResponse__AbsolutePathBuf, + path: V2ThreadReadResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22426,6 +23732,7 @@ export type V2ThreadResumeResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadResumeResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadResumeResponse__McpToolCallError | null; @@ -22476,9 +23783,10 @@ export type V2ThreadResumeResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadResumeResponse__AbsolutePathBuf; + readonly path: V2ThreadResumeResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22541,10 +23849,7 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22592,6 +23897,9 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadResumeResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22606,7 +23914,14 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadResumeResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadResumeResponse__McpToolCallResult, Schema.Null]), @@ -22706,9 +24021,16 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadResumeResponse__AbsolutePathBuf, + path: V2ThreadResumeResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22802,6 +24124,7 @@ export type V2ThreadRollbackResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadRollbackResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadRollbackResponse__McpToolCallError | null; @@ -22852,9 +24175,10 @@ export type V2ThreadRollbackResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadRollbackResponse__AbsolutePathBuf; + readonly path: V2ThreadRollbackResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22919,10 +24243,7 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22970,6 +24291,9 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadRollbackResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22984,7 +24308,14 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadRollbackResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadRollbackResponse__McpToolCallResult, Schema.Null]), @@ -23085,9 +24416,16 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadRollbackResponse__AbsolutePathBuf, + path: V2ThreadRollbackResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23190,6 +24528,7 @@ export type V2ThreadStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadStartedNotification__McpToolCallError | null; @@ -23242,9 +24581,10 @@ export type V2ThreadStartedNotification__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadStartedNotification__AbsolutePathBuf; + readonly path: V2ThreadStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -23309,10 +24649,7 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -23360,6 +24697,9 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -23374,7 +24714,14 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2ThreadStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadStartedNotification__McpToolCallResult, Schema.Null]), @@ -23475,9 +24822,16 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadStartedNotification__AbsolutePathBuf, + path: V2ThreadStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23571,6 +24925,7 @@ export type V2ThreadStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadStartResponse__McpToolCallError | null; @@ -23621,9 +24976,10 @@ export type V2ThreadStartResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadStartResponse__AbsolutePathBuf; + readonly path: V2ThreadStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -23686,10 +25042,7 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -23737,6 +25090,9 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -23751,7 +25107,14 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadStartResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadStartResponse__McpToolCallResult, Schema.Null]), @@ -23851,9 +25214,16 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadStartResponse__AbsolutePathBuf, + path: V2ThreadStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23947,6 +25317,7 @@ export type V2ThreadUnarchiveResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadUnarchiveResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadUnarchiveResponse__McpToolCallError | null; @@ -23997,9 +25368,10 @@ export type V2ThreadUnarchiveResponse__ThreadItem = } | { readonly id: string; - readonly path: V2ThreadUnarchiveResponse__AbsolutePathBuf; + readonly path: V2ThreadUnarchiveResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24064,10 +25436,7 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24115,6 +25484,9 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadUnarchiveResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24129,7 +25501,14 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadUnarchiveResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadUnarchiveResponse__McpToolCallResult, Schema.Null]), @@ -24230,9 +25609,16 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadUnarchiveResponse__AbsolutePathBuf, + path: V2ThreadUnarchiveResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -24326,6 +25712,7 @@ export type V2TurnCompletedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnCompletedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnCompletedNotification__McpToolCallError | null; @@ -24378,9 +25765,10 @@ export type V2TurnCompletedNotification__ThreadItem = } | { readonly id: string; - readonly path: V2TurnCompletedNotification__AbsolutePathBuf; + readonly path: V2TurnCompletedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24445,10 +25833,7 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24496,6 +25881,9 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnCompletedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24510,7 +25898,14 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( Schema.Union([V2TurnCompletedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnCompletedNotification__McpToolCallResult, Schema.Null]), @@ -24611,9 +26006,16 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnCompletedNotification__AbsolutePathBuf, + path: V2TurnCompletedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -24707,6 +26109,7 @@ export type V2TurnStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnStartedNotification__McpToolCallError | null; @@ -24757,9 +26160,10 @@ export type V2TurnStartedNotification__ThreadItem = } | { readonly id: string; - readonly path: V2TurnStartedNotification__AbsolutePathBuf; + readonly path: V2TurnStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24824,10 +26228,7 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24875,6 +26276,9 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24889,7 +26293,14 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2TurnStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnStartedNotification__McpToolCallResult, Schema.Null]), @@ -24990,9 +26401,16 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnStartedNotification__AbsolutePathBuf, + path: V2TurnStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -25086,6 +26504,7 @@ export type V2TurnStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnStartResponse__McpToolCallError | null; @@ -25136,9 +26555,10 @@ export type V2TurnStartResponse__ThreadItem = } | { readonly id: string; - readonly path: V2TurnStartResponse__AbsolutePathBuf; + readonly path: V2TurnStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -25201,10 +26621,7 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -25252,6 +26669,9 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -25264,7 +26684,14 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( ), error: Schema.optionalKey(Schema.Union([V2TurnStartResponse__McpToolCallError, Schema.Null])), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnStartResponse__McpToolCallResult, Schema.Null]), @@ -25362,9 +26789,16 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnStartResponse__AbsolutePathBuf, + path: V2TurnStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ title: "SleepThreadItem" }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -25401,16 +26835,25 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( export type ClientRequest__ExternalAgentConfigImportParams = { readonly migrationItems: ReadonlyArray; + readonly source?: string | null; }; export const ClientRequest__ExternalAgentConfigImportParams = Schema.Struct({ migrationItems: Schema.Array(ClientRequest__ExternalAgentConfigMigrationItem), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Source product that produced the migration items. Missing means unspecified.", + }), + Schema.Null, + ]), + ), }); export type CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct( { @@ -25430,7 +26873,7 @@ export const CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissi ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(CommandExecutionRequestApprovalParams__AbsolutePathBuf).annotate({ + Schema.Array(CommandExecutionRequestApprovalParams__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25438,7 +26881,7 @@ export const CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissi ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(CommandExecutionRequestApprovalParams__AbsolutePathBuf).annotate({ + Schema.Array(CommandExecutionRequestApprovalParams__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25458,8 +26901,8 @@ export const McpServerElicitationRequestParams__McpElicitationMultiSelectEnumSch export type PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct({ entries: Schema.optionalKey( @@ -25478,7 +26921,7 @@ export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(PermissionsRequestApprovalParams__AbsolutePathBuf).annotate({ + Schema.Array(PermissionsRequestApprovalParams__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25486,7 +26929,7 @@ export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(PermissionsRequestApprovalParams__AbsolutePathBuf).annotate({ + Schema.Array(PermissionsRequestApprovalParams__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25497,8 +26940,8 @@ export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = export type PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = Schema.Struct({ entries: Schema.optionalKey( @@ -25517,7 +26960,7 @@ export const PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(PermissionsRequestApprovalResponse__AbsolutePathBuf).annotate({ + Schema.Array(PermissionsRequestApprovalResponse__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25525,7 +26968,7 @@ export const PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(PermissionsRequestApprovalResponse__AbsolutePathBuf).annotate({ + Schema.Array(PermissionsRequestApprovalResponse__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25540,11 +26983,51 @@ export const ServerNotification__AppListUpdatedNotification = Schema.Struct({ data: Schema.Array(ServerNotification__AppInfo), }).annotate({ description: "EXPERIMENTAL - notification emitted when the app list changes." }); +export type ServerNotification__ExternalAgentConfigImportCompletedNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const ServerNotification__ExternalAgentConfigImportCompletedNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array(ServerNotification__ExternalAgentConfigImportTypeResult), +}); + +export type ServerNotification__ExternalAgentConfigImportProgressNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const ServerNotification__ExternalAgentConfigImportProgressNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array(ServerNotification__ExternalAgentConfigImportTypeResult), +}); + +export type ServerNotification__HookCompletedNotification = { + readonly run: ServerNotification__HookRunSummary; + readonly threadId: string; + readonly turnId?: string | null; +}; +export const ServerNotification__HookCompletedNotification = Schema.Struct({ + run: ServerNotification__HookRunSummary, + threadId: Schema.String, + turnId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + +export type ServerNotification__HookStartedNotification = { + readonly run: ServerNotification__HookRunSummary; + readonly threadId: string; + readonly turnId?: string | null; +}; +export const ServerNotification__HookStartedNotification = Schema.Struct({ + run: ServerNotification__HookRunSummary, + threadId: Schema.String, + turnId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type ServerNotification__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct({ entries: Schema.optionalKey( @@ -25560,7 +27043,7 @@ export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(ServerNotification__AbsolutePathBuf).annotate({ + Schema.Array(ServerNotification__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25568,7 +27051,7 @@ export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(ServerNotification__AbsolutePathBuf).annotate({ + Schema.Array(ServerNotification__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25576,28 +27059,6 @@ export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct ), }); -export type ServerNotification__HookCompletedNotification = { - readonly run: ServerNotification__HookRunSummary; - readonly threadId: string; - readonly turnId?: string | null; -}; -export const ServerNotification__HookCompletedNotification = Schema.Struct({ - run: ServerNotification__HookRunSummary, - threadId: Schema.String, - turnId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), -}); - -export type ServerNotification__HookStartedNotification = { - readonly run: ServerNotification__HookRunSummary; - readonly threadId: string; - readonly turnId?: string | null; -}; -export const ServerNotification__HookStartedNotification = Schema.Struct({ - run: ServerNotification__HookRunSummary, - threadId: Schema.String, - turnId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), -}); - export type ServerNotification__ErrorNotification = { readonly error: ServerNotification__TurnError; readonly threadId: string; @@ -25708,7 +27169,9 @@ export const ServerNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(ServerNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -25733,8 +27196,8 @@ export const ServerNotification__Turn = Schema.Struct({ export type ServerRequest__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const ServerRequest__AdditionalFileSystemPermissions = Schema.Struct({ entries: Schema.optionalKey( @@ -25750,7 +27213,7 @@ export const ServerRequest__AdditionalFileSystemPermissions = Schema.Struct({ ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(ServerRequest__AbsolutePathBuf).annotate({ + Schema.Array(ServerRequest__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25758,7 +27221,7 @@ export const ServerRequest__AdditionalFileSystemPermissions = Schema.Struct({ ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(ServerRequest__AbsolutePathBuf).annotate({ + Schema.Array(ServerRequest__LegacyAppPathString).annotate({ description: "This will be removed in favor of `entries`.", }), Schema.Null, @@ -25868,6 +27331,58 @@ export const V2ConfigReadResponse__Config = Schema.StructWithRest( [Schema.Record(Schema.String, Schema.Unknown)], ); +export type V2ConfigRequirementsReadResponse__ConfigRequirements = { + readonly allowAppshots?: boolean | null; + readonly allowManagedHooksOnly?: boolean | null; + readonly allowRemoteControl?: boolean | null; + readonly allowedApprovalPolicies?: ReadonlyArray | null; + readonly allowedPermissionProfiles?: { readonly [x: string]: boolean } | null; + readonly allowedSandboxModes?: ReadonlyArray | null; + readonly allowedWebSearchModes?: ReadonlyArray | null; + readonly allowedWindowsSandboxImplementations?: ReadonlyArray | null; + readonly computerUse?: V2ConfigRequirementsReadResponse__ComputerUseRequirements | null; + readonly defaultPermissions?: string | null; + readonly enforceResidency?: V2ConfigRequirementsReadResponse__ResidencyRequirement | null; + readonly featureRequirements?: { readonly [x: string]: boolean } | null; + readonly models?: V2ConfigRequirementsReadResponse__ModelsRequirements | null; +}; +export const V2ConfigRequirementsReadResponse__ConfigRequirements = Schema.Struct({ + allowAppshots: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowManagedHooksOnly: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowRemoteControl: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowedApprovalPolicies: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__AskForApproval), Schema.Null]), + ), + allowedPermissionProfiles: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), + ), + allowedSandboxModes: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__SandboxMode), Schema.Null]), + ), + allowedWebSearchModes: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__WebSearchMode), Schema.Null]), + ), + allowedWindowsSandboxImplementations: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2ConfigRequirementsReadResponse__WindowsSandboxSetupMode), + Schema.Null, + ]), + ), + computerUse: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ComputerUseRequirements, Schema.Null]), + ), + defaultPermissions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + enforceResidency: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ResidencyRequirement, Schema.Null]), + ), + featureRequirements: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), + ), + models: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ModelsRequirements, Schema.Null]), + ), +}); + export type V2ConfigWriteResponse__OverriddenMetadata = { readonly effectiveValue: unknown; readonly message: string; @@ -25882,8 +27397,8 @@ export const V2ConfigWriteResponse__OverriddenMetadata = Schema.Struct({ export type V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = Schema.Struct({ @@ -25903,17 +27418,17 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSy ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), + Schema.Array( + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + ).annotate({ description: "This will be removed in favor of `entries`." }), Schema.Null, ]), ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), + Schema.Array( + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + ).annotate({ description: "This will be removed in favor of `entries`." }), Schema.Null, ]), ), @@ -25922,8 +27437,8 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSy export type V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = { readonly entries?: ReadonlyArray | null; readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; export const V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = Schema.Struct({ @@ -25943,17 +27458,17 @@ export const V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSyst ), read: Schema.optionalKey( Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), + Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString).annotate( + { description: "This will be removed in favor of `entries`." }, + ), Schema.Null, ]), ), write: Schema.optionalKey( Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), + Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString).annotate( + { description: "This will be removed in favor of `entries`." }, + ), Schema.Null, ]), ), @@ -25973,6 +27488,7 @@ export type V2PluginInstalledResponse__PluginSummary = { readonly remotePluginId?: string | null; readonly shareContext?: V2PluginInstalledResponse__PluginShareContext | null; readonly source: V2PluginInstalledResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginInstalledResponse__PluginAuthPolicy, @@ -26011,6 +27527,14 @@ export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginInstalledResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginListResponse__PluginSummary = { @@ -26027,6 +27551,7 @@ export type V2PluginListResponse__PluginSummary = { readonly remotePluginId?: string | null; readonly shareContext?: V2PluginListResponse__PluginShareContext | null; readonly source: V2PluginListResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginListResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginListResponse__PluginAuthPolicy, @@ -26063,6 +27588,14 @@ export const V2PluginListResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginListResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginReadResponse__PluginSummary = { @@ -26079,6 +27612,7 @@ export type V2PluginReadResponse__PluginSummary = { readonly remotePluginId?: string | null; readonly shareContext?: V2PluginReadResponse__PluginShareContext | null; readonly source: V2PluginReadResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginReadResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginReadResponse__PluginAuthPolicy, @@ -26115,6 +27649,14 @@ export const V2PluginReadResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginReadResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginShareListResponse__PluginSummary = { @@ -26131,6 +27673,7 @@ export type V2PluginShareListResponse__PluginSummary = { readonly remotePluginId?: string | null; readonly shareContext?: V2PluginShareListResponse__PluginShareContext | null; readonly source: V2PluginShareListResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginShareListResponse__PluginAuthPolicy, @@ -26169,12 +27712,21 @@ export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginShareListResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly phase?: V2RawResponseItemCompletedNotification__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -26182,12 +27734,16 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -26195,6 +27751,7 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly action: V2RawResponseItemCompletedNotification__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status: V2RawResponseItemCompletedNotification__LocalShellStatus; readonly type: "local_shell_call"; } @@ -26202,6 +27759,7 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -26211,11 +27769,14 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -26223,12 +27784,16 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -26236,6 +27801,8 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -26243,26 +27810,42 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly action?: V2RawResponseItemCompletedNotification__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(V2RawResponseItemCompletedNotification__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), phase: Schema.optionalKey( Schema.Union([V2RawResponseItemCompletedNotification__MessagePhase, Schema.Null]), @@ -26273,6 +27856,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ author: Schema.String, content: Schema.Array(V2RawResponseItemCompletedNotification__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -26284,6 +27874,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union ]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), summary: Schema.Array(V2RawResponseItemCompletedNotification__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -26299,11 +27896,16 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), status: V2RawResponseItemCompletedNotification__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -26312,8 +27914,12 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -26323,8 +27929,12 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -26333,6 +27943,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -26340,11 +27957,16 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -26352,6 +27974,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -26361,6 +27990,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -26374,14 +28010,24 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Null, ]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -26391,6 +28037,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -26400,6 +28053,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -26445,7 +28105,9 @@ export const V2ReviewStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ReviewStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26512,7 +28174,9 @@ export const V2ThreadForkResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadForkResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26568,7 +28232,9 @@ export const V2ThreadListResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadListResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26624,7 +28290,9 @@ export const V2ThreadMetadataUpdateResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadMetadataUpdateResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26680,7 +28348,9 @@ export const V2ThreadReadResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadReadResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26736,7 +28406,9 @@ export const V2ThreadResumeResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadResumeResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26792,7 +28464,9 @@ export const V2ThreadRollbackResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadRollbackResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26885,7 +28559,9 @@ export const V2ThreadStartedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadStartedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26941,7 +28617,9 @@ export const V2ThreadStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26997,7 +28675,9 @@ export const V2ThreadUnarchiveResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadUnarchiveResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27053,7 +28733,9 @@ export const V2TurnCompletedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnCompletedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27109,7 +28791,9 @@ export const V2TurnStartedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnStartedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27165,7 +28849,9 @@ export const V2TurnStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27263,6 +28949,7 @@ export type ServerNotification__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27329,7 +29016,9 @@ export const ServerNotification__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27357,6 +29046,15 @@ export const ServerNotification__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27534,6 +29232,7 @@ export type V2PluginReadResponse__PluginDetail = { readonly marketplaceName: string; readonly marketplacePath?: V2PluginReadResponse__AbsolutePathBuf | null; readonly mcpServers: ReadonlyArray; + readonly shareUrl?: string | null; readonly skills: ReadonlyArray; readonly summary: V2PluginReadResponse__PluginSummary; }; @@ -27547,6 +29246,7 @@ export const V2PluginReadResponse__PluginDetail = Schema.Struct({ Schema.Union([V2PluginReadResponse__AbsolutePathBuf, Schema.Null]), ), mcpServers: Schema.Array(Schema.String), + shareUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), skills: Schema.Array(V2PluginReadResponse__SkillSummary), summary: V2PluginReadResponse__PluginSummary, }); @@ -27577,6 +29277,7 @@ export type V2ThreadForkResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27643,7 +29344,9 @@ export const V2ThreadForkResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27671,6 +29374,15 @@ export const V2ThreadForkResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27734,6 +29446,7 @@ export type V2ThreadListResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27800,7 +29513,9 @@ export const V2ThreadListResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27828,6 +29543,15 @@ export const V2ThreadListResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27891,6 +29615,7 @@ export type V2ThreadMetadataUpdateResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27957,7 +29682,9 @@ export const V2ThreadMetadataUpdateResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27985,6 +29712,15 @@ export const V2ThreadMetadataUpdateResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28048,6 +29784,7 @@ export type V2ThreadReadResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28114,7 +29851,9 @@ export const V2ThreadReadResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28142,6 +29881,15 @@ export const V2ThreadReadResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28205,6 +29953,7 @@ export type V2ThreadResumeResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28271,7 +30020,9 @@ export const V2ThreadResumeResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28299,6 +30050,15 @@ export const V2ThreadResumeResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28362,6 +30122,7 @@ export type V2ThreadStartedNotification__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28428,7 +30189,9 @@ export const V2ThreadStartedNotification__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28456,6 +30219,15 @@ export const V2ThreadStartedNotification__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28519,6 +30291,7 @@ export type V2ThreadStartResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28585,7 +30358,9 @@ export const V2ThreadStartResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28613,6 +30388,15 @@ export const V2ThreadStartResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28676,6 +30460,7 @@ export type V2ThreadUnarchiveResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28742,7 +30527,9 @@ export const V2ThreadUnarchiveResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28770,6 +30557,15 @@ export const V2ThreadUnarchiveResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -29284,6 +31080,15 @@ export type ServerRequest__McpServerElicitationRequestParams = readonly threadId: string; readonly turnId?: string | null; } + | { + readonly _meta?: unknown; + readonly message: string; + readonly mode: "openai/form"; + readonly requestedSchema: unknown; + readonly serverName: string; + readonly threadId: string; + readonly turnId?: string | null; + } | { readonly _meta?: unknown; readonly elicitationId: string; @@ -29313,6 +31118,23 @@ export const ServerRequest__McpServerElicitationRequestParams = Schema.Union( ]), ), }), + Schema.Struct({ + _meta: Schema.optionalKey(Schema.Unknown), + message: Schema.String, + mode: Schema.Literal("openai/form"), + requestedSchema: Schema.Unknown, + serverName: Schema.String, + threadId: Schema.String, + turnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + }), + Schema.Null, + ]), + ), + }), Schema.Struct({ _meta: Schema.optionalKey(Schema.Unknown), elicitationId: Schema.String, @@ -29769,11 +31591,21 @@ export type ClientRequest = readonly method: "account/rateLimits/read"; readonly params?: null; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "account/rateLimitResetCredit/consume"; + readonly params: ClientRequest__ConsumeAccountRateLimitResetCreditParams; + } | { readonly id: ClientRequest__RequestId; readonly method: "account/usage/read"; readonly params?: null; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "account/workspaceMessages/read"; + readonly params?: null; + } | { readonly id: ClientRequest__RequestId; readonly method: "account/sendAddCreditsNudgeEmail"; @@ -29819,6 +31651,11 @@ export type ClientRequest = readonly method: "externalAgentConfig/import"; readonly params: ClientRequest__ExternalAgentConfigImportParams; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "externalAgentConfig/import/readHistories"; + readonly params?: null; + } | { readonly id: ClientRequest__RequestId; readonly method: "config/value/write"; @@ -30269,6 +32106,13 @@ export const ClientRequest = Schema.Union( }), params: Schema.optionalKey(Schema.Null), }).annotate({ title: "Account/rateLimits/readRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("account/rateLimitResetCredit/consume").annotate({ + title: "Account/rateLimitResetCredit/consumeRequestMethod", + }), + params: ClientRequest__ConsumeAccountRateLimitResetCreditParams, + }).annotate({ title: "Account/rateLimitResetCredit/consumeRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("account/usage/read").annotate({ @@ -30276,6 +32120,13 @@ export const ClientRequest = Schema.Union( }), params: Schema.optionalKey(Schema.Null), }).annotate({ title: "Account/usage/readRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("account/workspaceMessages/read").annotate({ + title: "Account/workspaceMessages/readRequestMethod", + }), + params: Schema.optionalKey(Schema.Null), + }).annotate({ title: "Account/workspaceMessages/readRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("account/sendAddCreditsNudgeEmail").annotate({ @@ -30346,6 +32197,13 @@ export const ClientRequest = Schema.Union( }), params: ClientRequest__ExternalAgentConfigImportParams, }).annotate({ title: "ExternalAgentConfig/importRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("externalAgentConfig/import/readHistories").annotate({ + title: "ExternalAgentConfig/import/readHistoriesRequestMethod", + }), + params: Schema.optionalKey(Schema.Null), + }).annotate({ title: "ExternalAgentConfig/import/readHistoriesRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("config/value/write").annotate({ @@ -30409,7 +32267,9 @@ export const ClientRequest__CapabilityRootLocation = Schema.Union( [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -30430,19 +32290,59 @@ export const ClientRequest__CollaborationMode = Schema.Struct({ settings: ClientRequest__Settings, }).annotate({ description: "Collaboration mode for a Codex session." }); -export type ClientRequest__DynamicToolSpec = { - readonly deferLoading?: boolean; - readonly description: string; - readonly inputSchema: unknown; - readonly name: string; - readonly namespace?: string | null; -}; -export const ClientRequest__DynamicToolSpec = Schema.Struct({ - deferLoading: Schema.optionalKey(Schema.Boolean), - description: Schema.String, - inputSchema: Schema.Unknown, - name: Schema.String, - namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +export type ClientRequest__ConversationTextRole = "user" | "developer" | "assistant"; +export const ClientRequest__ConversationTextRole = Schema.Literals([ + "user", + "developer", + "assistant", +]); + +export type ClientRequest__DynamicToolSpec = + | { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; + } + | { + readonly description: string; + readonly name: string; + readonly tools: ReadonlyArray; + readonly type: "namespace"; + }; +export const ClientRequest__DynamicToolSpec = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolSpecType" }), + }).annotate({ title: "FunctionDynamicToolSpec" }), + Schema.Struct({ + description: Schema.String, + name: Schema.String, + tools: Schema.Array(ClientRequest__DynamicToolNamespaceTool), + type: Schema.Literal("namespace").annotate({ title: "NamespaceDynamicToolSpecType" }), + }).annotate({ title: "NamespaceDynamicToolSpec" }), + ], + { mode: "oneOf" }, +); + +export type ClientRequest__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const ClientRequest__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", }); export type ClientRequest__NetworkAccess = "restricted" | "enabled"; @@ -30512,10 +32412,21 @@ export const ClientRequest__RealtimeVoice = Schema.Literals([ "verse", ]); +export type ClientRequest__RemoteControlDisableParams = { readonly ephemeral?: boolean }; +export const ClientRequest__RemoteControlDisableParams = Schema.Struct({ + ephemeral: Schema.optionalKey(Schema.Boolean), +}); + +export type ClientRequest__RemoteControlEnableParams = { readonly ephemeral?: boolean }; +export const ClientRequest__RemoteControlEnableParams = Schema.Struct({ + ephemeral: Schema.optionalKey(Schema.Boolean), +}); + export type ClientRequest__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly phase?: ClientRequest__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -30523,12 +32434,16 @@ export type ClientRequest__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -30536,6 +32451,7 @@ export type ClientRequest__ResponseItem = readonly action: ClientRequest__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status: ClientRequest__LocalShellStatus; readonly type: "local_shell_call"; } @@ -30543,6 +32459,7 @@ export type ClientRequest__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -30552,11 +32469,14 @@ export type ClientRequest__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly output: ClientRequest__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -30564,12 +32484,16 @@ export type ClientRequest__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: ClientRequest__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -30577,6 +32501,8 @@ export type ClientRequest__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -30584,26 +32510,39 @@ export type ClientRequest__ResponseItem = | { readonly action?: ClientRequest__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const ClientRequest__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(ClientRequest__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), phase: Schema.optionalKey(Schema.Union([ClientRequest__MessagePhase, Schema.Null])), role: Schema.String, @@ -30612,6 +32551,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ author: Schema.String, content: Schema.Array(ClientRequest__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -30620,6 +32563,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Union([Schema.Array(ClientRequest__ReasoningItemContent), Schema.Null]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), summary: Schema.Array(ClientRequest__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -30635,11 +32582,13 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: ClientRequest__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -30648,8 +32597,9 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -30659,8 +32609,9 @@ export const ClientRequest__ResponseItem = Schema.Union( arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -30669,6 +32620,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), output: ClientRequest__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -30676,11 +32631,13 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -30688,6 +32645,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: ClientRequest__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -30697,6 +32658,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -30707,14 +32672,18 @@ export const ClientRequest__ResponseItem = Schema.Union( action: Schema.optionalKey( Schema.Union([ClientRequest__ResponsesApiWebSearchAction, Schema.Null]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -30724,6 +32693,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -30733,6 +32706,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -30760,7 +32737,9 @@ export const ClientRequest__SelectedCapabilityRoot = Schema.Struct({ [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -30775,6 +32754,9 @@ export const ClientRequest__SelectedCapabilityRoot = Schema.Struct({ description: "A user-selected root that can expose one or more runtime capabilities.", }); +export type ClientRequest__ThreadHistoryMode = "legacy" | "paginated"; +export const ClientRequest__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type ClientRequest__ThreadMemoryMode = "enabled" | "disabled"; export const ClientRequest__ThreadMemoryMode = Schema.Literals(["enabled", "disabled"]); @@ -30852,11 +32834,11 @@ export const ClientRequest__ThreadResumeInitialTurnsPageParams = Schema.Struct({ }); export type ClientRequest__TurnEnvironmentParams = { - readonly cwd: ClientRequest__AbsolutePathBuf; + readonly cwd: ClientRequest__LegacyAppPathString; readonly environmentId: string; }; export const ClientRequest__TurnEnvironmentParams = Schema.Struct({ - cwd: ClientRequest__AbsolutePathBuf, + cwd: ClientRequest__LegacyAppPathString, environmentId: Schema.String, }); @@ -30864,7 +32846,8 @@ export type CommandExecutionRequestApprovalParams = { readonly approvalId?: string | null; readonly command?: string | null; readonly commandActions?: ReadonlyArray | null; - readonly cwd?: CommandExecutionRequestApprovalParams__AbsolutePathBuf | null; + readonly cwd?: CommandExecutionRequestApprovalParams__LegacyAppPathString | null; + readonly environmentId?: string | null; readonly itemId: string; readonly networkApprovalContext?: CommandExecutionRequestApprovalParams__NetworkApprovalContext | null; readonly proposedExecpolicyAmendment?: ReadonlyArray | null; @@ -30899,9 +32882,16 @@ export const CommandExecutionRequestApprovalParams = Schema.Struct({ ]), ), cwd: Schema.optionalKey( - Schema.Union([CommandExecutionRequestApprovalParams__AbsolutePathBuf, Schema.Null]).annotate({ - description: "The command's working directory.", - }), + Schema.Union([ + CommandExecutionRequestApprovalParams__LegacyAppPathString, + Schema.Null, + ]).annotate({ description: "The command's working directory." }), + ), + environmentId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Environment in which the command will run." }), + Schema.Null, + ]), ), itemId: Schema.String, networkApprovalContext: Schema.optionalKey( @@ -31283,6 +33273,15 @@ export type McpServerElicitationRequestParams = readonly threadId: string; readonly turnId?: string | null; } + | { + readonly _meta?: unknown; + readonly message: string; + readonly mode: "openai/form"; + readonly requestedSchema: unknown; + readonly serverName: string; + readonly threadId: string; + readonly turnId?: string | null; + } | { readonly _meta?: unknown; readonly elicitationId: string; @@ -31312,6 +33311,23 @@ export const McpServerElicitationRequestParams = Schema.Union( ]), ), }).annotate({ title: "McpServerElicitationRequestParams" }), + Schema.Struct({ + _meta: Schema.optionalKey(Schema.Unknown), + message: Schema.String, + mode: Schema.Literal("openai/form"), + requestedSchema: Schema.Unknown, + serverName: Schema.String, + threadId: Schema.String, + turnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + }), + Schema.Null, + ]), + ), + }).annotate({ title: "McpServerElicitationRequestParams" }), Schema.Struct({ _meta: Schema.optionalKey(Schema.Unknown), elicitationId: Schema.String, @@ -31567,6 +33583,10 @@ export type ServerNotification = readonly method: "remoteControl/status/changed"; readonly params: ServerNotification__RemoteControlStatusChangedNotification; } + | { + readonly method: "externalAgentConfig/import/progress"; + readonly params: ServerNotification__ExternalAgentConfigImportProgressNotification; + } | { readonly method: "externalAgentConfig/import/completed"; readonly params: ServerNotification__ExternalAgentConfigImportCompletedNotification; @@ -31600,6 +33620,10 @@ export type ServerNotification = readonly method: "turn/moderationMetadata"; readonly params: ServerNotification__TurnModerationMetadataNotification; } + | { + readonly method: "model/safetyBuffering/updated"; + readonly params: ServerNotification__ModelSafetyBufferingUpdatedNotification; + } | { readonly method: "warning"; readonly params: ServerNotification__WarningNotification } | { readonly method: "guardianWarning"; @@ -31916,6 +33940,12 @@ export const ServerNotification = Schema.Union( }), params: ServerNotification__RemoteControlStatusChangedNotification, }).annotate({ title: "RemoteControl/status/changedNotification" }), + Schema.Struct({ + method: Schema.Literal("externalAgentConfig/import/progress").annotate({ + title: "ExternalAgentConfig/import/progressNotificationMethod", + }), + params: ServerNotification__ExternalAgentConfigImportProgressNotification, + }).annotate({ title: "ExternalAgentConfig/import/progressNotification" }), Schema.Struct({ method: Schema.Literal("externalAgentConfig/import/completed").annotate({ title: "ExternalAgentConfig/import/completedNotificationMethod", @@ -31971,6 +34001,12 @@ export const ServerNotification = Schema.Union( }), params: ServerNotification__TurnModerationMetadataNotification, }).annotate({ title: "Turn/moderationMetadataNotification" }), + Schema.Struct({ + method: Schema.Literal("model/safetyBuffering/updated").annotate({ + title: "Model/safetyBuffering/updatedNotificationMethod", + }), + params: ServerNotification__ModelSafetyBufferingUpdatedNotification, + }).annotate({ title: "Model/safetyBuffering/updatedNotification" }), Schema.Struct({ method: Schema.Literal("warning").annotate({ title: "WarningNotificationMethod" }), params: ServerNotification__WarningNotification, @@ -32157,6 +34193,21 @@ export const ServerNotification__HookSource = Schema.Literals([ "unknown", ]); +export type ServerNotification__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const ServerNotification__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type ServerNotification__NetworkAccess = "restricted" | "enabled"; export const ServerNotification__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -32185,6 +34236,14 @@ export const ServerNotification__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type ServerNotification__ThreadExtra = {}; +export const ServerNotification__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type ServerNotification__ThreadHistoryMode = "legacy" | "paginated"; +export const ServerNotification__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type ServerNotification__TurnItemsView = "notLoaded" | "summary" | "full"; export const ServerNotification__TurnItemsView = Schema.Literals(["notLoaded", "summary", "full"]); @@ -32412,12 +34471,21 @@ export const ServerRequest__CommandExecutionApprovalDecision = Schema.Union( ); export type ToolRequestUserInputParams = { + readonly autoResolutionMs?: number | null; readonly itemId: string; readonly questions: ReadonlyArray; readonly threadId: string; readonly turnId: string; }; export const ToolRequestUserInputParams = Schema.Struct({ + autoResolutionMs: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null, + ]), + ), itemId: Schema.String, questions: Schema.Array(ToolRequestUserInputParams__ToolRequestUserInputQuestion), threadId: Schema.String, @@ -33206,6 +35274,33 @@ export const V2ConfigWriteResponse = Schema.Struct({ version: Schema.String, }).annotate({ title: "ConfigWriteResponse" }); +export type V2ConsumeAccountRateLimitResetCreditParams = { + readonly creditId?: string | null; + readonly idempotencyKey: string; +}; +export const V2ConsumeAccountRateLimitResetCreditParams = Schema.Struct({ + creditId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + }), + Schema.Null, + ]), + ), + idempotencyKey: Schema.String.annotate({ + description: + "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + }), +}).annotate({ title: "ConsumeAccountRateLimitResetCreditParams" }); + +export type V2ConsumeAccountRateLimitResetCreditResponse = { + readonly outcome: V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome; +}; +export const V2ConsumeAccountRateLimitResetCreditResponse = Schema.Struct({ + outcome: V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome, +}).annotate({ title: "ConsumeAccountRateLimitResetCreditResponse" }); + export type V2ContextCompactedNotification = { readonly threadId: string; readonly turnId: string }; export const V2ContextCompactedNotification = Schema.Struct({ threadId: Schema.String, @@ -33345,7 +35440,7 @@ export const V2ExternalAgentConfigDetectParams = Schema.Struct({ ), includeHome: Schema.optionalKey( Schema.Boolean.annotate({ - description: "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + description: "If true, include detection under the user's home directory.", }), ), }).annotate({ title: "ExternalAgentConfigDetectParams" }); @@ -33357,22 +35452,57 @@ export const V2ExternalAgentConfigDetectResponse = Schema.Struct({ items: Schema.Array(V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationItem), }).annotate({ title: "ExternalAgentConfigDetectResponse" }); -export type V2ExternalAgentConfigImportCompletedNotification = {}; -export const V2ExternalAgentConfigImportCompletedNotification = Schema.Struct({}).annotate({ - title: "ExternalAgentConfigImportCompletedNotification", -}); +export type V2ExternalAgentConfigImportCompletedNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportCompletedNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult, + ), +}).annotate({ title: "ExternalAgentConfigImportCompletedNotification" }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse = { + readonly data: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportHistoriesReadResponse = Schema.Struct({ + data: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory, + ), +}).annotate({ title: "ExternalAgentConfigImportHistoriesReadResponse" }); export type V2ExternalAgentConfigImportParams = { readonly migrationItems: ReadonlyArray; + readonly source?: string | null; }; export const V2ExternalAgentConfigImportParams = Schema.Struct({ migrationItems: Schema.Array(V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Source product that produced the migration items. Missing means unspecified.", + }), + Schema.Null, + ]), + ), }).annotate({ title: "ExternalAgentConfigImportParams" }); -export type V2ExternalAgentConfigImportResponse = {}; -export const V2ExternalAgentConfigImportResponse = Schema.Struct({}).annotate({ - title: "ExternalAgentConfigImportResponse", -}); +export type V2ExternalAgentConfigImportProgressNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportProgressNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult, + ), +}).annotate({ title: "ExternalAgentConfigImportProgressNotification" }); + +export type V2ExternalAgentConfigImportResponse = { readonly importId: string }; +export const V2ExternalAgentConfigImportResponse = Schema.Struct({ + importId: Schema.String, +}).annotate({ title: "ExternalAgentConfigImportResponse" }); export type V2FeedbackUploadParams = { readonly classification: string; @@ -33735,6 +35865,7 @@ export const V2GetAccountParams = Schema.Struct({ }).annotate({ title: "GetAccountParams" }); export type V2GetAccountRateLimitsResponse = { + readonly rateLimitResetCredits?: V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary | null; readonly rateLimits: { readonly credits?: V2GetAccountRateLimitsResponse__CreditsSnapshot | null; readonly individualLimit?: V2GetAccountRateLimitsResponse__SpendControlLimitSnapshot | null; @@ -33750,6 +35881,9 @@ export type V2GetAccountRateLimitsResponse = { } | null; }; export const V2GetAccountRateLimitsResponse = Schema.Struct({ + rateLimitResetCredits: Schema.optionalKey( + Schema.Union([V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary, Schema.Null]), + ), rateLimits: Schema.Struct({ credits: Schema.optionalKey( Schema.Union([V2GetAccountRateLimitsResponse__CreditsSnapshot, Schema.Null]), @@ -33793,6 +35927,12 @@ export const V2GetAccountResponse = Schema.Struct({ requiresOpenaiAuth: Schema.Boolean, }).annotate({ title: "GetAccountResponse" }); +export type V2GetAccountResponse__AmazonBedrockCredentialSource = "codexManaged" | "awsManaged"; +export const V2GetAccountResponse__AmazonBedrockCredentialSource = Schema.Literals([ + "codexManaged", + "awsManaged", +]); + export type V2GetAccountTokenUsageResponse = { readonly dailyUsageBuckets?: ReadonlyArray | null; readonly summary: V2GetAccountTokenUsageResponse__AccountTokenUsageSummary; @@ -33807,6 +35947,19 @@ export const V2GetAccountTokenUsageResponse = Schema.Struct({ summary: V2GetAccountTokenUsageResponse__AccountTokenUsageSummary, }).annotate({ title: "GetAccountTokenUsageResponse" }); +export type V2GetWorkspaceMessagesResponse = { + readonly featureEnabled: boolean; + readonly messages: ReadonlyArray; +}; +export const V2GetWorkspaceMessagesResponse = Schema.Struct({ + featureEnabled: Schema.Boolean.annotate({ + description: "Whether the workspace-message backend route is available for this client.", + }), + messages: Schema.Array(V2GetWorkspaceMessagesResponse__WorkspaceMessage).annotate({ + description: "Active workspace messages returned by the backend.", + }), +}).annotate({ title: "GetWorkspaceMessagesResponse" }); + export type V2GuardianWarningNotification = { readonly message: string; readonly threadId: string }; export const V2GuardianWarningNotification = Schema.Struct({ message: Schema.String.annotate({ @@ -34338,21 +36491,25 @@ export type V2McpServerOauthLoginCompletedNotification = { readonly error?: string | null; readonly name: string; readonly success: boolean; + readonly threadId?: string | null; }; export const V2McpServerOauthLoginCompletedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, success: Schema.Boolean, + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }).annotate({ title: "McpServerOauthLoginCompletedNotification" }); export type V2McpServerOauthLoginParams = { readonly name: string; readonly scopes?: ReadonlyArray | null; + readonly threadId?: string | null; readonly timeoutSecs?: number | null; }; export const V2McpServerOauthLoginParams = Schema.Struct({ name: Schema.String, scopes: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), timeoutSecs: Schema.optionalKey( Schema.Union([Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), Schema.Null]), ), @@ -34370,12 +36527,19 @@ export const V2McpServerRefreshResponse = Schema.Struct({}).annotate({ export type V2McpServerStatusUpdatedNotification = { readonly error?: string | null; + readonly failureReason?: V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason | null; readonly name: string; readonly status: V2McpServerStatusUpdatedNotification__McpServerStartupState; readonly threadId?: string | null; }; export const V2McpServerStatusUpdatedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureReason: Schema.optionalKey( + Schema.Union([ + V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason, + Schema.Null, + ]), + ), name: Schema.String, status: V2McpServerStatusUpdatedNotification__McpServerStartupState, threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -34505,6 +36669,25 @@ export const V2ModelReroutedNotification = Schema.Struct({ turnId: Schema.String, }).annotate({ title: "ModelReroutedNotification" }); +export type V2ModelSafetyBufferingUpdatedNotification = { + readonly fasterModel?: string | null; + readonly model: string; + readonly reasons: ReadonlyArray; + readonly showBufferingUi: boolean; + readonly threadId: string; + readonly turnId: string; + readonly useCases: ReadonlyArray; +}; +export const V2ModelSafetyBufferingUpdatedNotification = Schema.Struct({ + fasterModel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + model: Schema.String, + reasons: Schema.Array(Schema.String), + showBufferingUi: Schema.Boolean, + threadId: Schema.String, + turnId: Schema.String, + useCases: Schema.Array(Schema.String), +}).annotate({ title: "ModelSafetyBufferingUpdatedNotification" }); + export type V2ModelVerificationNotification = { readonly threadId: string; readonly turnId: string; @@ -35226,6 +37409,7 @@ export type V2ThreadForkParams = { readonly cwd?: string | null; readonly developerInstructions?: string | null; readonly ephemeral?: boolean; + readonly lastTurnId?: string | null; readonly model?: string | null; readonly modelProvider?: string | null; readonly sandbox?: V2ThreadForkParams__SandboxMode | null; @@ -35250,6 +37434,15 @@ export const V2ThreadForkParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), developerInstructions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), ephemeral: Schema.optionalKey(Schema.Boolean), + lastTurnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.", + }), + Schema.Null, + ]), + ), model: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -35284,7 +37477,7 @@ export type V2ThreadForkResponse = { readonly approvalPolicy: V2ThreadForkResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadForkResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadForkResponse__ReasoningEffort | null; @@ -35310,8 +37503,9 @@ export const V2ThreadForkResponse = Schema.Struct({ }), cwd: V2ThreadForkResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadForkResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadForkResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -35433,6 +37627,21 @@ export const V2ThreadForkResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadForkResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadForkResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadForkResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadForkResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -35498,6 +37707,14 @@ export const V2ThreadForkResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadForkResponse__ThreadExtra = {}; +export const V2ThreadForkResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadForkResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadForkResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadForkResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -35786,6 +38003,14 @@ export const V2ThreadListResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadListResponse__ThreadExtra = {}; +export const V2ThreadListResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadListResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadListResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadListResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -35957,6 +38182,17 @@ export const V2ThreadMetadataUpdateResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadMetadataUpdateResponse__ThreadExtra = {}; +export const V2ThreadMetadataUpdateResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadMetadataUpdateResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadMetadataUpdateResponse__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadMetadataUpdateResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36077,6 +38313,14 @@ export const V2ThreadReadResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadReadResponse__ThreadExtra = {}; +export const V2ThreadReadResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadReadResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadReadResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadReadResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36271,6 +38515,7 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly phase?: V2ThreadResumeParams__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -36278,12 +38523,16 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -36291,6 +38540,7 @@ export type V2ThreadResumeParams__ResponseItem = readonly action: V2ThreadResumeParams__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status: V2ThreadResumeParams__LocalShellStatus; readonly type: "local_shell_call"; } @@ -36298,6 +38548,7 @@ export type V2ThreadResumeParams__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -36307,11 +38558,14 @@ export type V2ThreadResumeParams__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly output: V2ThreadResumeParams__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -36319,12 +38573,16 @@ export type V2ThreadResumeParams__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: V2ThreadResumeParams__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -36332,6 +38590,8 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -36339,26 +38599,39 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly action?: V2ThreadResumeParams__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const V2ThreadResumeParams__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(V2ThreadResumeParams__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), phase: Schema.optionalKey(Schema.Union([V2ThreadResumeParams__MessagePhase, Schema.Null])), role: Schema.String, @@ -36367,6 +38640,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ author: Schema.String, content: Schema.Array(V2ThreadResumeParams__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -36375,6 +38652,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Union([Schema.Array(V2ThreadResumeParams__ReasoningItemContent), Schema.Null]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), summary: Schema.Array(V2ThreadResumeParams__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -36390,11 +38671,13 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: V2ThreadResumeParams__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -36403,8 +38686,9 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -36414,8 +38698,9 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -36424,6 +38709,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), output: V2ThreadResumeParams__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -36431,11 +38720,13 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -36443,6 +38734,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: V2ThreadResumeParams__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -36452,6 +38747,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -36462,14 +38761,18 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( action: Schema.optionalKey( Schema.Union([V2ThreadResumeParams__ResponsesApiWebSearchAction, Schema.Null]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -36479,6 +38782,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -36488,6 +38795,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -36529,7 +38840,7 @@ export type V2ThreadResumeResponse = { readonly approvalPolicy: V2ThreadResumeResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadResumeResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadResumeResponse__ReasoningEffort | null; @@ -36555,8 +38866,9 @@ export const V2ThreadResumeResponse = Schema.Struct({ }), cwd: V2ThreadResumeResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadResumeResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadResumeResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -36684,6 +38996,21 @@ export const V2ThreadResumeResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadResumeResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadResumeResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadResumeResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadResumeResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -36749,6 +39076,14 @@ export const V2ThreadResumeResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadResumeResponse__ThreadExtra = {}; +export const V2ThreadResumeResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadResumeResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadResumeResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadResumeResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36804,7 +39139,10 @@ export const V2ThreadRollbackParams = Schema.Struct({ .check(Schema.isInt()) .check(Schema.isGreaterThanOrEqualTo(0)), threadId: Schema.String, -}).annotate({ title: "ThreadRollbackParams" }); +}).annotate({ + title: "ThreadRollbackParams", + description: "DEPRECATED: `thread/rollback` will be removed soon.", +}); export type V2ThreadRollbackResponse = { readonly thread: { @@ -36822,6 +39160,7 @@ export type V2ThreadRollbackResponse = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -36890,7 +39229,9 @@ export const V2ThreadRollbackResponse = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -36918,6 +39259,15 @@ export const V2ThreadRollbackResponse = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -37050,6 +39400,7 @@ export type V2ThreadRollbackResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -37116,7 +39467,9 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -37144,6 +39497,15 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -37192,6 +39554,14 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ }).check(Schema.isInt()), }); +export type V2ThreadRollbackResponse__ThreadExtra = {}; +export const V2ThreadRollbackResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadRollbackResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadRollbackResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadRollbackResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37246,6 +39616,21 @@ export const V2ThreadSettingsUpdatedNotification = Schema.Struct({ threadSettings: V2ThreadSettingsUpdatedNotification__ThreadSettings, }).annotate({ title: "ThreadSettingsUpdatedNotification" }); +export type V2ThreadSettingsUpdatedNotification__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadSettingsUpdatedNotification__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadSettingsUpdatedNotification__NetworkAccess = "restricted" | "enabled"; export const V2ThreadSettingsUpdatedNotification__NetworkAccess = Schema.Literals([ "restricted", @@ -37339,6 +39724,17 @@ export const V2ThreadStartedNotification__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartedNotification__ThreadExtra = {}; +export const V2ThreadStartedNotification__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadStartedNotification__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartedNotification__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadStartedNotification__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37423,6 +39819,12 @@ export const V2ThreadStartParams = Schema.Struct({ ), }).annotate({ title: "ThreadStartParams" }); +export type V2ThreadStartParams__AbsolutePathBuf = string; +export const V2ThreadStartParams__AbsolutePathBuf = Schema.String.annotate({ + description: + "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", +}); + export type V2ThreadStartParams__CapabilityRootLocation = { readonly environmentId: string; readonly path: string; @@ -37432,7 +39834,9 @@ export const V2ThreadStartParams__CapabilityRootLocation = Schema.Union( [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -37444,19 +39848,52 @@ export const V2ThreadStartParams__CapabilityRootLocation = Schema.Union( { mode: "oneOf" }, ).annotate({ description: "Location used to resolve a selected capability root." }); -export type V2ThreadStartParams__DynamicToolSpec = { - readonly deferLoading?: boolean; - readonly description: string; - readonly inputSchema: unknown; - readonly name: string; - readonly namespace?: string | null; -}; -export const V2ThreadStartParams__DynamicToolSpec = Schema.Struct({ - deferLoading: Schema.optionalKey(Schema.Boolean), - description: Schema.String, - inputSchema: Schema.Unknown, - name: Schema.String, - namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +export type V2ThreadStartParams__DynamicToolSpec = + | { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; + } + | { + readonly description: string; + readonly name: string; + readonly tools: ReadonlyArray; + readonly type: "namespace"; + }; +export const V2ThreadStartParams__DynamicToolSpec = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolSpecType" }), + }).annotate({ title: "FunctionDynamicToolSpec" }), + Schema.Struct({ + description: Schema.String, + name: Schema.String, + tools: Schema.Array(V2ThreadStartParams__DynamicToolNamespaceTool), + type: Schema.Literal("namespace").annotate({ title: "NamespaceDynamicToolSpecType" }), + }).annotate({ title: "NamespaceDynamicToolSpec" }), + ], + { mode: "oneOf" }, +); + +export type V2ThreadStartParams__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadStartParams__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", }); export type V2ThreadStartParams__SelectedCapabilityRoot = { @@ -37475,7 +39912,9 @@ export const V2ThreadStartParams__SelectedCapabilityRoot = Schema.Struct({ [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -37490,12 +39929,15 @@ export const V2ThreadStartParams__SelectedCapabilityRoot = Schema.Struct({ description: "A user-selected root that can expose one or more runtime capabilities.", }); +export type V2ThreadStartParams__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartParams__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadStartParams__TurnEnvironmentParams = { - readonly cwd: V2ThreadStartParams__AbsolutePathBuf; + readonly cwd: V2ThreadStartParams__LegacyAppPathString; readonly environmentId: string; }; export const V2ThreadStartParams__TurnEnvironmentParams = Schema.Struct({ - cwd: V2ThreadStartParams__AbsolutePathBuf, + cwd: V2ThreadStartParams__LegacyAppPathString, environmentId: Schema.String, }); @@ -37503,7 +39945,7 @@ export type V2ThreadStartResponse = { readonly approvalPolicy: V2ThreadStartResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadStartResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadStartResponse__ReasoningEffort | null; @@ -37529,8 +39971,9 @@ export const V2ThreadStartResponse = Schema.Struct({ }), cwd: V2ThreadStartResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadStartResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadStartResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -37655,6 +40098,21 @@ export const V2ThreadStartResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadStartResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadStartResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadStartResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadStartResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -37720,6 +40178,14 @@ export const V2ThreadStartResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartResponse__ThreadExtra = {}; +export const V2ThreadStartResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadStartResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadStartResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37854,6 +40320,17 @@ export const V2ThreadUnarchiveResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadUnarchiveResponse__ThreadExtra = {}; +export const V2ThreadUnarchiveResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadUnarchiveResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadUnarchiveResponse__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadUnarchiveResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -38187,15 +40664,30 @@ export const V2TurnStartParams__CollaborationMode = Schema.Struct({ settings: V2TurnStartParams__Settings, }).annotate({ description: "Collaboration mode for a Codex session." }); +export type V2TurnStartParams__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2TurnStartParams__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2TurnStartParams__NetworkAccess = "restricted" | "enabled"; export const V2TurnStartParams__NetworkAccess = Schema.Literals(["restricted", "enabled"]); export type V2TurnStartParams__TurnEnvironmentParams = { - readonly cwd: V2TurnStartParams__AbsolutePathBuf; + readonly cwd: V2TurnStartParams__LegacyAppPathString; readonly environmentId: string; }; export const V2TurnStartParams__TurnEnvironmentParams = Schema.Struct({ - cwd: V2TurnStartParams__AbsolutePathBuf, + cwd: V2TurnStartParams__LegacyAppPathString, environmentId: Schema.String, });