Skip to content

Document the state and ownership refusals, and make their printed recovery runnable - #18

Merged
thisisjun786 merged 5 commits into
devfrom
codex/jun-103-operations-doc
Sep 17, 2026
Merged

thisisjun786 merged 5 commits into
devfrom
codex/jun-103-operations-doc

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Closes the discoverability half of JUN-103's diagnostics criterion: that an operator can find out which database was selected and why a command refused.

The problem

JUN-103 shipped six operator-visible refusal reasons — ambiguous_state_directory, unidentified_state_directory, state_directory_serves_another_socket, supervised_store_mismatch, replaced_by_new_launch, ownership_unverifiable. At 1f04c91 all six appear in src/ and none appear anywhere in docs/. An operator who hits one has the reason string, maybe a list of candidate paths, and nothing to read.

Worse, the recovery advice the code prints does not survive being read as instructions. The ambiguity refusal ended with:

"--state <the directory above> once, to adopt it deliberately"

That is not a command. It never says which directory. It drops the --socket that made two stores candidates for each other in the first place. And it promises an adoption that does not happen — choosing one of two claiming stores leaves both still recording the socket, so the very next invocation relying on default discovery is refused again. The different-socket refusal printed no recovery at all.

What changed

cli.py_recovery_commands() builds complete, runnable commands that carry the socket and inspect each candidate with both doctor and service status. Every command only reads: provenance is recorded by opening a store with a socket, which is exactly what the refusal prevented, so inspection is safe to repeat. The contested case now states in the payload that choosing does not retire the other store and that every participant needs the same --state until one is gone. The different-socket refusal gains its own two-command list plus a note explaining there is nothing to adopt, because using a store does not rewrite the socket it recorded.

_program() derives the invocation from argv rather than hardcoding it, since the console script and python3 -m codex_session_relay.cli are both ordinary ways to reach this code and a command naming the wrong one is a command the operator has to translate.

docs/operations.md — a new "When the rules cannot pick a store" subsection covering the three selection refusals, when each fires, and the inspect-before-adopt recovery. Two limits are stated rather than glossed. Under Ownership, a reason-to-action table for the four ownership refusals, because the distinctions decide the operator's next move: not_ours is a definite mismatch, ownership_unverifiable is insufficient evidence and often resolves itself, replaced_by_new_launch means the service is not stopped and the original launch may already have been terminated, supervised_store_mismatch is not repaired by a restart.

docs/invariants.md — one row claimed more than the code delivers. "stop and disable hold the lock across the decision" omitted the handoff window, so it now says what is true, and three limits are recorded beneath it: lock_is_held reporting an unopenable lock file as not held, disable/enable writing shared intent outside the lock on the accept path, and start confirming from a record plus a separate probe.

Those three are the seam that produced a new finding in five consecutive review rounds on #8. They are documented here rather than patched, because each narrow fix revealed the next window and the seam wants one deliberate revision.

Accuracy corrections caught in review

An independent audit of the plan corrected three things before they shipped:

  • "doctor and ack-proof are never refused" is false. They are exempt from the state-selection guards only; doctor still exits non-zero when a same-store comparison it was asked to make comes back unproven or mismatched.
  • The guards run before dispatch, so they apply to commands that open no store at all — service status included. The text says "when default discovery would select a new database", not "only for commands that create stores".
  • "Records no socket" also covers a store whose metadata is unreadable or malformed, since store_socket converts read errors to None.

Validation

Two regression tests in tests/test_cli.py::ContestedSocket, both verified red against the previous payload (KeyError: 'recover' for the different-socket case; the vanished "directory above" phrasing still present for the ambiguity case):

  • test_the_refusal_prints_commands_an_operator_can_actually_run — every command carries the socket, every candidate is inspected by both doctor and service status, the unnamed-directory phrasing is gone, and the payload states that choosing does not retire the other store.
  • test_the_wrong_socket_refusal_offers_a_matching_pair_not_an_adoption — both sockets appear in the recovery list and the note explains why there is no adoption.

Relay suite 792 passed / 30 skipped / 6 subtests. validate.py, contracts.py, check_operations_contract.py, unittest discover -s scripts/ci/tests, secrets.sh, git diff --check all exit 0. CRW_PACKAGES_TMPDIR=/var/tmp packages.py exits 0 — bridge 119 tests, relay 852.

Scope

Source and docs only, inside packages/codex-session-relay/. No behaviour change to delivery, the daemon, or the transport; the only runtime difference is the content of two refusal payloads that already existed.


Devin Review

…close

Six operator-visible refusal reasons landed with JUN-103 and none of them appear
anywhere in the package docs. An operator who hits ambiguous_state_directory has
the reason string, the candidate paths, and nothing to read. The issue's own
criteria ask that the selected database and the diagnostics be discoverable, so
this is the deliverable finishing rather than new scope.

operations.md gains two pieces. Under "Where the state lives", what happens when
the precedence rules land on something ambiguous: the three selection refusals,
when each fires, and the inspect-before-adopt recovery written as commands that
carry the socket. Two limits are stated rather than glossed - choosing one of two
claiming stores does not remove the ambiguity, so every participant has to pass
the same explicit --state until one store is retired; and the different-socket
refusal prints no recovery list because using a store does not rewrite the socket
it recorded. The doctor and ack-proof exemption is scoped to these guards, since
doctor still refuses an unproven same-store comparison.

Under "Ownership", the refusal vocabulary as a reason-to-action table, because the
distinctions decide the operator's next move: not_ours is a definite mismatch,
ownership_unverifiable is insufficient evidence and often resolves itself,
replaced_by_new_launch means the service is NOT stopped and the original launch
may already have been terminated, supervised_store_mismatch is not repaired by a
restart.

invariants.md corrects a row that claimed more than the code delivers. "stop and
disable hold the lock across the decision" omitted the handoff, so it now says
what is true - they decide under the lock and stop writes its record under it -
and three limits are recorded beneath it: lock_is_held reporting an unopenable
lock file as not held, disable and enable writing shared intent outside the lock
on the accept path, and start confirming from a record plus a separate probe.

Those three are the seam that produced a new finding in five consecutive review
rounds on PR #8. They are documented here rather than patched, because each
narrow fix so far revealed the next window and the seam wants one deliberate
revision.
The recovery list these refusals print is the whole of what an operator has to
work from, and it did not survive being read as instructions. It ended with
"--state <the directory above> once, to adopt it deliberately", which is not a
command, never says which directory, and silently drops the --socket that made
two stores candidates for each other in the first place. It also promised an
adoption that does not happen: choosing one of two claiming stores leaves both
still recording the socket, so the next invocation that relies on default
discovery is refused again, and every participant of that assignment needs the
same explicit directory until one store is retired. The refusal now says so.

The different-socket refusal printed no recovery at all. That one has nothing to
adopt, because using a store does not rewrite the socket it recorded, so it now
offers the two readings that identify the matching pair and states the reason
there is no third option.

Both lists are complete commands carrying the socket, and every one of them only
reads: provenance is written by opening a store, which is the thing the refusal
prevented, so inspection is safe to repeat. The program name comes from argv
rather than a constant, because the console script and python3 -m are both
ordinary ways to get here and a command naming the wrong one is a command the
operator has to translate.

Two regression tests pin the payload shape - every command carries the socket,
every candidate is inspected, the vanished "directory above" phrasing is gone,
and the contested case states that choosing does not retire. Both fail against
the previous payload.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-17T04:55:33.170068Z 6f25f73 New commits
🔒 Security Review Completed 2026-09-17T03:42:12.109949Z c7cfe42 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

devin-ai-integration[bot]

This comment was marked as resolved.

Review found both halves of the same mistake in the recovery commands this PR
added, and both are mine.

The paths went in unquoted. A state directory with a space splits into two
arguments; one carrying shell syntax runs it. The whole point of printing a
command is that it gets pasted, so this made following the refusal's own advice
the dangerous act. Every interpolated path is quoted now.

And _program discarded the directory. A console script that is not on PATH
rendered as a bare name, so pasting reached a different installation or nothing
at all. It keeps the directory when argv[0] has one.

The tests assert the property rather than the spelling: each command is put
through shlex.split and the path has to come back as one intact argument. That
matters because the first version of these assertions looked for "$(" and failed
on correctly quoted output, where the substitution is literal. Both fail against
the unquoted form, which splits a spaced path in two.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Review found four ways the refusal advice this branch added was wrong about its own
behaviour.

The socket-first line has no --state to override anything, so when
CODEX_SESSION_RELAY_STATE is what pinned the selection in the first place, pasting
'find the store that belongs to this socket' re-selected the store that had just
produced the refusal and returned the same error. It is printed with env -u now,
and only when the variable is actually set, so the ordinary case stays plain.

The module form named a bare python3. The relay may be running under a virtualenv
or a versioned interpreter that is not on PATH under that name, where the printed
line reaches a different installation or nothing at all. It names the running
interpreter, quoted.

And operations.md described code that no longer exists. It said the different-socket
refusal prints no recovery list, which stopped being true one commit later; the
point it was making - that there is nothing here to adopt - survives and is now made
about the list that is actually printed. It also claimed every lifecycle refusal is
returned before the command acts, directly contradicting the replaced_by_new_launch
row three lines above it, which says the launch may already have been terminated.
That one is a refusal to claim success, not a refusal to act, and the text says so.

Both code fixes were confirmed failing against the previous commit. The env-u
regression runs the printed command rather than matching its text.

Separately, and not from the review: StateDirectory did not pin HOME, so resolving a
socket's directory read the siblings next to the REAL one and opened the databases
this host actually runs on. Only their shared-memory index was touched, never the
databases or their WAL, but a unit test has no business reaching them at all. It now
pins HOME the way the Precedence class beside it already did. This predates the
branch; it is fixed here because it is what made the isolation evidence for this work
untrue.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c4cd41476

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/codex-session-relay/src/codex_session_relay/cli.py Outdated
The previous commit removed CODEX_SESSION_RELAY_STATE from the socket-first recovery
line whenever the variable was set. But --state wins over the variable, so it can
cause this refusal while the variable still points at the store that does record the
requested socket. Reproduced: with --state on a store recording another socket and
the variable on the store recording the wanted one, the printed line landed on
~/.local/state/codex-session-relay/39579c81447aa5d7 with exists=False, under a
caption promising the store that belongs to this socket. The variable would have
found it.

The prefix is now conditioned on the selection's own source rather than on the
variable merely being set, so it appears exactly in the case that motivated it.

Confirmed failing against the previous commit.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread packages/codex-session-relay/src/codex_session_relay/cli.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f25f73472

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/codex-session-relay/src/codex_session_relay/cli.py
Comment thread packages/codex-session-relay/src/codex_session_relay/cli.py
@thisisjun786
thisisjun786 merged commit 162d9fe into dev Sep 17, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant