From 43cf4f4aed5bcceb48968acb9569b3d001b425fd Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:59:04 +0200 Subject: [PATCH 1/2] fix(ai): distinguish partial Codex quota status --- hancore.shibumi.ai/AiUsagePanel.qml | 8 ++- hancore.shibumi.ai/Service.qml | 12 +++- tests/ai-plugin-smoke.qml | 92 +++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index 0525919..64d3c8b 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -18,6 +18,10 @@ ShibumiPanel { readonly property int renderedModelCount: modelRepeater.count readonly property bool providerReady: provider && provider.ready !== undefined ? provider.ready === true : true + readonly property string providerStatusLabel: provider && aiService + && typeof aiService.providerStatusText === "function" + ? aiService.providerStatusText(provider) + : providerReady ? "live" : "stale" readonly property bool providerHasUsage: provider && (Number(provider.rateLimitPercent) >= 0 || Number(provider.secondaryRateLimitPercent) >= 0) @@ -223,8 +227,8 @@ ShibumiPanel { Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: panel.providerReady ? "live" : "stale" - color: panel.providerReady ? panel.controlMuted + text: panel.providerStatusLabel + color: panel.providerStatusLabel === "live" ? panel.controlMuted : panel.controlAccent font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.caption diff --git a/hancore.shibumi.ai/Service.qml b/hancore.shibumi.ai/Service.qml index e8efabf..e22a390 100644 --- a/hancore.shibumi.ai/Service.qml +++ b/hancore.shibumi.ai/Service.qml @@ -382,6 +382,15 @@ Item { return authHelp || "No current usage or limit data." } + function providerStatusText(provider) { + if (!provider || provider.ready === false) return "stale" + if (String(provider.providerId || "") === "codex" + && Number(provider.rateLimitPercent) < 0 + && Number(provider.secondaryRateLimitPercent) < 0) + return "partial" + return "live" + } + function displayPercent(provider, value) { const number = Number(value) if (!isFinite(number) || number < 0) return -1 @@ -431,7 +440,8 @@ Item { let heading = String(provider.providerName || provider.providerId || "AI") if (displayTierLabel(provider.tierLabel)) heading += " (" + displayTierLabel(provider.tierLabel) + ")" - if (provider.ready === false) heading += " · stale" + const status = providerStatusText(provider) + if (status !== "live") heading += " · " + status lines.push(heading) if (Number(provider.rateLimitPercent) >= 0) { const reset = resetText(provider, provider.rateLimitResetAt) diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 98288c1..c5841b2 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -14,6 +14,10 @@ ShellRoot { property bool waitingBackendTransition: false property bool waitingExpiryProbe: false property var expiryProbeRecord: null + property int codexStatusProbeStage: 0 + property var codexStatusOriginalRecord: null + property string codexStatusOriginalTool: "" + property bool codexStatusProbePassed: false property bool midnightRacePassed: false property int midnightRaceStage: 0 property int midnightRaceTicks: 0 @@ -81,6 +85,7 @@ ShellRoot { && codex.todayTotalTokens === 1031649 && codex.latestModel === "" && claude.ready && codex.ready + && agentsService.providerStatusText(codex) === "live" && !agentsService.providerHasCurrentData(claude) && agentsService.providerCurrentDataMessage(claude) === "Run `claude auth login` to restore authoritative usage." @@ -92,6 +97,86 @@ ShellRoot { && typeof codex.refresh !== "function" } + function codexStatusRecord(ready, withLimits) { + return JSON.stringify({ + schemaVersion: 1, + id: "codex", + name: "Codex", + ready: ready, + updatedAt: new Date().toISOString(), + limits: withLimits + ? [{ label: "Weekly (7-day)", percent: 0.24, resetsAt: "" }] : [], + tierLabel: "", + usageStatusText: withLimits ? "" : "Codex limits unavailable", + authHelpText: withLimits ? "" : "initialize", + todayPrompts: 14, + todaySessions: 2, + todayTotalTokens: 1031649, + totalPrompts: 69089, + totalSessions: 136, + activeDays: 31, + modelUsage: { "gpt-test": { inputTokens: 700000 } } + }) + } + + function advanceCodexStatusProbe() { + if (codexStatusProbeStage === 0) { + codexStatusOriginalRecord = agentsService.agentsCodexRecord + codexStatusOriginalTool = fakeState.selectedTool + fakeState.selectedTool = "codex" + fakeState.revision++ + agentsState.revision++ + agentsService.applyAgentRecord("codex", + codexStatusRecord(true, false)) + codexStatusProbeStage = 1 + return false + } + + const provider = agentsService.providerFor("codex") + if (!provider) { + fail("Codex status probe lost its schema-v1 record") + return false + } + if (codexStatusProbeStage === 1) { + if (agentsService.providerStatusText(provider) !== "partial" + || emptyAgentsPanel.providerStatusLabel !== "partial" + || agentsService.tooltipText().indexOf("Codex · partial") < 0) { + fail("Codex partial status did not propagate") + return false + } + agentsService.applyAgentRecord("codex", + codexStatusRecord(true, true)) + codexStatusProbeStage = 2 + return false + } + if (codexStatusProbeStage === 2) { + if (agentsService.providerStatusText(provider) !== "live" + || emptyAgentsPanel.providerStatusLabel !== "live" + || agentsService.tooltipText().indexOf("Codex · partial") >= 0) { + fail("Codex live status did not replace partial") + return false + } + agentsService.applyAgentRecord("codex", + codexStatusRecord(false, true)) + codexStatusProbeStage = 3 + return false + } + if (agentsService.providerStatusText(provider) !== "stale" + || emptyAgentsPanel.providerStatusLabel !== "stale" + || agentsService.tooltipText().indexOf("Codex · stale") < 0) { + fail("Codex stale status did not take precedence") + return false + } + + agentsService.agentsCodexRecord = codexStatusOriginalRecord + agentsService.providerRevision++ + fakeState.selectedTool = codexStatusOriginalTool + fakeState.revision++ + agentsState.revision++ + codexStatusProbePassed = true + return true + } + function readyRecordWithoutCurrentDataAccepted() { const recordNow = Date.now() const currentUpdatedAt = new Date(recordNow).toISOString() @@ -899,6 +984,11 @@ ShellRoot { if (first.panelLoaded || secondLoader.item !== null || root.clickTargets.length !== 1) return root.fail("panel/widget teardown") + if (!root.codexStatusProbePassed) { + root.advanceCodexStatusProbe() + root.ticks = 0 + return + } if (!root.waitingExpiryProbe) { if (!root.readyRecordWithoutCurrentDataAccepted()) return root.fail("ready empty/stale agents record contract") @@ -925,6 +1015,8 @@ ShellRoot { agentsService.providerRevision++ if (agentsService.providerFor("claude").ready || agentsService.providerFor("codex").ready + || agentsService.providerStatusText( + agentsService.providerFor("codex")) !== "stale" || agentsService.usagePercent( agentsService.providerFor("claude")) !== -1 || agentsService.usagePercent( From 45b7b4b2b9f934625450f8e528fac3c2b9e0eda5 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:54:00 +0200 Subject: [PATCH 2/2] fix(ai): restore Codex quota percentages --- contracts/backend-boundary-v1.json | 2 + hancore.shibumi.ai/scripts/agents-update | 36 +++++++++++++++- hancore.shibumi.ai/scripts/compat-bin/codex | 45 +++++++++++++++++++ tests/ai-plugin-regression.sh | 48 ++++++++++++++++++++- 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100755 hancore.shibumi.ai/scripts/compat-bin/codex diff --git a/contracts/backend-boundary-v1.json b/contracts/backend-boundary-v1.json index 5ec7a95..ccd3113 100644 --- a/contracts/backend-boundary-v1.json +++ b/contracts/backend-boundary-v1.json @@ -61,6 +61,7 @@ {"scope":"hostIdRule","target":"omarchy.weather","category":"omarchyBackend","owner":"hancore.shibumi.center","capability":"weather","count":6,"backend":"omarchy.weather","removalStep":"Step 8"}, {"scope":"hostIdRule","target":"omarchy.wifiqr","category":"omarchyBackend","owner":"hancore.shibumi.network","capability":"network","count":3,"backend":"omarchy.network","removalStep":"Step 5"}, {"scope":"commandRule","value":"omarchy-agent-usage-","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"}, + {"scope":"commandRule","value":"omarchy-agent-usage-codex","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"}, {"scope":"commandRule","value":"omarchy-agent-usage-update","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"}, {"scope":"commandRule","value":"omarchy-audio-output-set-default","category":"omarchyBackend","owner":"hancore.shibumi.bluetooth","capability":"bluetooth","count":2,"backend":"omarchy.audio","removalStep":"Step 4A"}, {"scope":"commandRule","value":"omarchy-audio-output-sink","category":"omarchyBackend","owner":"hancore.shibumi.audio","capability":"audio","count":1,"backend":"omarchy.audio","removalStep":"Step 4A"}, @@ -396,6 +397,7 @@ ], "commandRules": [ {"value": "omarchy-agent-usage-", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the host Agents collector command after the Shibumi provider adapter is authoritative."}, + {"value": "omarchy-agent-usage-codex", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the revision-bound Codex collector compatibility probe with the complete Step 8 provider migration."}, {"value": "omarchy-agent-usage-update", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the host Agents update command after the Shibumi provider adapter is authoritative."}, {"value": "omarchy-audio-output-set-default", "count": 2, "category": "omarchyBackend", "owner": "hancore.shibumi.bluetooth", "capability": "bluetooth", "backend": "omarchy.audio", "removalStep": "Step 4A", "reason": "The transitional Bluetooth route and native Audio seam both use this helper until the atomic cutover removes the former."}, {"value": "omarchy-audio-input-set-default", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.audio", "capability": "audio", "backend": "omarchy.audio", "removalStep": "Step 4A", "reason": "Native Audio must preserve the official input-route handoff until the atomic cutover is complete."}, diff --git a/hancore.shibumi.ai/scripts/agents-update b/hancore.shibumi.ai/scripts/agents-update index 721d0d2..9ce5301 100755 --- a/hancore.shibumi.ai/scripts/agents-update +++ b/hancore.shibumi.ai/scripts/agents-update @@ -5,8 +5,11 @@ set -euo pipefail omarchy_path=${1:-} shift || true timeout_seconds=${SHIBUMI_AI_UPDATE_TIMEOUT:-120} +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +compat_bin="$script_dir/compat-bin" child_pid="" watchdog_pid="" +unset SHIBUMI_AI_REAL_CODEX fail() { printf 'shibumi-ai agents update: %s\n' "$*" >&2 @@ -88,6 +91,8 @@ if [[ ${1:-} == --probe-only ]]; then exit 0 fi +codex_requested=false +agent_id_count=0 for argument in "$@"; do case $argument in --force | --limits-only) continue ;; @@ -97,9 +102,38 @@ for argument in "$@"; do || fail "invalid agent id: $argument" collector="$omarchy_path/bin/omarchy-agent-usage-$argument" [[ -x $collector ]] || fail "agent collector is missing: $collector" + agent_id_count=$((agent_id_count + 1)) + [[ $argument != codex ]] || codex_requested=true done +if (( agent_id_count == 0 )) \ + && [[ -x $omarchy_path/bin/omarchy-agent-usage-codex ]]; then + codex_requested=true +fi + +compat_environment=() +if $codex_requested; then + codex_search_path=${PATH:-/usr/bin:/bin} + for candidate_dir in \ + "${HOME:?}/.local/bin" \ + "$HOME/.npm-global/bin" \ + "$HOME/.local/share/mise/shims"; do + case :$codex_search_path: in + *:"$candidate_dir":*) ;; + *) codex_search_path+=":$candidate_dir" ;; + esac + done + real_codex=$(PATH="$codex_search_path" type -P codex 2>/dev/null || true) + if [[ $real_codex == /* && -x $real_codex \ + && ! $real_codex -ef "$compat_bin/codex" ]]; then + compat_environment+=( + "PATH=$compat_bin:$codex_search_path" + "SHIBUMI_AI_REAL_CODEX=$real_codex" + ) + fi +fi -setsid env OMARCHY_PATH="$omarchy_path" "$updater" "$@" & +setsid env OMARCHY_PATH="$omarchy_path" \ + "${compat_environment[@]}" "$updater" "$@" & child_pid=$! watch_parent "$$" "$child_pid" & watchdog_pid=$! diff --git a/hancore.shibumi.ai/scripts/compat-bin/codex b/hancore.shibumi.ai/scripts/compat-bin/codex new file mode 100755 index 0000000..967c8c8 --- /dev/null +++ b/hancore.shibumi.ai/scripts/compat-bin/codex @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +real_codex=${SHIBUMI_AI_REAL_CODEX:-} +if [[ $real_codex != /* || ! -x $real_codex \ + || $real_codex -ef "${BASH_SOURCE[0]}" ]]; then + printf 'shibumi-ai codex compatibility: real Codex command is unavailable\n' >&2 + exit 127 +fi + +arguments=("$@") +app_server=false +for argument in "${arguments[@]}"; do + if [[ $argument == app-server ]]; then + app_server=true + break + fi +done + +# Omarchy v4.0.0 requests the former `untrusted` approval policy. Codex 0.149 +# removed that spelling in favor of `never`, so its app-server exits before +# initialize and the otherwise authoritative host collector publishes no +# limits. Translate only this removed app-server argument; every other Codex +# invocation and argument passes through unchanged. +if $app_server; then + for ((index = 0; index < ${#arguments[@]}; index++)); do + case ${arguments[index]} in + -a|--ask-for-approval) + if (( index + 1 < ${#arguments[@]} )) \ + && [[ ${arguments[index + 1]} == untrusted ]]; then + arguments[index + 1]=never + fi + ;; + -a=untrusted) + arguments[index]=-a=never + ;; + --ask-for-approval=untrusted) + arguments[index]=--ask-for-approval=never + ;; + esac + done +fi + +exec "$real_codex" "${arguments[@]}" diff --git a/tests/ai-plugin-regression.sh b/tests/ai-plugin-regression.sh index e668700..368aa30 100755 --- a/tests/ai-plugin-regression.sh +++ b/tests/ai-plugin-regression.sh @@ -222,6 +222,51 @@ set -e [[ $missing_collector_rc -ne 0 ]] \ || fail "missing requested agent collector was accepted" +compat_root="$tmpdir/compat-omarchy" +compat_real_bin="$tmpdir/home/.local/bin" +compat_base_bin="$tmpdir/compat-base-bin" +compat_log="$tmpdir/state/codex-compat-args" +mkdir -p "$compat_root/bin" "$compat_real_bin" "$compat_base_bin" +for utility in bash dirname env setsid sleep; do + ln -s "$(command -v "$utility")" "$compat_base_bin/$utility" +done +cat >"$compat_root/bin/omarchy-agent-usage-update" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +env codex -a untrusted exec +env codex -s read-only -a untrusted app-server +EOF +install -m 0755 /usr/bin/true \ + "$compat_root/bin/omarchy-agent-usage-codex" +cat >"$compat_real_bin/codex" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >>"${XDG_STATE_HOME:?}/codex-compat-args" +EOF +chmod 0755 "$compat_root/bin/omarchy-agent-usage-update" \ + "$compat_real_bin/codex" +run_codex_compat_case() { + local label=$1 + shift + : >"$compat_log" + PATH="$compat_base_bin" \ + HOME="$tmpdir/home" XDG_STATE_HOME="$tmpdir/state" \ + "$wrapper" "$compat_root" "$@" >/dev/null + [[ $(wc -l <"$compat_log") -eq 2 ]] \ + || fail "Codex compatibility shim changed the $label invocation count" + grep -Fx -- '-a untrusted exec' "$compat_log" >/dev/null \ + || fail "Codex compatibility shim changed a non-app-server $label invocation" + grep -Fx -- '-s read-only -a never app-server' "$compat_log" >/dev/null \ + || fail "Codex compatibility shim did not translate the $label policy" +} +# shellcheck disable=SC2329 # Exported to prove PATH-only lookup ignores it. +codex() { return 97; } +export -f codex +run_codex_compat_case explicit codex +run_codex_compat_case all-agents +run_codex_compat_case forced-all --force +unset -f codex + widget="$repo_root/hancore.shibumi.ai/BarWidget.qml" service="$repo_root/hancore.shibumi.ai/Service.qml" rg -q 'serviceFor\("hancore\.shibumi\.ai"\)' "$widget" \ @@ -312,7 +357,8 @@ rg -q 'newest_model\(messages, today_ms, tomorrow_ms\)' \ "$repo_root/hancore.shibumi.ai/scripts/opencode-usage" \ || fail "OpenCode latest model does not share the today scope" [[ -x $repo_root/hancore.shibumi.ai/scripts/opencode-usage \ - && -x $repo_root/hancore.shibumi.ai/scripts/agents-update ]] \ + && -x $repo_root/hancore.shibumi.ai/scripts/agents-update \ + && -x $repo_root/hancore.shibumi.ai/scripts/compat-bin/codex ]] \ || fail "AI provider scripts are not executable" [[ -s $repo_root/hancore.shibumi.ai/assets/codex.svg \ && -s $repo_root/hancore.shibumi.ai/assets/opencode-mark.svg ]] \