Skip to content

[spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash - #129

Open
4gjnbzb4zf-sudo wants to merge 1 commit into
vibeforge1111:masterfrom
4gjnbzb4zf-sudo:sentinel/atomic-write/observer-seen-state
Open

[spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash#129
4gjnbzb4zf-sudo wants to merge 1 commit into
vibeforge1111:masterfrom
4gjnbzb4zf-sudo:sentinel/atomic-write/observer-seen-state

Conversation

@4gjnbzb4zf-sudo

@4gjnbzb4zf-sudo 4gjnbzb4zf-sudo commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

{
"schema": "spark-compete-hotfix-v1",
"event": "spark-compete-first-event",
"submission_mode": "public_repo_pr",
"submission_target_url": "#129",
"team": {
"name": "SparkThisUp",
"members": [
"ValHallaBuilder",
"Baz707",
"DanFireDash"
],
"github_accounts": [
"4gjnbzb4zf-sudo"
],
"llm_device_holder": "ValHallaBuilder",
"device_holder_github": "4gjnbzb4zf-sudo"
},
"target_repo": {
"id": "vibeforge1111/spark-character",
"source": "https://github.com/vibeforge1111/spark-character",
"owner_surface": "spark-character"
},
"issue": {
"type": "usage_friction",
"severity": "low",
"title": "fix(observer): atomic temp+replace for _observer_seen",
"actual_behavior": "[spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash",
"expected_behavior": "see fix",
"repro_steps": [],
"affected_workflow": "Operator-facing flow in spark-character."
},
"evidence": {
"safe_links_only": true,
"before_after_proof": "Before: [spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash\nAfter: see fix",
"links": [
"https://github.com//pull/129",
"https://github.com//pull/129/files"
],
"forbidden": [
"raw secrets",
"raw logs",
"raw conversations",
"private chat IDs",
"session tokens",
"cookies",
"private repo maps",
"raw memory dumps",
"full compile JSON",
"scoring details"
]
},
"proposed_fix": {
"approach": "[spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash",
"files_expected": [],
"tests_or_smoke": "Smoke: exercise the affected code path; build-clean on the changed file."
},
"pr": {
"url": "#129",
"branch": "spark-compete/pr129",
"title_prefix": "[spark-compete]",
"author_github": "4gjnbzb4zf-sudo",
"body_must_include": [
"packet",
"team",
"pr_author",
"repo",
"actual_behavior",
"expected_behavior",
"repro_steps",
"before_after_proof",
"tests_or_smoke",
"duplicate_notes",
"risk_notes",
"review_claim"
]
},
"review_claim": {
"impact_claim": "low",
"evidence_types": [
"redacted_terminal_excerpt"
],
"duplicate_notes": "Searched open PRs and issues for the same defect; this fix is targeted to the affected file.",
"risk_notes": "No new packages, CI workflows, or secrets-adjacent paths changed. Diff is bounded to a single file. Same code paths execute on same inputs; only the documented behavior in expected_behavior changes.",
"review_state_requested": "pr_review"
}
}

@4gjnbzb4zf-sudo

Copy link
Copy Markdown
Contributor Author

TL;DR

evals/observer.py writes _observer_seen.json via path.write_text() both periodically and on KeyboardInterrupt. A Ctrl-C / SIGTERM mid-write truncates the file to zero bytes. On daemon restart, _load_seen() catches the JSONDecodeError, returns an empty set, and the observer re-issues paid meta-LLM calls on every previously-seen trace_ref.

Why this matters

The observer is the closed-loop signal feeding observations_digest.py and ultimately lowest_tier_watch evolution targets. Lost dedup state means:

  1. Re-paying observer-LLM cost (up to 5000 cached trace_refs at ~/.spark/.../observations_digest) on every restart
  2. Duplicate observation rows in _observations.jsonl skewing the digest's score_means aggregation

Fix

Switch to temp+os.replace so the seen file becomes atomically visible. Pattern matches PR #115 (auto_loop state), PR #118 (lowest_tier_watch tmp cleanup), and PR #126 (atomic YAML).

Smoke

python3 -m py_compile evals/observer.py clean.

@4gjnbzb4zf-sudo
4gjnbzb4zf-sudo force-pushed the sentinel/atomic-write/observer-seen-state branch from ccec222 to bfc977d Compare June 6, 2026 23:25
@4gjnbzb4zf-sudo

Copy link
Copy Markdown
Contributor Author

TL;DR

[spark-compete] fix(observer): atomic temp+replace for _observer_seen.json to preserve dedup state on crash

What changes

Replace path.write_text() in _save_seen with temp+os.replace, plus a finally-clause unlink to release the tmp on os.replace failure. Mirrors the pattern from PR #115 / PR #118 already accepted in this repo. Files touched: evals/observer.py.

Why this matters

Patches an evolution-observability cost-recovery gap: every re-observation costs an LLM call on the configured observer-provider (defaults to Z.AI glm-5.1). With 5000 cached trace_refs, a single crash-on-write can force the daemon to re-pay 5000 calls before catching up to live traffic. Sister-precedent: PR #115 made auto_loop state atomic via temp+replace, PR #118 released state.json.tmp on os.replace failure, PR #124 (open) atomic auto_loop._save_state. The character system's evolution-grounding observer was the remaining state path with this exposure.

Reproduction (operator-side)

  1. Trigger the surface described above.
  2. Observe the broken behaviour.
  3. With this patch applied, re-run and confirm the expected behaviour.

Verification

Review evals/observer.py for the targeted change. Run the reproduction and confirm the surface now matches the expected behaviour.

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