[object Object]

← back to Rejected Prompts Viewer

feat: HOLE 2 — capture last 3 user turns as context so refusals to a lone 'go'/'do it' keep what was actually asked; v1.3.0

cfc28cd966d2a3f72b79f4ba95aff790cc6ac50d · 2026-08-19 12:20:45 -0700 · Steve Abrams

Files touched

Diff

commit cfc28cd966d2a3f72b79f4ba95aff790cc6ac50d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 12:20:45 2026 -0700

    feat: HOLE 2 — capture last 3 user turns as context so refusals to a lone 'go'/'do it' keep what was actually asked; v1.3.0
---
 package.json |  2 +-
 scan.py      | 18 ++++++++++++------
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/package.json b/package.json
index 24af2ef..e3048a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "rejected-prompts-viewer",
-  "version": "1.2.3",
+  "version": "1.3.0",
   "private": true,
   "description": "Web viewer of prompts Claude declined on copyright/trademark/website-ToS/IP grounds, mined from Claude Code transcripts.",
   "scripts": {
diff --git a/scan.py b/scan.py
index b66cbcf..ccbf506 100644
--- a/scan.py
+++ b/scan.py
@@ -15,8 +15,9 @@ PROJROOT = os.path.expanduser('~/.claude/projects')
 
 # Tunables — change these without hunting through logic below
 MAX_ASST_LEN  = 2500   # assistant turns longer than this are almost never bare refusals
-USER_TRUNCATE = 600    # chars stored for the user prompt
+USER_TRUNCATE = 900    # chars stored for the user context (raised for multi-turn — HOLE 2)
 ASST_TRUNCATE = 800    # chars stored for the assistant decline snippet
+USER_TURNS    = 3      # how many recent user turns to include as context (a lone "go"/"do it" loses what was asked)
 
 decline = re.compile(r"(I can'?t|I cannot|I won'?t|I'm not able|I am not able|I'm not going to|I'm not comfortable|I shouldn'?t|I have to decline|I'd rather not|not something I can|can'?t ethically|won'?t help|refus|decline to|not willing to|in good conscience)", re.IGNORECASE)
 reason = re.compile(r"(copyright|trademark|intellectual property|terms of service|\bToS\b|their terms|against .{0,20}terms|scrap\w* (without|permission|their)|circumvent|bypass\w* (the )?(bot|captcha|paywall|rate.?limit|detection|login|auth)|unauthorized access|impersonat|counterfeit|\bDMCA\b|someone else'?s (brand|work|design|content)|protected work|passing off|knockoff|knock-off|rip.?off .{0,15}brand|clone .{0,15}(brand|site|store))", re.IGNORECASE)
@@ -42,7 +43,7 @@ for f in glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True):
             'rejected.agentabrams', 'refusal_cands', 'refusal_classified', 'qwen3.8-27b-heretic')):
         continue
     lines = raw.splitlines()
-    prev_user = None
+    recent_users = []  # rolling last-USER_TURNS user turns for context (HOLE 2)
     for l in lines:
         if '"user"' not in l and '"assistant"' not in l:
             continue
@@ -55,11 +56,14 @@ for f in glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True):
             continue
         if t == 'user':
             c = m.get('content')
+            txt = None
             if isinstance(c, str):
-                prev_user = c
+                txt = c
             elif isinstance(c, list):
                 txt = ' '.join(x.get('text', '') for x in c if isinstance(x, dict) and x.get('type') == 'text')
-                prev_user = txt or prev_user
+            if txt and txt.strip():
+                recent_users.append(txt.strip())
+                del recent_users[:-USER_TURNS]  # keep only the last USER_TURNS
         elif t == 'assistant':
             for it in (m.get('content') or []):
                 if isinstance(it, dict) and it.get('type') == 'text':
@@ -70,14 +74,16 @@ for f in glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True):
                     netB = strong.search(tx) and not capability.search(tx)
                     if not (netA or netB):
                         continue
-                    if META.search(tx) or META.search(prev_user or ''):  # skip this build's own meta-chatter
+                    # multi-turn context, oldest->newest; keep the most-recent tail if over budget
+                    user_ctx = '  ⏎  '.join(recent_users)[-USER_TRUNCATE:]
+                    if META.search(tx) or META.search(user_ctx):  # skip this build's own meta-chatter
                         continue
                     key = (os.path.basename(f), tx[:200])
                     if key in seen:
                         continue
                     seen.add(key)
                     cands.append({'file': os.path.basename(f), 'ts': d.get('timestamp'),
-                                  'user': (prev_user or '')[:USER_TRUNCATE], 'refusal': tx[:ASST_TRUNCATE],
+                                  'user': user_ctx, 'refusal': tx[:ASST_TRUNCATE],
                                   'net': 'A' if netA else 'B'})
                     break
 

← 1a42d25 data: refresh to 90 candidates (4 recovered from HOLE 1 fix)  ·  back to Rejected Prompts Viewer  ·  auto-data-snapshot: 2026-08-19T12:22:10 (1 data files) — __p 5d3f8ff →