mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 09:26:50 +08:00
* MUL-7053 fix(daemon): explain the codex retired-compaction 404
Codex fails a turn with:
Error running remote compact task: unexpected status 404 Not Found:
{"detail":"Not Found"}, url: .../codex/responses/compact, cf-ray: ...
and stops there. The line that chose that URL — `remote_compaction_v2 =
false` under `[features]` in ~/.codex/config.toml — appears nowhere in the
text, so the error names no file, no key and no remedy. Codex before 26.901
ignored that value, so a line pasted long ago as a workaround for an
unrelated defect starts failing on upgrade without the user touching
anything. Nor does it resolve on its own: the conversation stays above the
auto-compact threshold, so every following turn retries the same retired
call. The reporter in GH #8000 had to open an issue to learn the fix was
deleting one line.
Recognise the failure and append the remedy to the error text, the same way
the Hermes "no LLM provider configured" hint works — a constant, kept clear
of every phrase taskfailure.Classify and the resume guards match, since it
is persisted in agent_task_queue.error and re-scanned there indefinitely.
The raw error still reaches the user unchanged; the hint rides behind it.
The compaction marker alone does not fire it: a transient 5xx or a rate
limit on the same call would send users to edit a file that was never the
problem. It needs the 404 status, or the legacy `responses/compact` path,
which is evidence of the stale flag whatever status came back.
Text only, deliberately. This does NOT retire the session: unlike the
resume-unsafe failures classified alongside it, the thread is recoverable
and recovers by itself once the flag is gone, so dropping the session
pointer would discard the conversation the one-line fix brings back.
Not doing what PR #8019 proposed — forcing `--enable remote_compaction_v2`
in daemon-managed sessions. That override carries no Multica semantics
(unlike fast_mode, which honours a tier the user paid for), upstream has
stated it will fix the flag, and it would then be permanent dead code that
blocks anyone with a legitimate reason to disable it.
Upstream: openai/codex#42468
Co-authored-by: multica-agent <[email protected]>
* MUL-7053 docs(codex): state when the compaction hint can be deleted
The hint is scoped to a defect upstream intends to fix, so the question it
has to answer for a future reader is what happens then. Under either
upstream remedy — Stage::Removed or a 404 fallback — the predicate stops
matching and nothing has to be unwound: it overrides no config and blocks
nobody from disabling the flag deliberately. Record that, and the contrast
with the override declined in #8019, next to the code rather than in the PR
thread.
Co-authored-by: multica-agent <[email protected]>
* MUL-7053 fix(codex): key the compaction hint on the route, not the status
Two ways the hint could fire on a case its remedy does not fix, both found
in review of #8032.
Requiring only the compaction marker plus a 404 was wrong about which route
failed. Codex wraps v2 compaction failures in the same "Error running remote
compact task" prefix (compact_remote_v2.rs), and a v2 stream can answer 404
too — so a user whose setting was already correct would be told to go turn
it off. Require the retired /responses/compact path instead: it cannot be
produced by the v2 route, and a compaction request reaching it means the
legacy route was selected whatever status came back. The status then stops
mattering, so a non-404 on that path matches too.
That gives up the case where Codex reports the failure without the url tail.
Nothing else in that text identifies the route and the two errors are
otherwise indistinguishable, so the choice was between missing some cases
and misdirecting the users whose config was never the problem. A missed hint
costs what today already costs; a wrong one spends someone's afternoon.
The hint also asserted the shared config file was the source. It is the
common case and the only one the upstream reports mention, but the launch
arguments reach the same state: codexBlockedArgs blocks only --listen, and
the sole feature-conflict stripping here is scoped to fast_mode under an
explicit service tier, so `--disable remote_compaction_v2` and
`-c features.remote_compaction_v2=false` survive from custom args, daemon
extra args, and a custom runtime profile's prefix alike. Anyone in that case
would have followed the hint to edit a line that is not there. The text now
names both sources — it stays a constant, which is load-bearing for the
resume guards, so it cannot narrow itself to the source that actually
applied. TestCodexLaunchArgsCanSelectTheRetiredCompactionRoute pins that the
argument half describes something real, and fails if a later change starts
stripping those spellings.
Also drops the "before 26.901" / "years ago" framing, which mixed the
Desktop build numbers with CLI versions and overstated how long the
workaround had been circulating.
Co-authored-by: multica-agent <[email protected]>
* MUL-7053 fix(daemon): stop the compaction hint contradicting itself
Adding the launch arguments as a second source left the closing line behind.
The hint sent people to check custom_args, a daemon's extra args, or a
profile's fixed args, and then asserted nothing on the Multica side needed
changing — stranding exactly the users that second source was added for,
which is the same dead end the addition was meant to close.
Scope the exclusion to the one target that is genuinely wrong to edit: the
per-task copy, which is rebuilt from the shared config every run, so an edit
there is discarded. Both real sources are simply re-read on the next run, so
that is what the text now says.
Also covers the third argv region the hint names. The prior test exercised
ExtraArgs and CustomArgs, but a custom runtime profile's fixed args reach the
child through the launch prefix, which has its own filters —
FilterLaunchPrefix, the managed-mcp_config override strip, and the priority
tier's fast_mode strip. The setting survives all three; asserting it is what
makes the hint's third source a checked claim rather than a comment.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
175 lines
6.7 KiB
Go
175 lines
6.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// codexRetiredCompactionLiveError is the failure exactly as GH #8000 reported
|
|
// it, request id and cf-ray included — the hex ids are part of the fixture on
|
|
// purpose, since a status-code match would find digits inside them.
|
|
const codexRetiredCompactionLiveError = `Error running remote compact task: unexpected status 404 Not Found: ` +
|
|
`{"detail":"Not Found"}, url: https://chatgpt.com/backend-api/codex/responses/compact, ` +
|
|
`cf-ray: a35507973eee3d57-SJC, request id: 9eed88ee-822d-446e-9b73-df49d56fe7e0`
|
|
|
|
func TestCodexRetiredCompactionError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
errText string
|
|
want bool
|
|
}{
|
|
{"live failure", codexRetiredCompactionLiveError, true},
|
|
// The path is what identifies the route, so any status on it counts:
|
|
// a compaction request reaching /responses/compact at all means the
|
|
// legacy route was selected, and the remedy is the same either way.
|
|
{
|
|
"retired path with a non-404 status",
|
|
`Error running remote compact task: unexpected status 502 Bad Gateway, ` +
|
|
`url: https://chatgpt.com/backend-api/codex/responses/compact`,
|
|
true,
|
|
},
|
|
{
|
|
"case insensitive",
|
|
`ERROR RUNNING REMOTE COMPACT TASK: UNEXPECTED STATUS 404 NOT FOUND, ` +
|
|
`URL: HTTPS://CHATGPT.COM/BACKEND-API/CODEX/RESPONSES/COMPACT`,
|
|
true,
|
|
},
|
|
|
|
// The reason the path is required rather than the status. Codex wraps
|
|
// v2 compaction failures in the SAME "Error running remote compact
|
|
// task" prefix (compact_remote_v2.rs), and a v2 stream can answer 404
|
|
// too. Firing here would tell a user whose setting is already correct
|
|
// to go turn it off — the exact opposite of the fix.
|
|
{
|
|
"v2 route returning the same status",
|
|
`Error running remote compact task: unexpected status 404 Not Found: {"detail":"Not Found"}, ` +
|
|
`url: https://chatgpt.com/backend-api/codex/responses`,
|
|
false,
|
|
},
|
|
{
|
|
"v2 route failing some other way",
|
|
`Error running remote compact task: remote compaction v2 stream closed before response.completed`,
|
|
false,
|
|
},
|
|
// The accepted false negative: without the url tail nothing in the
|
|
// text says which route ran, and guessing would land on the case
|
|
// above. No hint costs what today already costs; a wrong hint is worse.
|
|
{
|
|
"no url tail",
|
|
`Error running remote compact task: unexpected status 404 Not Found: {"detail":"Not Found"}`,
|
|
false,
|
|
},
|
|
// The path without the compaction marker is some other request.
|
|
{
|
|
"path outside a compaction failure",
|
|
`Error running turn: unexpected status 404 Not Found, ` +
|
|
`url: https://chatgpt.com/backend-api/codex/responses/compact`,
|
|
false,
|
|
},
|
|
{"unrelated codex failure", "codex thread/resume failed: token too long", false},
|
|
{"empty", "", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := CodexRetiredCompactionError(tc.errText); got != tc.want {
|
|
t.Errorf("CodexRetiredCompactionError(%q) = %v, want %v", tc.errText, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCodexLaunchArgsCanSelectTheRetiredCompactionRoute is the behavioural
|
|
// basis for the hint naming launch arguments alongside the config file.
|
|
//
|
|
// The remedy text is a constant, so it cannot know which source turned the
|
|
// setting off — which makes "is an argument even a real source?" a claim the
|
|
// code has to back. It is: codexBlockedArgs blocks only --listen, and every
|
|
// feature-conflict filter here is scoped to fast_mode or the managed
|
|
// mcp_servers namespace. So both spellings survive from all three argv regions
|
|
// the hint names — an agent's custom args, a daemon's extra args, and a custom
|
|
// runtime profile's fixed args — and the config file is not the only place to
|
|
// look.
|
|
//
|
|
// If a future change starts stripping these — the #8019 override, say — this
|
|
// test fails, which is the signal to drop that half of the hint rather than
|
|
// keep pointing users at a source that no longer exists.
|
|
func TestCodexLaunchArgsCanSelectTheRetiredCompactionRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{"disable flag", []string{"--disable", "remote_compaction_v2"}},
|
|
{"disable flag with inline value", []string{"--disable=remote_compaction_v2"}},
|
|
{"config override", []string{"-c", "features.remote_compaction_v2=false"}},
|
|
{"config override shell-quoted", []string{"'-c'", "'features.remote_compaction_v2=false'"}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
// Both argv regions, since custom_args and a daemon's extra args
|
|
// are separate inputs and the hint names both.
|
|
for region, args := range map[string][]string{
|
|
"extra_args": tc.args,
|
|
"custom_args": tc.args,
|
|
} {
|
|
var extra, custom []string
|
|
if region == "extra_args" {
|
|
extra = args
|
|
} else {
|
|
custom = args
|
|
}
|
|
got := NormalizeCodexLaunchArgs(extra, custom, nil, nil)
|
|
if !slices.Contains(got, "remote_compaction_v2") &&
|
|
!slices.Contains(got, "--disable=remote_compaction_v2") &&
|
|
!slices.Contains(got, "features.remote_compaction_v2=false") {
|
|
t.Errorf("%s: setting did not survive normalization: %q", region, got)
|
|
}
|
|
}
|
|
|
|
// The one place this package does strip a --disable is the
|
|
// fast_mode conflict removal under an explicit service tier.
|
|
// It is scoped to that feature, and the effective argv is where
|
|
// that has to hold.
|
|
effective := buildCodexArgs(ExecOptions{
|
|
CustomArgs: tc.args,
|
|
ServiceTier: codexFastServiceTier,
|
|
}, nil)
|
|
if !mentionsRemoteCompactionV2(effective) {
|
|
t.Errorf("priority tier stripped an unrelated setting: %q", effective)
|
|
}
|
|
|
|
// The third argv region the hint names: a custom runtime profile's
|
|
// fixed args. It reaches the child through the launch prefix, not
|
|
// through NormalizeCodexLaunchArgs, so it has its own filters —
|
|
// all three of which the setting has to survive for the hint to be
|
|
// telling the truth about where to look.
|
|
prefix := FilterLaunchPrefix("codex", tc.args, nil)
|
|
if !mentionsRemoteCompactionV2(prefix) {
|
|
t.Errorf("launch prefix filter dropped the setting: %q", prefix)
|
|
}
|
|
if got := filterCodexCustomConfigOverrides(prefix, nil); !mentionsRemoteCompactionV2(got) {
|
|
t.Errorf("managed mcp_config prefix filter dropped the setting: %q", got)
|
|
}
|
|
if got := stripCodexFastModeConflicts(prefix, nil); !mentionsRemoteCompactionV2(got) {
|
|
t.Errorf("priority tier prefix filter dropped the setting: %q", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// mentionsRemoteCompactionV2 reports whether the setting survived in any of its
|
|
// spellings — as a standalone --disable value, an inline one, or a -c override.
|
|
func mentionsRemoteCompactionV2(args []string) bool {
|
|
return slices.ContainsFunc(args, func(arg string) bool {
|
|
return strings.Contains(arg, "remote_compaction_v2")
|
|
})
|
|
}
|