diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02344a0aa2..eef67dbf3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -516,7 +516,7 @@ jobs: if [ "$suite_status" -eq 0 ]; then exit 0 fi - if ! grep -Eqi 'oh no: Bun has crashed|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." exit "$suite_status" fi @@ -611,7 +611,31 @@ jobs: # the only one left on Bun's 5s default, and it is the slowest hardware on the board. # Three of its failures were the default firing on tests that had not hung — the # composed-acceptance cases spawn a real `ocx start` and were still working at 41s. - run: bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 + # + # The retry is the same one the macOS leg already carries, for the same reason: a Bun + # runtime panic is a crash in the interpreter, not a test result, and failing the shard + # on it reports a defect this repository does not have (#2152). An ordinary assertion + # failure returns its status immediately — only the crash signatures below are retried, + # and only once, so a genuinely broken build cannot be retried into green. + shell: bash + run: | + set +e + set -uo pipefail + suite_log="$(mktemp -t ocx-windows-suite.XXXXXX)" + for attempt in 1 2; do + bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 2>&1 | tee "$suite_log" + suite_status="${PIPESTATUS[0]}" + if [ "$suite_status" -eq 0 ]; then + exit 0 + fi + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + echo "::error::Windows shard ${{ matrix.shard }}/4 failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + exit "$suite_status" + fi + echo "::warning::Bun runtime crash in Windows shard ${{ matrix.shard }}/4 (exit ${suite_status}, attempt ${attempt})." + done + echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/4; failing after one retry." + exit 1 - name: CLI help smoke run: bun run src/cli/index.ts help diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png new file mode 100644 index 0000000000..d2810138f7 Binary files /dev/null and b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png differ diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png new file mode 100644 index 0000000000..86de3cdfe5 Binary files /dev/null and b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png differ diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 113fc419d2..d6c503134a 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -638,6 +638,9 @@ export const de: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Nur abgefangene Helfer", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Abgefangene Helfer-Anfrage", "logs.filter.conversation.label": "Konversation", "logs.filter.conversation.placeholder": "Konversations-ID einfügen", "logs.filter.conversation.clear": "Löschen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 997fcdc806..87892599ce 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -671,6 +671,9 @@ export const en = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Intercepted helpers only", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Intercepted helper request", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Paste conversation id", "logs.filter.conversation.clear": "Clear", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 112fd7ad2a..4da3bc33f1 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -652,6 +652,9 @@ export const fr: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Assistants interceptés uniquement", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Requête d'assistant interceptée", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Coller l’ID de conversation", "logs.filter.conversation.clear": "Effacer", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 69fdac1c93..a65cc76d6b 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -614,6 +614,9 @@ export const ja: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "インターセプトされたヘルパーのみ", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "インターセプトされたヘルパー要求", "logs.filter.conversation.label": "会話", "logs.filter.conversation.placeholder": "会話 ID を貼り付け", "logs.filter.conversation.clear": "クリア", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 9f491a1efc..ba19d179e8 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -657,6 +657,9 @@ export const ko: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "가로챈 헬퍼만", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "가로챈 헬퍼 요청", "logs.filter.conversation.label": "대화", "logs.filter.conversation.placeholder": "대화 ID 붙여넣기", "logs.filter.conversation.clear": "지우기", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 00aa9cb06b..f41af16d00 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -655,6 +655,9 @@ export const ru: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Только перехваченные помощники", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Перехваченный запрос помощника", "logs.filter.conversation.label": "Диалог", "logs.filter.conversation.placeholder": "Вставьте ID диалога", "logs.filter.conversation.clear": "Сбросить", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 743be2af77..101dc684c4 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -662,6 +662,9 @@ export const tr: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Yalnizca yakalanan yardimcilar", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Yakalanan yardimci istegi", "logs.filter.conversation.label": "Sohbet", "logs.filter.conversation.placeholder": "Sohbet ID'sini yapıştırın", "logs.filter.conversation.clear": "Temizle", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c195aead79..89b891d603 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -509,6 +509,9 @@ export const zhTW: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "僅已攔截的輔助請求", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "已攔截的輔助請求", "logs.filter.conversation.label": "對話", "logs.filter.conversation.placeholder": "貼上對話 ID", "logs.filter.conversation.clear": "清除", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7dd4beb860..c5d588efa0 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -650,6 +650,9 @@ export const zh: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "仅已拦截的辅助请求", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "已拦截的辅助请求", "logs.filter.conversation.label": "会话", "logs.filter.conversation.placeholder": "粘贴会话 ID", "logs.filter.conversation.clear": "清除", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index 55a7ef634d..4ec0aede31 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -133,6 +133,15 @@ export interface LogEntry { provider: string; surface?: LogSurface; conversationId?: string; + /** + * The original helper model, when Shadow Call Intercept rewrote this request. + * + * Present ONLY for an intercepted request. A helper request that was not intercepted -- + * interception off, no replacement model, or a slug the matcher does not recognize -- is + * indistinguishable here from ordinary traffic, which is why the filter below says + * "intercepted" rather than "helper". + */ + shadowCallRewrittenFrom?: string; requestedEffort?: string; effectiveEffort?: string; reasoningWireField?: string; @@ -174,6 +183,7 @@ function validCachedLogs(cached: LogEntry[] | null): LogEntry[] | null { || typeof entry.provider !== "string" || typeof entry.status !== "number" || typeof entry.durationMs !== "number" + || (entry.shadowCallRewrittenFrom !== undefined && typeof entry.shadowCallRewrittenFrom !== "string") || !validCachedRouteDecision(entry.routeDecision) ) { return null; @@ -355,6 +365,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const [autoRefresh, setAutoRefresh] = useState(true); const [detail, setDetail] = useState(null); const [surfaceFilter, setSurfaceFilter] = useState("all"); + const [interceptedHelpersOnly, setInterceptedHelpersOnly] = useState(false); const [conversationFilter, setConversationFilter] = useState(""); const [conversationQueryHash, setConversationQueryHash] = useState(); const scrollContainerRef = useRef(null); @@ -472,6 +483,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const filteredLogs = logs.filter(log => ( logMatchesSurface(log, surfaceFilter) + && (!interceptedHelpersOnly || Boolean(log.shadowCallRewrittenFrom)) && (!conversationQuery || matchesLogConversationId(log.conversationId, conversationQuery, conversationQueryHash)) )); const conversationTotals = conversationQuery ? summarizeFilteredLogs(filteredLogs) : null; @@ -566,6 +578,20 @@ export default function Logs({ apiBase }: { apiBase: string }) { ))} + {/* + "Intercepted", not "helper". The marker only exists when Shadow Call Intercept + rewrote the request, so a helper request that was not intercepted looks exactly like + ordinary traffic here. A broader label would promise a classification this data + cannot support. + */} +