Skip to content

Commit cf5279e

Browse files
Jonathan Harrisonclaude
andcommitted
fix(memory): strip attached-file prefixes from cocoon queries
Dream cycle caught uploaded file content polluting cocoon wrapped.query (same class as the July 5 enriched-query incident, for file uploads). - extract_primary_user_query + new strip_attached_files helper: anchor on the LAST '--- End of File ---' so a file that itself contains the marker strings (e.g. an uploaded cocoon JSON) is handled correctly - codette_forge_bridge reuses the shared helper - utilities/clean_file_contaminated_cocoons.py: cleaned 29 historical cocoons (backups in cocoons/_backup_file_cleanup); 8 residual are a separate malformation (no end-marker / prompt-completion leakage) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2df9840 commit cf5279e

3 files changed

Lines changed: 123 additions & 1 deletion

File tree

inference/codette_forge_bridge.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,14 @@ def _extract_primary_user_query(query: str) -> str:
10081008
if not query:
10091009
return ""
10101010

1011+
# Attached-file blocks the server prepends as
1012+
# "--- Attached File: name (size) ---\n<content>\n--- End of File ---\n\n"
1013+
# (uploaded code/docs were polluting cocoon queries — the dream cycle
1014+
# caught 29 contaminated cocoons in one week, July 17 2026). Anchored
1015+
# on the LAST end marker so nested markers in an uploaded file are safe.
1016+
from codette_shared import strip_attached_files
1017+
query = strip_attached_files(query)
1018+
10111019
# Strip prepended bracketed blocks (may stack: coherence + constraints)
10121020
_block_re = re.compile(
10131021
r"^\[(?:COHERENCE ANCHORS|SESSION CONSTRAINTS)[^\]\n]*\]\s*\n"

inference/codette_shared.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,28 @@
187187
]
188188

189189

190+
_FILE_END_MARKER = "--- End of File ---"
191+
192+
193+
def strip_attached_files(query: str) -> str:
194+
"""Remove server-prepended attached-file blocks, returning the user message.
195+
196+
Server format: "--- Attached File: name (size) ---\n<content>\n--- End of
197+
File ---\n\n<user msg>", one or more blocks. Because an uploaded file can
198+
itself contain the marker strings (e.g. a cocoon JSON), we anchor on the
199+
LAST end marker rather than peeling non-greedily: if the query begins with
200+
a file block, the user's words are whatever follows the final end marker."""
201+
if query.lstrip().startswith("--- Attached File:") and _FILE_END_MARKER in query:
202+
return query[query.rindex(_FILE_END_MARKER) + len(_FILE_END_MARKER):].strip()
203+
return query
204+
205+
190206
def extract_primary_user_query(query: str) -> str:
191-
"""Strip server-injected memory sections before constraint extraction."""
207+
"""Strip server-injected file blocks + memory sections before constraint
208+
extraction. Memory sections are appended after a "\\n\\n---\\n" sentinel."""
192209
if not query:
193210
return ""
211+
query = strip_attached_files(query)
194212
sentinel = "\n\n---\n"
195213
if sentinel in query:
196214
return query.split(sentinel, 1)[0].strip()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python3
2+
"""One-off: strip attached-file prefixes from stored cocoon queries.
3+
4+
The dream cycle (July 17 2026) caught cocoons whose `wrapped.query` held
5+
uploaded file content instead of the user's actual message — the same
6+
disease class as the July 5 enriched-query incident, but for file uploads.
7+
The storage path is now fixed (extract_primary_user_query strips file
8+
blocks); this cleans the historical records.
9+
10+
Conservative: only rewrites a cocoon when extraction yields a NON-EMPTY
11+
query that differs from the stored one. File-only uploads (no accompanying
12+
message → empty extraction) are left untouched and reported, not blanked.
13+
Backs up every modified file to cocoons/_backup_file_cleanup/ first.
14+
15+
Usage:
16+
python utilities/clean_file_contaminated_cocoons.py --dry-run
17+
python utilities/clean_file_contaminated_cocoons.py
18+
"""
19+
import argparse
20+
import json
21+
import re
22+
import shutil
23+
import sys
24+
from pathlib import Path
25+
26+
_REPO = Path(__file__).resolve().parent.parent
27+
COCOON_DIR = _REPO / "cocoons"
28+
BACKUP_DIR = COCOON_DIR / "_backup_file_cleanup"
29+
30+
_FILE_END_MARKER = "--- End of File ---"
31+
32+
33+
def clean_query(q: str) -> str:
34+
# Anchor on the LAST end marker — an uploaded file can itself contain the
35+
# marker strings (e.g. a cocoon JSON), so non-greedy peeling breaks.
36+
if q.lstrip().startswith("--- Attached File:") and _FILE_END_MARKER in q:
37+
q = q[q.rindex(_FILE_END_MARKER) + len(_FILE_END_MARKER):]
38+
sentinel = "\n\n---\n"
39+
if sentinel in q:
40+
q = q.split(sentinel, 1)[0]
41+
return q.strip()
42+
43+
44+
def is_contaminated(q: str) -> bool:
45+
return isinstance(q, str) and (
46+
"--- Attached File:" in q or "--- End of File ---" in q)
47+
48+
49+
def main():
50+
ap = argparse.ArgumentParser()
51+
ap.add_argument("--dry-run", action="store_true")
52+
args = ap.parse_args()
53+
54+
cleaned = skipped_empty = 0
55+
if not args.dry_run:
56+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
57+
58+
for f in COCOON_DIR.glob("*.json"):
59+
try:
60+
d = json.loads(f.read_text(encoding="utf-8"))
61+
except Exception:
62+
continue
63+
w = d.get("wrapped")
64+
if not isinstance(w, dict):
65+
continue
66+
q = w.get("query")
67+
if not is_contaminated(q):
68+
continue
69+
70+
new_q = clean_query(q)
71+
if not new_q:
72+
skipped_empty += 1
73+
print(f" SKIP (file-only, no user msg): {f.name}")
74+
continue
75+
if new_q == q:
76+
continue
77+
78+
print(f" CLEAN {f.name}: {new_q[:70]!r}")
79+
if not args.dry_run:
80+
shutil.copy2(f, BACKUP_DIR / f.name)
81+
w["query"] = new_q
82+
# also fix v3 mirror if it duplicated the contaminated query
83+
v3 = d.get("v3")
84+
if isinstance(v3, dict) and is_contaminated(v3.get("query", "")):
85+
v3["query"] = new_q
86+
f.write_text(json.dumps(d, ensure_ascii=False, indent=2), encoding="utf-8")
87+
cleaned += 1
88+
89+
print(f"\n{'DRY-RUN: would clean' if args.dry_run else 'Cleaned'} {cleaned} cocoon(s); "
90+
f"{skipped_empty} file-only skipped.")
91+
if not args.dry_run and cleaned:
92+
print(f"Backups: {BACKUP_DIR}")
93+
94+
95+
if __name__ == "__main__":
96+
main()

0 commit comments

Comments
 (0)