Skip to content

harden: lock episodic writes + fix pi install.sh silent orphan - #19

Merged
codejunkie99 merged 1 commit into
masterfrom
fix/episodic-write-locking
Apr 23, 2026
Merged

harden: lock episodic writes + fix pi install.sh silent orphan#19
codejunkie99 merged 1 commit into
masterfrom
fix/episodic-write-locking

Conversation

@codejunkie99

Copy link
Copy Markdown
Owner

Two shared-infrastructure bugs, separate from PRs #16 and #17 because they predate both.

Summary

1. AGENT_LEARNINGS.jsonl concurrent writespost_execution.py and on_failure.py both do plain open("a") + f.write(). POSIX O_APPEND is atomic only up to PIPE_BUF (4 KB on Linux/macOS). The reflection field in log_execution is uncapped and can exceed 4 KB on high-importance failure logs. Every downstream reader (auto_dream.py, cluster.py, context_budget.py, show.py) skips JSONDecodeError lines silently, so one over-PIPE_BUF interleave = one episodic entry gone with no signal. New _episodic_io.append_jsonl() helper: append-binary open + fcntl.flock(LOCK_EX). Cross-process mutex, no new deps. Fall back to unlocked append on platforms without fcntl (native Windows Python; WSL/git-bash/macOS/Linux all have it).

2. pi install.sh silently leaves stale skills on re-installln -sfn src dest where dest is a real directory (from a prior copy-fallback install) silently creates dest/<basename-of-src> INSIDE the dir and exits 0. The old if ln -sfn; then branch took the success path, the rm -rf + cp fallback never ran, orphans stuck around forever. Confirmed on macOS. Fixed by checking -L / -d explicitly before ln -sfn, mirroring the pattern I used in the codex adapter (PR #16 follow-up).

Test plan

  • 40 concurrent writers × 500 × 2 KB entries → 20,000 parseable lines, 0 corruption
  • pi re-install over a real-dir .pi/skills with an injected orphan skill → orphan deleted via rsync --delete (rm+cp fallback if rsync unavailable)
  • pi fresh install unchanged (symlink path)
  • pi re-install over existing symlink unchanged (cheap repoint)

Scope notes

  • No change to downstream readers; their JSONDecodeError tolerance stays, but with the lock there should be no corrupted lines for them to skip.
  • Windows users without fcntl get the same behavior they had before (no regression).

Two pre-existing infrastructure bugs flagged during the PR #17 cross-model
review, fixed here against master because they predate that PR and
affect every harness.

## 1. Concurrent writes to AGENT_LEARNINGS.jsonl

post_execution.py and on_failure.py both did plain `open(EPISODIC, "a")`
→ `f.write(json.dumps(entry) + "\n")`. POSIX O_APPEND makes single
`write(2)` calls atomic only up to PIPE_BUF (4 KB on Linux/macOS).

In practice most entries stay under that ceiling and the unlocked code
never corrupts, but the `reflection` field is uncapped in log_execution
and can easily exceed 4 KB on high-importance failure logs. Every
downstream reader (auto_dream.py, cluster.py, context_budget.py,
show.py) skips `json.JSONDecodeError` lines silently — so one
over-PIPE_BUF interleave = one episodic entry gone with no signal.

Fix: new `_episodic_io.append_jsonl()` helper that opens in append-
binary mode (no Python text-mode buffering quirks) and wraps the
write in `fcntl.flock(LOCK_EX)`. Shared by both writers. On platforms
without fcntl (native Windows Python) behavior falls back to the
pre-fix unlocked append; WSL, git-bash/Cygwin, macOS, Linux all have
fcntl.

Verified: 40 concurrent writers × 500 entries × 2 KB reflection each
→ 20,000 parseable lines, zero corruption.

## 2. pi install.sh silently leaves stale skills on re-install

`ln -sfn src dest` where `dest` is a REAL directory (e.g. from an
earlier copy-fallback install) silently creates `dest/<basename-of-src>`
INSIDE the dir and exits 0. The existing `if ln -sfn; then` branch
took the success path, the `rm -rf + cp` fallback never ran, and
orphans stuck around forever. Verified on macOS, confirmed the
symlink-inside-dir behavior.

Fix: check `-L` (symlink) and `-d` (real dir) explicitly before
calling `ln -sfn`, mirror the pattern used by the codex adapter
(PR #16 follow-up). Existing symlink → cheap repoint. Real directory
→ rsync --delete when available, rm+cp otherwise. Non-existent →
symlink or copy fallback. Same three-branch shape, no more silent
wrong behavior.

Verified: re-install after orphan-skill was added to a real-dir
`.pi/skills` → rsync --delete removes the orphan.

@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: 90d77d9e38

ℹ️ 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".

os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "ab") as f:
if _HAVE_FLOCK:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Catch flock errors and fall back to unlocked append

append_jsonl only falls back when fcntl is missing, but fcntl.flock(...) can still raise OSError on filesystems that do not support advisory locks (for example, some network or virtual mounts). In that case both log_execution and on_failure will raise and can abort the hook path instead of just recording without locking, which is a regression from the previous append-only behavior.

Useful? React with 👍 / 👎.

@codejunkie99
codejunkie99 merged commit 358ceff into master Apr 23, 2026
@codejunkie99
codejunkie99 deleted the fix/episodic-write-locking branch April 23, 2026 10:09
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