Codex cwd resolution: a rollout with no thread id must count toward ambiguity, not vanish before the count (#499) - #518
Conversation
The Codex cwd path's answer rests on a count: "exactly one rollout records this cwd, so it is the session I am in". `readRolloutMeta` returned `undefined` for a `session_meta` header stating no `payload.id`, and the scan loop reads `undefined` as "not a rollout at all", so a header that demonstrably recorded the invocation cwd never reached the candidate list. Two sessions having run in one directory therefore resolved at `ok: true` to whichever of them happened to carry a thread id, with no disclosure that a rival had been discarded. That is the artefact-of-the-bound failure LLP 0067 refuses on for a truncated scan, reached through a discard the bound never touches, and it lands on a privacy verb: `hyp session ignore` opts out one of two indistinguishable sessions and prints success. The thread id is not what this path resolves (it returns the container and carries the thread only as provenance), so requiring one to be counted was never load-bearing. `readRolloutMeta` now means only "this file's first line is not a session_meta header", and the callers refuse on what they each need: - the cwd scan counts every header recording the cwd, then refuses a lone unvouchable one by name rather than through the old (and untrue) "no Codex rollout records cwd" message - the stated-thread path skips a header naming no thread explicitly: it is an identity test, not a count, so nothing there changes - the ambiguity message names an id-less candidate as such, so the count cannot exceed the names Found while working #499 §1, which recorded round 4 of #456's `x1`/`x2` fixtures and asked that anyone touching this filter re-run them. Executed rather than reasoned about: `x1` resolved, `x2` refused. Both are now pinned. `x2` keeps refusing, which was the trade round 4 declined to give up. Refs #499
The counting rule this change documents governs `hyp session`'s resolver only. The Codex `hypaware-privacy` skill body still drops an id-less `session_meta` header before its own count, so on the two-rollouts fixture the verb refuses while the script resolves and POSTs the survivor: round 4 of #456 predicted that divergence, triage found it did not reproduce because both sides discarded, and fixing one side makes it real. Left standing because #435 retires the script onto this verb, but recorded so the doc does not read as a rule both surfaces follow. Co-Authored-By: Claude <noreply@anthropic.com>
Verdict: ship, with one finding recorded (doc fix landed as
|
…ay "upgrade" Round-2 review finding on #518 (issue #499). Once an id-less `session_meta` header survives the reader to reach the single-candidate checks, a header stating neither `payload.id` nor `session_id` becomes newly reachable, and it hit the missing-container branch first: `legacyRolloutError`, whose text is "Upgrade Codex (current versions write session_meta.session_id)" and whose argument is "its thread id is NOT that container". Neither clause holds for this file. The back-fill that rule exists to defeat needs an `id` to back-fill from, so a header without one is not a pre-field Codex, and the message names a value the header does not carry. The two refusals are therefore asked in the other order: the unvouchable-header refusal, which names the field that is actually missing, goes first, and `legacyRolloutError` is reached only with a thread id in hand, which is what both its clauses assume. Fail-closed either way and both point at the explicit-id escape hatch, so this is the diagnosis, not the behaviour. The genuine old-Codex case (a thread id, no `session_id`) keeps its own message, pinned by a test that passes both before and after. LLP 0067 §cli-session-id records the ordering and why. Its earlier self-reference to §cli-legacy-rollout, which sits in the section the paragraph is already inside, is replaced with a plain forward reference. Co-Authored-By: Claude <noreply@anthropic.com>
Verdict: ship. Round 1's [Low] is fixed and pushed (
|
Triage verdict: SHIPBoth review rounds are exhausted with one residual finding still open at head Scope question re-judged independently: holds#499 §1's closing sentence ("no code change is being requested here") is Residual 1 - [Medium]
|
Works the three deferred findings #499 records from #456's review/triage history. Two of them are correctly parked on other open issues and are stated as such below. The third, §1, was recorded as a non-finding ("no code change is being requested here") plus an adjacent trade-off; re-executing its own fixtures shows the non-finding is right and the surrounding path is not.
Finding 1 - executed, and the fixture that was said to be safe is a fail-open
Behavioural. Regression-tested, fail-then-pass below.
#499 §1 corrected round 4 of #456's review: round 4 reasoned (without executing the CLI) that the skill's
not payload.get('id')filter would let the script resolve where the CLI refuses as ambiguous. Triage executed both sides on round 4'sx1fixture and found they agree -readRolloutMeta'sif (meta?.threadId === undefined) return undefinedmakes the CLI drop the same candidate. Verdict: "not a divergence".That is correct, and it is the wrong question. Both sides agree on a wrong answer. Re-running
x1againstorigin/master:ok: true. Two sessions demonstrably ran in that directory and the resolver picked one, confidently, because the other happened to lack a thread id. The identically-shaped fixture where both carry ids refuses as expected (2 Codex rollouts record cwd ...), which is what the cwd path is for.Root cause
The cwd path's entire claim is a count: "exactly one rollout records this cwd, so it is the session I am in".
readRolloutMetacollapsed two different things into oneundefined:session_metaheader" (correct to skip), andcwdand asession_idbut nopayload.id".The scan loop reads
undefinedas not a rollout, so the second kind never reached the candidate list - it did not merely fail to resolve, it removed the evidence that the survivor was not alone. LLP 0067 §cli-session-id already refuses on exactly this shape when it arrives via a truncated scan ("the rollout that would have made it ambiguous may be one of the files never looked at"); this is the same artefact reached by a discard the bound never touches. It lands on the privacy verb:hyp session ignoreopts out one of two indistinguishable sessions and prints success.The thread id is not what this path resolves - it returns the container and carries the thread only as provenance - so requiring one in order to be counted was never load-bearing.
Fix
readRolloutMetanow returnsundefinedonly for "not asession_metaheader", and each caller refuses on what it actually needs:x2trade, deliberately preserved - but now with a refusal that names the file and the missing field, instead of the old message which claimed "no Codex rollout ... records cwd" about a rollout that plainly did.no thread id (<file>), so the count cannot exceed the names.LLP 0067 §cli-session-id gains the rule the code now follows ("recording the cwd is the whole of what makes a rollout a candidate"; usability is judged after the count, on the one candidate), landed in the same commit.
Fail-then-pass evidence
Three tests added to
test/plugins/ai-gateway-session-status.test.js. With the tests present andsession_command.jsreverted toorigin/master:Test 26 passes both ways on purpose: it pins that the counting change does not leak into
resolveFromStatedThread.With the fix applied:
The refusals the fix produces, on round 4's own fixtures:
tempCodexHomegainsnoThread: true, the mirror of its existinglegacy, so the fixture class is expressible from the test file rather than reconstructed by hand next time.Finding 2 - the POST receipt / malformed-body path
Not actionable here; no test, because there is no change. #499 §2 concludes this is already tracked by #460 ("
hyp session ignore's POST receipt cannot prove the id matches live traffic - it's Set membership, not drop verification"), and that the malformed-body case fails closed today (realcreateControlHandler400s;curl --fail-with-bodyunderset -eaborts at exit 22 before any "confirmed" prints). Verified #460 is still OPEN. Fixing it here would duplicate that issue's scope, which is a change to what the receipt proves, not a defect in this file.Finding 3 -
python3hard dependencyNot actionable here; no test, because there is no change. #499 §3 concludes this is tracked by #435 ("Collapse the session opt-out skills onto
hyp sessionand update LLP 0066/0067"), whose acceptance criteria call for removingpython3outright, and that the dependency is byte-identical toorigin/masterand fails closed (exit 127, 0 gateway calls). Verified #435 is still OPEN. Removing the dependency means collapsing five skills onto the verb, which is #435's design work, not a fix to land under this issue.Checks
npm test: 3081 pass / 8 fail / 1 skipped. The 8 are the known pre-existingtest/core/leave-command.test.jsbaseline, reproduced identically on cleanorigin/master(3078 pass / 8 fail) in this same worktree. The delta is exactly the 3 new tests.npm run typecheck: clean. (It caught a real narrowing gap inresolveFromStatedThreadwhile widening the return type; fixed before commit.)@refadded or removed, no inlineimport('...')types, no@typedef.Note for review
The behaviour change is narrow but it is a change: a tree where two rollouts record one cwd and one lacks
payload.idnow refuses where it previously resolved. That is the intended direction (a refusal costs a re-run with an explicit id; a confident wrong key costs the user the recording they believed they had stopped, LLP 0067), but it is worth a maintainer's eye because #499 §1 explicitly recorded "no code change is being requested here". That sentence is about the skill script's filter, which this PR does not touch;x2's refusal, the trade round 4 declined to give up, is preserved intact.Fixes #499