Skip to content

Merge upstream/main: ask command + flush-race reconciliation - #28

Merged
usjoh merged 3 commits into
mainfrom
feat/upstream-sync-2026-07-29
Jul 29, 2026
Merged

Merge upstream/main: ask command + flush-race reconciliation#28
usjoh merged 3 commits into
mainfrom
feat/upstream-sync-2026-07-29

Conversation

@usjoh

@usjoh usjoh commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Brings the fork current with upstream/main (2 commits) and reconciles the headline conflict: two independent fixes to the same flush race.

The reconciliation — both layers kept, deliberately

Upstream a1a91b8 fixed the StateWriter temp-file race by switching both flush sites to atomic_write_json (per-writer mkstemp, last-replace-wins). Our fork fixed the same race on 2026-07-18 with _flush_lock serialization. These are complementary, not redundant:

  • Lock without unique names leaves a landmine: any future flush path that skips the lock re-arms the shared-.json.tmp FileNotFoundError crash (upstream measured it as a ~30% shutdown-test flake).
  • Unique names without the lock trade the crash for a silent ordering race: stop()'s join(timeout=2) can return with the writer thread mid-flush, and a stale in-flight snapshot can then overwrite the final report_ready=True state — Ringside polls that file.

Resolution: with self._flush_lock: atomic_write_json(...) at both sites, with the layered rationale in the comment. Upstream's new test_state_writer_concurrency.py passes unchanged under the combined form.

Also in this sync (deda7c1)

ringer.py ask — one bounded read-only question, one clean worker, an inlined context-packet selector — plus max_attempts/redact_spec manifest fields, tests/TESTING.md, and Python floor 3.11→3.12. Upstream's SKILL.md changes auto-merged cleanly around our expect_files bullet (verified present). MODEL-NOTES union-merged, both evidence sets kept, per house style.

Verified

Check Result
Full suite 317 tests, OK, 0 skips (was 282; upstream adds 35)
Upstream's concurrency tests under lock+atomic 3/3
Formerly ~30%-flaky shutdown tests 12/12 consecutive file runs
ask --help smoke works
All template + repo manifests lint clean
Local expect_files skill bullet survived automerge

🤖 Generated with Claude Code

justfinethanku and others added 3 commits July 28, 2026 18:32
…lready ship (NateBJones-Projects#83)

* Fold the useful half of the token-saver PR into the skill we already ship

PR NateBJones-Projects#82 proposed a second "token-saver" skill plus a pre-call HTTP gateway
(~7,200 lines). Most of it was either already enforced by ringer.py or
didn't work: the gateway requires your own API key, so it converts a
flat-rate OAuth plan into metered API billing, and its own docs say it is
incompatible with Claude Code. Ringer already gets the same saving in a
better shape — every task spawns a clean worker that sees only the spec,
opt-in and verified by an executed check.

What's kept:

- `ringer.py ask` — one bounded, read-only question answered by one clean
  worker over a selected context packet. This fills a real gap: such a
  question needs a model but not a manifest, and answering it inline pulls
  whole files into an already-expensive context. The packet selector is
  INLINED rather than added as a sibling module, so ringer.py stays
  single-file per CONTRIBUTING.
- `max_attempts` and `redact_spec` as general manifest fields.

What changed from NateBJones-Projects#82's version:

- Redaction is opt-in (`--redact`), not hardcoded on. NateBJones-Projects#82 redacted every
  `ask` call, which made those runs unreviewable.
- `ask` shows on Ringside and registers an artifact. NateBJones-Projects#82 suppressed both,
  contradicting the skill's own "runs are watched, not hidden" rule.
- A matching passage that overflows the packet budget now says so and
  names the byte figure to raise the cap to. It previously reported "no
  passage matched the request", sending the reader after a selection bug
  that did not exist.

The skill gains an `ask` section placed BELOW the four rules and labelled
as their one sanctioned exception, stating plainly that its check proves
an answer was produced, never that it is right — there is nothing to
execute against free-form prose. Plus a section on the orchestrator's own
token discipline, which the playbook never covered.

Supported Python floor moves 3.11 -> 3.12: CI has only ever run 3.12, so
3.11 was a promise nothing enforced.

Not carried over: the gateway, the pre-call router, the separate skill and
its duplicate `.agents/` tree, the installer, and the codex-thin engine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Close a symlink escape in ask's directory scan, and three review findings

Pre-merge review (GPT-5.6 Sol, high effort) returned DO NOT MERGE. Findings
confirmed by reproduction, and fixed here.

BLOCKER — directory scans could read outside the tree you named. Every
filename check in source_files ran on the DIRECTORY ENTRY, and only
afterwards was the path resolved and read. A symlink with a benign name
therefore pulled its target's contents into the packet, bypassing the
sensitive-filename filter entirely. Reproduced: inside/safe-notes.md ->
../outside/secret-private.txt put the secret straight into the packet that
would have been sent to a provider. Scans now require the resolved path to
stay inside the named tree and re-run the name checks on the real target.
An explicitly supplied file is exempt: naming it is consent.

Also fixed:

- The runtime guard still admitted Python 3.11 while the docs had moved to
  3.12. It now matches what we say and what CI runs.
- `ask` did not start Ringside, so a run could begin against a dark watch
  page — contradicting the prose added in the previous commit. It now calls
  ensure_hud_running like `run` does.
- `max_attempts` and `redact_spec` accepted truthy stand-ins: `1.5` became
  1 and silently removed the retry, and the string "false" became True.
  Both are now type-checked. Note `timeout_s` and `full_access` coerce the
  same way on main; that is pre-existing and left alone deliberately rather
  than changed under cover of this PR.

Two prose overclaims corrected. The README said `ask` refuses when nothing
matches; it does not — a source small enough to fit is included whole
regardless of relevance, and the docs now say so, because choosing the
sources is the real work. And redaction was described as keeping the
request "out of the log", when it covers Ringer's own state and eval
records only — raw worker output is captured verbatim by invariant, so a
worker that echoes its request still writes that text to worker.log.

Declined: the reviewer wanted relevance gating so `ask` would refuse
unrelated small sources. Including a small file wholesale is cheap and
more likely to answer; the defect was the documentation promising
otherwise, so the prose moved rather than the behaviour.

9 new tests, including the reproduced escape and its in-tree variant.
250 tests green across three consecutive runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es-Projects#84)

`test_sigterm_cleans_up_active_worker_and_finishes_state` and
`test_second_signal_during_shutdown_does_not_cancel_cleanup` failed about
30% of runs (measured 4/12 and 3/12 on main). Not timing sensitivity in
the tests — a real race in StateWriter.

StateWriter.flush wrote `<run>.json.tmp`: one fixed temp name shared by
every caller. The background writer thread flushes on a timer while
explicit flushes come from set_port, close, and the signal path, so two
flushes overlap routinely. Both wrote the same temp file; the first
os.replace consumed it and the second raised FileNotFoundError on a path
that no longer existed. That propagated to the top-level handler, which
exited 2 — so the test asserting 130 saw 2 and failed. The same fixed
name appeared again in the end-of-run re-flush.

Both sites now use atomic_write_json, already in the file and already
correct: tempfile.mkstemp gives each writer its own temp file, and the
final os.replace is atomic, so overlapping flushes are last-write-wins
instead of one of them crashing.

After the fix, 20/20 passes on each of the two tests, and 4 consecutive
clean full-suite runs. Before, the suite failed roughly every other run.

Verified the new tests actually catch it: run against the pre-fix commit,
test_parallel_flushes_never_lose_their_temp_file fails with exactly the
FileNotFoundError described above. The other two guard adjacent
invariants (no torn reads, no leaked temp files) and pass either way.

Left alone deliberately: _write_active_runs and write_settings use
`.{name}.{pid}.tmp`. They are called from the main thread at run start and
end, so the pid suffix is sufficient there, and there is no observed
defect to justify touching them in this change.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2026-07-29

# Conflicts:
#	docs/MODEL-NOTES.md
#	ringer.py
@usjoh
usjoh merged commit a2f133c into main Jul 29, 2026
2 of 3 checks passed
@usjoh
usjoh deleted the feat/upstream-sync-2026-07-29 branch July 29, 2026 18:35
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.

2 participants