Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .agent/harness/hooks/_episodic_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Cross-platform locked append for episodic JSONL writes.

POSIX `write(2)` in O_APPEND mode is atomic for payloads up to PIPE_BUF
(4 KB on Linux, 512 B minimum per POSIX). Most episodic entries fit,
but failure entries with reflection + context + detail can exceed that,
and two harness hooks writing from the same process (or from two Pi
sessions on the same repo) can interleave bytes mid-line. Silent
corruption is worse than a visible error because every downstream
reader (`auto_dream.py`, `cluster.py`, `context_budget.py`,
`show.py`) skips `JSONDecodeError` lines without surfacing the loss.

This module serializes appends with `fcntl.flock(LOCK_EX)` on POSIX.
On platforms without `fcntl` (native Windows Python) the lock is a
no-op and behavior matches the pre-lock baseline. WSL, git-bash via
Cygwin, macOS, and Linux all provide `fcntl`.
"""
import json
import os

try:
import fcntl # POSIX
_HAVE_FLOCK = True
except ImportError:
_HAVE_FLOCK = False


def append_jsonl(path: str, entry: dict) -> dict:
"""Serialize `entry` to one JSON line and append to `path`.

Uses `open(..., "ab")` (append-binary) to bypass Python's text-mode
buffering and guarantee a single `write(2)` per call. `fcntl.flock`
provides cross-process mutual exclusion on POSIX.
"""
payload = (json.dumps(entry) + "\n").encode("utf-8")
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 👍 / 👎.

try:
f.write(payload)
f.flush()
finally:
if _HAVE_FLOCK:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
return entry
6 changes: 2 additions & 4 deletions .agent/harness/hooks/on_failure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Failures are learning. High pain score + rewrite flag after repeat offenses."""
import json, datetime, os
from ._provenance import build_source
from ._episodic_io import append_jsonl

ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
EPISODIC = os.path.join(ROOT, "memory/episodic/AGENT_LEARNINGS.jsonl")
Expand Down Expand Up @@ -69,7 +70,4 @@ def on_failure(skill_name, action, error, context="", confidence=0.9,
f"Flag for rewrite."
)
entry["pain_score"] = 10
os.makedirs(os.path.dirname(EPISODIC), exist_ok=True)
with open(EPISODIC, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry
return append_jsonl(EPISODIC, entry)
8 changes: 3 additions & 5 deletions .agent/harness/hooks/post_execution.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Runs after every action. Appends a structured entry to episodic memory."""
import json, datetime, os
import datetime, os
from ._provenance import build_source
from ._episodic_io import append_jsonl

ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
EPISODIC = os.path.join(ROOT, "memory/episodic/AGENT_LEARNINGS.jsonl")
Expand All @@ -15,7 +16,6 @@ def log_execution(skill_name, action, result, success, reflection="",
a higher value (e.g. 5) for high-importance successful operations so
recurring patterns cross the dream-cycle promotion threshold (7.0).
"""
os.makedirs(os.path.dirname(EPISODIC), exist_ok=True)
if pain_score is None:
pain_score = 2 if success else 7
entry = {
Expand All @@ -31,6 +31,4 @@ def log_execution(skill_name, action, result, success, reflection="",
"source": build_source(skill_name),
"evidence_ids": list(evidence_ids) if evidence_ids else [],
}
with open(EPISODIC, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry
return append_jsonl(EPISODIC, entry)
28 changes: 22 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,31 @@ case "$ADAPTER" in
echo " + AGENTS.md"
fi
mkdir -p "$TARGET/.pi"
# symlink .pi/skills -> .agent/skills so pi sees the one true skill tree.
# ln -sfn atomically replaces an existing symlink; fall back to cp -R
# on filesystems that don't support symlinks (e.g. Windows without dev mode).
# Keep .pi/skills in sync with .agent/skills (the one true skill tree).
# Handle three shapes explicitly: `ln -sfn src dest` against a REAL
# directory silently creates `dest/<basename-of-src>` INSIDE the dir
# on macOS/Linux and exits 0, which would leave stale orphans forever.
# Check -L before -d so existing symlinks take the cheap repoint path,
# and reserve the rsync path for real dirs left from a prior copy
# fallback install.
SKILLS_SRC="$(cd "$TARGET/.agent/skills" && pwd)"
if ln -sfn "$SKILLS_SRC" "$TARGET/.pi/skills" 2>/dev/null; then
SKILLS_DEST="$TARGET/.pi/skills"
if [[ -L "$SKILLS_DEST" ]]; then
ln -sfn "$SKILLS_SRC" "$SKILLS_DEST"
echo " + .pi/skills -> $SKILLS_SRC"
elif [[ -d "$SKILLS_DEST" ]]; then
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "$SKILLS_SRC/" "$SKILLS_DEST/"
echo " ~ synced .agent/skills → .pi/skills (rsync --delete)"
else
rm -rf "$SKILLS_DEST"
cp -R "$SKILLS_SRC" "$SKILLS_DEST"
echo " ~ replaced .pi/skills with current .agent/skills (no rsync)"
fi
elif ln -sfn "$SKILLS_SRC" "$SKILLS_DEST" 2>/dev/null; then
echo " + .pi/skills -> $SKILLS_SRC"
else
rm -rf "$TARGET/.pi/skills"
cp -R "$SKILLS_SRC" "$TARGET/.pi/skills"
cp -R "$SKILLS_SRC" "$SKILLS_DEST"
echo " + .pi/skills (copy; symlink not supported here)"
fi
;;
Expand Down