Skip to content

fix(memory+guard): quarantine scan-rejected atoms and fix socket_path transport - #87

Merged
jkyberneees merged 1 commit into
mainfrom
fix/memory-scan-quarantine-guard-socket
Jul 22, 2026
Merged

fix(memory+guard): quarantine scan-rejected atoms and fix socket_path transport#87
jkyberneees merged 1 commit into
mainfrom
fix/memory-scan-quarantine-guard-socket

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Follow-ups to the PIGuard false-positive investigation in #85. Two independent fixes for Extended Memory reliability with the guard enabled.

Fix 1 — guard-rejected atoms go to quarantine instead of vanishing

Problem: addAtom ran the injection scan before quarantine routing and returned the error, so a guard rejection (false positive or not) silently destroyed the memory. The extractor also pre-scanned and dropped atoms at extraction time. With the sidecar in the loop, any false positive meant permanent, unreviewable memory loss — and even a correct rejection gave the operator no way to inspect what was blocked.

Change:

  • Scan rejections are routed to quarantine with a recorded reason (scan_rejected: ..., alongside tainted for untrusted source classes). odek memory extended quarantine now prints the reason per entry.
  • PromoteAtom skips the guard rescan — the human review IS the approval; a rescan would reject the same false positive again, making promotion of scan-rejected atoms impossible.
  • The extractor no longer pre-scans: the single scan gate is at persistence (addAtom), so per-turn extraction, tool adds, and programmatic adds all get identical quarantine-on-reject behavior.

Policy change to be aware of: previously a tainted atom matching an injection pattern was never stored anywhere (TestQuarantineScanBeforeStore); now it lands in quarantine (never in recall) pending human review. This is the intended trade-off — quarantine content is never recalled or injected into prompts; it only exists for CLI review and ages out via quarantine_ttl_days.

Fix 2 — guard.socket_path speaks the daemon's actual protocol

Problem: the piguard client dialed the configured Unix socket but sent HTTP POSTs, while the go-prompt-injection-guard daemon speaks newline-delimited JSON (one JSON object per line). Every scan errored (malformed HTTP response), and with fallback_to_local: true (the default) this silently degraded to local-only scanning — operators who configured socket_path had no semantic guard while believing they did.

Change: socket mode now forwards each request as one NDJSON line per call (same framing the vendored HTTP gateway uses), shares the response handling with HTTP mode via a common rpc path, and surfaces {"error": ...} daemon replies as errors instead of decoding them as empty classifications. docs/CONFIG.md updated (socket_path semantics, and the threshold row now documents the label-confidence semantics fixed in #85).

Verification

  • Unit: new TestScanRejectedAtomQuarantined (reason recorded, excluded from live store, promote restores), TestPromoteBypassesGuardRescan (guard rejecting everything, promote still succeeds), extractor pass-through tests, socket-mode tests with a fake NDJSON daemon (detect/batch/long, dial error).
  • E2E against the real daemon container (arm64 build from fix(docker): vendor arch-safe PIGuard daemon build for Apple Silicon #86): odek's guard.New client over the Unix socket — benign facts BENIGN/accepted, injections INJECTION at 0.9999/rejected, batch and long endpoints working.
  • go test ./... full suite green, -race green on guard+memory, golangci-lint 0 issues.

Stacked on top of #86 (daemon build) only for local testing; the diff is independent.

… transport

Two follow-ups to the PIGuard false-positive investigation (#85):

1. Scan-rejected atoms no longer vanish. The guard scan in addAtom ran
   before quarantine routing and hard-dropped rejections, so any guard
   false positive silently destroyed a legitimate memory with no review
   path. Rejections now go to quarantine with a recorded reason
   ("scan_rejected: ..." alongside "tainted"), visible via
   odek memory extended quarantine. PromoteAtom skips the guard rescan:
   the human review is the approval, and a rescan would reject the same
   false positive again. The extractor no longer pre-scans atoms - the
   single scan gate is at persistence, so extraction and manual adds get
   identical quarantine-on-reject behavior.

2. guard.socket_path actually works now. The piguard client dialed the
   Unix socket but spoke HTTP, while the go-prompt-injection-guard daemon
   speaks newline-delimited JSON - every scan errored and fallback_to_local
   silently degraded to local-only scanning. Socket mode now forwards the
   request as one NDJSON line per call (matching the daemon protocol),
   surfaces {"error":...} daemon replies, and is covered by fake-daemon
   socket tests plus an E2E run against the real daemon container
   (BENIGN facts pass, injections rejected, batch/long work).
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 06bfda4 Commit Preview URL

Branch Preview URL
Jul 22 2026, 08:21 AM

@jkyberneees
jkyberneees merged commit 4a319d1 into main Jul 22, 2026
8 checks passed
jkyberneees added a commit that referenced this pull request Jul 22, 2026
…y memory (#88)

Consolidates fixes for 21 audited bugs in the memory system.

Correctness:
- extended: scan-rejected atoms now go through enforceCap before
  quarantine (regression from #87: quarantine could exceed max_size_mb
  and wedge the store, since the evictor only evicts trusted atoms)
- extended: ReturnAfterBreak summarized the 5 OLDEST atoms (backwards
  iteration over a newest-first list) instead of the most recent
- extended: final recall union was re-sorted by pure RetentionScore,
  discarding the blended 0.6*similarity+0.4*retention score and the LLM
  rerank order; the union is now deduped by ID and sorted by composite
- extended: extracted atoms dedup by normalized text (refresh CreatedAt,
  keep higher confidence) instead of accumulating duplicates
- extended: eviction now removes association links (no dangling IDs),
  ForgetAtom works for quarantine-only atoms, UTF-8-safe truncation,
  quarantine TTL eviction no longer writes under a read lock
- loop: skill/episode recall no longer re-runs on every iteration when
  the result is empty (dedup key was only set on non-empty results)
- loop: per-message dedup keys reset between Run/RunWithMessages calls
  (a repeated identical REPL message previously skipped all memory hooks)
- legacy: merge_on_write no longer drops facts on 30-char prefix
  collisions (new index-based FactStore.ReplaceAt)
- legacy: episode Search query cache invalidated on write/promote/prune
- legacy: BuildSystemPrompt includes the extended user-state block even
  when legacy facts are empty, and caches the empty result
- legacy: LLM ranker's explicit 'none relevant' is honored instead of
  being overridden with unranked candidates
- legacy: Consolidate enforces the facts char cap and actually trims
- extended: fenced/preamble-wrapped LLM JSON (extractor, user-model
  inference, predictor) now parses instead of silently dropping output

Efficiency:
- extended: batch per-turn atom adds - one index rebuild per turn
  instead of one per atom, one assoc.Persist per batch
- extended: vector index reuses one embedder across rebuilds so the
  HTTP embedding cache actually warms (was a full-corpus re-embed over
  the network per added atom); same sharing for episode dedup + index
  via embedding.Shared (HTTP backend only; RP stays per-corpus)
- extended: recall fan-out cut from ~8 LLM calls to 2 (type filtering
  over the existing candidate set, no rerank for predicted intents)
- extended: one store load per recall query instead of a full atoms.json
  re-parse per candidate; AnaphoraResolve drops its duplicate search
- extended: per-turn extraction + anaphora run async via
  MemoryManager.RunBackground instead of blocking the ReAct loop
- legacy: session-end episode extraction/consolidation are tracked and
  drained with a bounded 15s wait in Agent.Close, so episodes survive
  CLI exit instead of dying with the process
- extended: background user-model inference guarded against overlap and
  pending-review duplicates

Tests: 30+ new tests across memory/extended/loop/embedding/odek; full
suite, -race on memory+loop+embedding, and repo-wide golangci-lint all
clean.
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