Skip to content

Codex cwd resolution: a rollout with no thread id must count toward ambiguity, not vanish before the count (#499) - #518

Merged
philcunliffe merged 3 commits into
masterfrom
fix/issue-499
Jul 31, 2026
Merged

Codex cwd resolution: a rollout with no thread id must count toward ambiguity, not vanish before the count (#499)#518
philcunliffe merged 3 commits into
masterfrom
fix/issue-499

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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's x1 fixture and found they agree - readRolloutMeta's if (meta?.threadId === undefined) return undefined makes 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 x1 against origin/master:

x1 (two rollouts record /repo/here, one with no payload.id):
  {"ok":true,"sessionId":"C2","source":"codex_rollout",
   "evidence":"rollout-2026-01-02-b.jsonl","threadId":"THREAD-B"}

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". readRolloutMeta collapsed two different things into one undefined:

  • "this file's first line is not a session_meta header" (correct to skip), and
  • "this file's header states a cwd and a session_id but no payload.id".

The scan loop reads undefined as 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 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 in order to be counted was never load-bearing.

Fix

readRolloutMeta now returns undefined only for "not a session_meta header", and each caller refuses on what it actually needs:

  • cwd scan: counts every header recording the cwd. A lone id-less candidate still refuses rather than resolving the container it states - that is round 4's x2 trade, 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.
  • stated-thread path: skips a header naming no thread explicitly. That path is an identity test, not a count, so its behaviour is unchanged.
  • ambiguity message: names an id-less candidate as 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 and session_command.js reverted to origin/master:

ok 23 - refuses (never guesses newest) when several Codex rollouts match the cwd
not ok 24 - a cwd match with NO thread id still makes the answer ambiguous: it is not discarded before the count
    two rollouts record this cwd, so neither is the unique match
    true !== false
not ok 25 - a LONE cwd match with no thread id refuses rather than resolving the container it states
    error: 'say which field is missing'
    actual: 'could not resolve a session id: no client stated one (...) and no Codex
             rollout under /tmp/hyp-codex-home-B5hO7t/sessions records cwd /repo/here. ...'
ok 26 - a stated thread ignores an id-less rollout entirely: that path is identity, not counting
# tests 47  # pass 45  # fail 2

Test 26 passes both ways on purpose: it pins that the counting change does not leak into resolveFromStatedThread.

With the fix applied:

# tests 47  # pass 47  # fail 0

The refusals the fix produces, on round 4's own fixtures:

x1 -> 2 Codex rollouts record cwd /repo/here - no thread id (rollout-...-a.jsonl),
      THREAD-B (rollout-...-b.jsonl). Pass the intended session id explicitly ...
x2 -> the only Codex rollout recording cwd /repo/here (rollout-...-a.jsonl) states no
      thread id (payload.id), which every session_meta Codex writes carries ...

tempCodexHome gains noThread: true, the mirror of its existing legacy, 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 (real createControlHandler 400s; curl --fail-with-body under set -e aborts 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 - python3 hard dependency

Not 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 session and update LLP 0066/0067"), whose acceptance criteria call for removing python3 outright, and that the dependency is byte-identical to origin/master and 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-existing test/core/leave-command.test.js baseline, reproduced identically on clean origin/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 in resolveFromStatedThread while widening the return type; fixed before commit.)
  • Style: no semicolons added, 0 em dashes added anywhere in the diff, no @ref added or removed, no inline import('...') 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.id now 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

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>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: ship, with one finding recorded (doc fix landed as a6f0112)

The scope question resolves in the author's favour. The behaviour change is a
real fail-open closed, the trade-off round 4 asked to keep is kept, and the one
thing the change costs is now recorded rather than left implicit. Everything
below was executed, not reasoned about.

The scope question: is "no code change is being requested here" violated?

No, on three pieces of evidence.

  1. The sentence is scoped to the script. Follow-up: deferred review findings from PR #456 #499 §1's closing paragraph runs
    "if anyone later fixes this filter to mirror the CLI more literally ...
    they should re-run round 4's x1/x2 fixtures and confirm x2 still
    refuses. No code change is being requested here." "This filter" has one
    referent in that paragraph: the skill's not payload.get('id'). This PR does
    not touch it, and it does re-run both fixtures and does keep x2 refusing
    (session_command.js:622, test at
    test/plugins/ai-gateway-session-status.test.js:403). The instruction that
    sentence attaches to is followed, not evaded.

  2. §1's conclusion is about agreement, not correctness. "Not a divergence"
    is a true statement about two surfaces returning the same answer. It is
    silent on whether that answer is right. Running the same fixture on
    origin/master:

    master x1: {"ok":true,"sessionId":"C2","source":"codex_rollout",
                "evidence":"rollout-2026-01-02-b.jsonl","threadId":"THREAD-B"}
    

    Two session_meta headers state cwd=/repo/here. The resolver returns one
    of them at ok: true, and the identically-shaped fixture where both carry
    ids refuses. The only difference between resolving and refusing is a field
    this path does not resolve on. That is a defect §1 did not look for, found by
    executing §1's own fixture, which is what a follow-up issue is for.

  3. The rule the fix restores is already in the doc. LLP 0067
    §cli-session-id refuses a unique cwd match reached over a truncated listing
    because "the rollout that would have made it ambiguous may be one of the
    files never looked at". A candidate discarded inside the reader is the same
    artefact by a different route, and the count is the same uniqueness claim.
    The fix makes the code match guidance that predates it rather than inventing
    a policy.

The x2 trade is correct and deliberate: a lone id-less header refuses instead
of resolving the container it states, which is the half round 4 declined to give
up, and the refusal now names the file and the missing field instead of the old
message claiming no rollout recorded the cwd at all.

Findings

1. [Medium] Fixing one side makes round 4's predicted divergence real - hypaware-core/plugins-workspace/codex/skills/hypaware-privacy/SKILL.md:72

if payload.get('cwd') != cwd or not payload.get('id'): continue still drops an
id-less header before the script's own len(matches) > 1 count. Both surfaces
on the x1 fixture, at this head:

SCRIPT: RESOLVES and POSTs session_id = C2 from rollout-2026-01-02-b.jsonl
CLI:    {"ok":false,"error":"... 2 Codex rollouts record cwd /repo/here -
         no thread id (rollout-...-a.jsonl), THREAD-B (rollout-...-b.jsonl) ..."}

That is exactly the shape round 4 flagged (script resolves, CLI refuses as
ambiguous, a wrongly-confirmed opt-out), which triage correctly found did not
reproduce because both sides discarded. Repairing one side makes the prediction
come true. The PR's own root cause applies verbatim to the script: the count is
the uniqueness claim, and a header dropped for a field the script does not send
takes the ambiguity with it.

Not fixed in code, deliberately. That filter has been declined twice on
purpose, #435 retires this script onto the verb rather than repairing it, and a
reviewer rewriting a privacy skill autonomously would exceed this PR's scope far
more clearly than the PR exceeds #499's. What was not acceptable was leaving it
silent: #499 §1's record ("both surfaces refuse or resolve to the same candidate
set on every fixture tested") stops being true the moment this merges, and the
new LLP paragraph otherwise reads as a rule both surfaces follow.

Fixed as documentation in a6f0112: LLP 0067 §cli-session-id now names the
skill body as the exception, states which fixture separates the two surfaces,
and points at #435. Suggest a comment on #435 (or a one-line follow-up issue) so
the fail-open is tracked on the issue that owns the file, not only in the design
doc.

2. [Low] A header with neither payload.id nor session_id gets the wrong diagnosis - hypaware-core/plugins-workspace/ai-gateway/src/session_command.js:613

Newly reachable state: before this change such a file was dropped before the
count, so it never reached the single-candidate checks. It now hits the
only.sessionId === undefined branch first and gets legacyRolloutError, whose
text is "Upgrade Codex (current versions write session_meta.session_id)". A
header carrying no payload.id is not an old Codex, it is a file nothing
accounts for, which is precisely what the new branch three lines below says
better. Fail-closed either way and both messages point at the explicit-id escape
hatch, so this is wording, not behaviour. Left alone because the ordering is a
defensible author call (an unreadable container is the more fundamental
failure); flagged so the choice is a made one rather than an accident.

3. [Nit] The new LLP paragraphs cross-link the section they sit in - llp/0067-session-opt-out.design.md:454-474

They land inside #### session_id absent is unresolvable {#cli-legacy-rollout}
and close with "exactly as §cli-legacy-rollout reads the
raw line without trusting it". Pre-existing structural quirk (the ambiguity,
truncation and staleness paragraphs already sit under that #### rather than
directly under {#cli-session-id}), so not worth restructuring here, but the
self-link reads oddly. The @refs are unaffected and resolve: #cli (:215)
and #cli-session-id (:305) both exist, and the #### is nested inside the
latter, so all four refs in session_command.js still land.

4. [Info] Unrelated flake observed - test/core/policy-command.test.js

One full-suite run produced a ninth failure: all 38 subtests passed (1..38),
then the file failed on async activity after the test ended
(ENOENT ... dev-telemetry/metrics-*.jsonl, teardown racing the temp-dir
removal). Re-running the file alone: 38/38. Not caused by this branch (the run
that produced it differed from a clean run by a markdown-only edit). Noted
because a dev-telemetry write outliving its HYP_HOME is the kind of thing the
log-driven-development rule cares about.

Verification performed

  • Blast radius (the risky part). readRolloutMeta is module-private (no
    export), and a repo-wide grep finds exactly two callers, both in the same
    file: the cwd scan (:588) and resolveFromStatedThread (:691). Each
    refusal is correct. The cwd scan counts first, then refuses per candidate on
    staleness, container, and now thread id. The stated-thread path is an identity
    test, so a header naming no thread is skipped and behaviour is unchanged; test
    26 pins that and passes both before and after, which is the right way to pin a
    non-change. codex/src/rollout-cwd.js and codex/src/backfill.js call core's
    readRolloutSessionMeta directly and are untouched, so nothing outside this
    file sees the widened return. The one remaining meaning of undefined ("not a
    session_meta header") is still pinned by the existing test at :332.
    legacyRolloutError interpolates no thread id, so the widened type cannot
    leak an undefined into a message.
  • Fail-then-pass, reproduced by reverting the source only (git checkout origin/master -- session_command.js, tests left at this head): tests 24 and
    25 fail, 47 total / 45 pass / 2 fail. Restored: 0 fail. Matches the claim
    exactly, including test 26 passing both ways.
  • Suite: 3081 pass / 8 fail / 1 skipped. The 8 are the known
    test/core/leave-command.test.js baseline (test/core/leave-command.test.js: 8 of 11 tests fail on master (exit 1 from hyp leave), red since the tests were introduced #512) and nothing else.
  • npm run typecheck: clean.
  • House rules: 0 em dashes added in the diff, no semicolon-terminated added
    JS lines, no inline import('...') types, no @typedef, no type-import
    specifier changes. The @ref on readRolloutMeta was re-read against
    §cli-session-id and still applies; the annotation map entry at :686 is still
    accurate.

What I changed

a6f0112, documentation only, no source or test touched: eleven lines in
llp/0067-session-opt-out.design.md naming the surviving skill-body divergence
from finding 1, the fixture that separates the two surfaces, and #435 as the
issue that owns the file.

…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>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: ship. Round 1's [Low] is fixed and pushed (a37805b); the [Medium] stays parked, correctly

Round 2 at a6f0112, with one commit landed on top. Nothing new found in the
behaviour; the one change this round is a diagnosis, not a refusal. Everything
below was executed.

1. The round-1 docs commit landed and is accurate

a6f0112 is on the branch and is documentation only (git diff --stat 0208b02..a6f0112 touches llp/0067-session-opt-out.design.md alone). Read
against the shipped code, all three of its claims hold:

  • "Recording the cwd is the whole of what makes a rollout a candidate" matches
    the loop at session_command.js:586-591, whose only filter is meta.cwd !== args.cwd.
  • "A lone such header still refuses" matches :615, and "usability is judged
    after the count, one candidate at a time" matches the order of the age,
    container and header checks inside the length === 1 block.
  • The skill-body exception paragraph matches the file: SKILL.md:72 still
    reads if payload.get('cwd') != cwd or not payload.get('id'): continue, and
    the PR's diff against origin/master touches nothing under
    plugins-workspace/codex/skills/ at all.

2. [Low] fixed - hypaware-core/plugins-workspace/ai-gateway/src/session_command.js:612

Round 1 flagged that a header stating neither payload.id nor session_id is
newly reachable and lands on legacyRolloutError first, whose text is "Upgrade
Codex (current versions write session_meta.session_id)".

Fixed, because the misdiagnosis is worse than "wording" on a second look.
legacyRolloutError makes two claims that are both false for this file:

  • "Upgrade Codex" is the right advice only for a rollout predating the field.
    §cli-legacy-rollout exists to defeat Codex's Deserialize back-fill of
    session_id from id - and that back-fill needs an id to back-fill from.
    A header without one is not a pre-field Codex by that section's own mechanism.
  • "Its thread id is NOT that container" argues about a value the header does
    not carry.

So the two refusals now ask in the other order: the unvouchable-header refusal
(which names the field actually missing) goes first, and legacyRolloutError is
reached only with a thread id in hand, which is what both its clauses assume.
That also restores the invariant it had before this PR, when readRolloutMeta
guaranteed a thread id on every candidate. Both branches are ok: false and
both point at the explicit-id escape hatch, so no refusal changed - only which
one fires.

Two tests, at test/plugins/ai-gateway-session-status.test.js:428 and :451:

  • "a header stating NEITHER field is not diagnosed as an old Codex" -
    fail-then-pass verified by reverting session_command.js to a6f0112
    with the tests left in place: not ok 26, 48 pass / 1 fail. Restored: 49 / 0.
  • "a rollout that DOES state a thread id but no session_id is still the legacy
    diagnosis"
    - passes both ways on purpose, pinning that the reorder does not
    cost the old-Codex case its own actionable message.

The one-clause change in the surviving refusal ("the session_id on it" ->
"nothing it states about the session") drops a presupposition that a
session_id is present, which is exactly the case this branch now also serves.

llp/0067 §cli-session-id gains the ordering and its reason, in the same
commit. That edit also resolves round 1's [Nit] 3 at zero cost: the paragraph's
link to [§cli-legacy-rollout](#cli-legacy-rollout), which pointed at the
#### section it already sits inside, is now a plain forward reference. The
structural quirk itself (ambiguity/truncation/staleness paragraphs living under
that ####) is pre-existing and still not worth restructuring here.

3. [Medium] skill-script divergence - re-examined, still parked, not made worse

hypaware-core/plugins-workspace/codex/skills/hypaware-privacy/SKILL.md:72.
Re-confirmed at this head:

Still a real fail-open on the script path until #435 lands. It should not block
this PR: the alternative is leaving a fail-open in the verb that the privacy
skills are being collapsed onto.

4. Master moved - merges cleanly

origin/master is now 71c6231 (#505, #502, #491 merged since round 1).
git merge --no-commit --no-ff origin/master from this branch: "Automatic
merge went well"
, exit 0, no conflicts, at both a6f0112 and the new head
a37805b. Reset, not committed; the branch is unmerged as before. The overlap
is real but disjoint (#491 and #502 touch codex/src/exchange-projector.js,
ai-gateway/src/source.js and their LLPs, not session_command.js or
llp/0067). mergeStateStatus is BLOCKED on review, not on conflict.

5. Fresh review at this head - nothing else found

  • Blast radius re-verified, not inherited. readRolloutMeta is still
    module-private with exactly two callers, both in-file (:588, :699), and
    resolveSessionIdForCli has exactly two consumers, also both in-file
    (:153, :243). Nothing outside the file observes the widened return.
  • Every refusal whose reachability changed, walked. A session_meta header
    with no payload object, or with a cwd that is absent/blank/relative, is
    now returned by the reader where it used to be discarded, and is filtered by
    meta.cwd !== args.cwd instead - same outcome, no new branch. A blank or
    whitespace-only payload.id reaches the new refusal, which is correct:
    metaField trims, and §cli-legacy-rollout already rules blank is absent.
    resolveFromStatedThread's found === undefined continue is exactly the old
    reader-side discard moved one level out, so that path is unchanged, as its
    test (passing both ways) pins.
  • @refs re-read, all four resolve. #cli and #cli-session-id both exist
    in llp/0067; the annotation-map row for readRolloutMeta (:704) still
    describes what the function does.
  • Adjacent LLPs checked for staleness (living-docs rule), since both
    describe this function by name. llp/0150 says readRolloutMeta "passes an
    absent cwd straight through rather than discarding" and "the predicate
    governs the field, not the file" - the change makes that more true, not
    stale. llp/0083's no-id divergence note is about codex/src/rollout-cwd.js's
    live resolver, a different reader, and is untouched. Neither needs an edit.
  • PR body accuracy: its "Note for review" still describes the shipped
    change, including the x2 trade. The error strings it quotes are unchanged by
    this round except for the presupposition clause noted above.

Checks

What I changed

a37805b, pushed to fix/issue-499:

  • session_command.js - the two single-candidate refusals swap order, with the
    reason at the call site; legacyRolloutError's JSDoc records that both
    callers must establish a thread id first, since its text relies on one twice;
    the surviving refusal drops a clause presupposing a session_id.
  • test/plugins/ai-gateway-session-status.test.js - two tests, one
    fail-then-pass, one non-change pin.
  • llp/0067-session-opt-out.design.md - §cli-session-id records the ordering
    and its reason, and loses the self-referential link.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Triage verdict: SHIP

Both review rounds are exhausted with one residual finding still open at head
a37805b. Judged independently rather than deferring to the rounds' own
verdict.

Scope question re-judged independently: holds

#499 §1's closing sentence ("no code change is being requested here") is
locally scoped to the paragraph it sits in, which is entirely about the
hypaware-privacy skill's not payload.get('id') filter - the referent of
"this filter" two sentences earlier. #518 does not touch that file
(git diff origin/master...a37805b --stat -- plugins-workspace/codex/skills/
is empty). #499 §1's actual conclusion ("not a divergence") is a true
statement about two surfaces agreeing, not about either being correct - #518
re-executed §1's own x1 fixture and found both surfaces agreed on a wrong
answer (a two-rollout cwd match silently resolving because one candidate
lacked payload.id). LLP 0067 §cli-session-id's pre-existing truncated-walk
paragraph ("the rollout that would have made it ambiguous may be one of the
files never looked at") already states the general principle this fix
applies to a second mechanism of evidence loss (discard-in-reader, not
bound-truncation) - confirmed pre-existing via git diff (not part of this
PR's diff to the LLP). The fix makes the code match a rule the doc already
stated, it does not invent new scope.

Residual 1 - [Medium] SKILL.md:72 divergence: non-blocking, tracked

Re-verified: real, privacy-relevant (the script resolves and POSTs where the
CLI now refuses), and created live by this merge fixing only the CLI side of
a bug the script still has. Not introduced by this PR's diff (empty under
plugins-workspace/codex/skills/), not new to the script's own behaviour
(byte-identical to origin/master), and already owned by #435 (OPEN, carries
a marker-signed cross-ref comment with the exact fixture and both surfaces'
outputs). Blocking #518 over this would leave the CLI's fail-open live too,
for no improvement to the script. Filed as #534 for a durable trail on this
PR's own review thread, backlinking #435 as the file's actual owner.

Residual 2 - legacyRolloutError reordering: verified landed, not a residual

Re-read the diff directly: resolveSessionIdForCli now checks
only.threadId === undefined (names the missing payload.id) BEFORE
only.sessionId === undefined (legacyRolloutError, "Upgrade Codex"), so a
header stating neither field gets the correct diagnosis. Confirmed by
running test/plugins/ai-gateway-session-status.test.js in a clean worktree:
49/49 pass, including both ordering-pinned tests. No further action needed.

Checks re-run in a clean worktree at a37805b

Follow-up issue: #534. Nothing further blocks this PR; next tick's
ready-hold flips it out of draft.

@philcunliffe
philcunliffe marked this pull request as ready for review July 31, 2026 06:41
@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 31, 2026
@philcunliffe
philcunliffe merged commit 2ef9fc5 into master Jul 31, 2026
9 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-499 branch July 31, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow-up: deferred review findings from PR #456

1 participant