panopticon-cli 1.0.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/LICENSE +21 -21
  2. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/PKG-INFO +2 -3
  3. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/README.md +43 -44
  4. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/__init__.py +11 -11
  5. panopticon_cli-1.1.1/panopticon/cli.py +278 -0
  6. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/memory.py +60 -56
  7. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/observer.py +74 -64
  8. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/policies.py +68 -53
  9. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon/sentinel.py +143 -122
  10. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/PKG-INFO +2 -3
  11. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/pyproject.toml +47 -43
  12. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_cli.py +15 -14
  13. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_memory.py +38 -29
  14. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_observer.py +37 -34
  15. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_policies.py +47 -35
  16. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/tests/test_sentinel.py +33 -32
  17. panopticon_cli-1.0.0/panopticon/cli.py +0 -164
  18. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/SOURCES.txt +0 -0
  19. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/dependency_links.txt +0 -0
  20. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/entry_points.txt +0 -0
  21. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/requires.txt +0 -0
  22. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/panopticon_cli.egg-info/top_level.txt +0 -0
  23. {panopticon_cli-1.0.0 → panopticon_cli-1.1.1}/setup.cfg +0 -0
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Ak
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: panopticon-cli
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: The Cognitive Immune System for Autonomous CLI Agents.
5
5
  Author: Ak
6
6
  License: MIT
@@ -33,10 +33,9 @@ Dynamic: license-file
33
33
  > **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
34
34
 
35
35
  [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
36
- [![PyPI Downloads](https://static.pepy.tech/badge/panopticon-cli)](https://pypi.org/project/panopticon-cli/)
37
36
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![CI](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
38
38
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
39
- [![CI](https://github.com/ak/panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak/panopticon/actions)
40
39
 
41
40
  Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
42
41
 
@@ -1,44 +1,43 @@
1
- # PANOPTICON 👁️
2
-
3
- > **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
4
-
5
- [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
6
- [![PyPI Downloads](https://static.pepy.tech/badge/panopticon-cli)](https://pypi.org/project/panopticon-cli/)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
9
- [![CI](https://github.com/ak/panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak/panopticon/actions)
10
-
11
- Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
12
-
13
- **Panopticon** is a zero-latency, non-blocking wrapper that silently watches your agent's terminal output. When the AI inevitably tries to do something incredibly stupid, Panopticon's **State Guillotine** drops.
14
-
15
- It intercepts the rogue process, uses a Meta-Agent to figure out why your AI is crying, and **forcefully injects the correction directly into the agent's live `stdin` stream** like a disappointed senior developer taking over the keyboard. Oh, and it saves that failure to a SQLite database so the agent never makes the exact same mistake twice.
16
-
17
- ## Why you need this
18
-
19
- - **Native TTY Wrapping:** Slap it in front of any CLI. Loading spinners, ANSI colors, and interactive prompts still work perfectly. We just spy on them natively.
20
- - **The Policy Cascade (Iron Dome):**
21
- - **Level 1 (Blacklist):** Instant, zero-cost kills for destructive actions. Because your AI *will* try to drop your production database eventually.
22
- - **Level 2 (Fuzzy Heuristics):** Zero-cost math thresholds that catch repetitive loops. Stops the agent from running `cat missing_file.py` 300 times in a row.
23
- - **Level 3 (Universal Semantic Logic):** Streams a sliding-window of the terminal to a Meta-Agent (Claude, GPT, or Gemini) to judge your sub-agent's poor life choices.
24
- - **True Live Injection:** We don't just kill the agent and leave you hanging. Panopticon literally types the course-correction prompt into the interactive terminal for you, saving the session.
25
- - **Persistent Semantic Memory:** AI agents have goldfish memory. We use SQLite and Jaccard keyword routing to permanently scar them with their past failures so they actually learn.
26
-
27
- ## Quick Start
28
-
29
- ```bash
30
- # Install globally from PyPI
31
- pip install panopticon-cli
32
-
33
- # Export your preferred API Key (Panopticon dynamically routes to whatever you actually pay for)
34
- export OPENAI_API_KEY="sk-..."
35
- # OR export ANTHROPIC_API_KEY="..."
36
- # OR export GEMINI_API_KEY="..."
37
-
38
- # Slap it in front of your agent!
39
- panopticon claude
40
- # OR
41
- panopticon agy
42
- ```
43
-
44
- See [USAGE.md](USAGE.md) for deeper configuration so you can start dropping the Guillotine.
1
+ # PANOPTICON 👁️
2
+
3
+ > **The Cognitive Immune System (and glorified babysitter) for Autonomous CLI Agents.**
4
+
5
+ [![PyPI version](https://badge.fury.io/py/panopticon-cli.svg)](https://pypi.org/project/panopticon-cli/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![CI](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml/badge.svg)](https://github.com/ak495867/Panopticon/actions/workflows/ci.yml)
8
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
9
+
10
+ Autonomous CLI agents (like Claude Code, AutoGPT, and Antigravity) are amazing... right until they hallucinate, burn through $50 of your API credits, or confidently try to `rm -rf /` your hard drive.
11
+
12
+ **Panopticon** is a zero-latency, non-blocking wrapper that silently watches your agent's terminal output. When the AI inevitably tries to do something incredibly stupid, Panopticon's **State Guillotine** drops.
13
+
14
+ It intercepts the rogue process, uses a Meta-Agent to figure out why your AI is crying, and **forcefully injects the correction directly into the agent's live `stdin` stream** like a disappointed senior developer taking over the keyboard. Oh, and it saves that failure to a SQLite database so the agent never makes the exact same mistake twice.
15
+
16
+ ## Why you need this
17
+
18
+ - **Native TTY Wrapping:** Slap it in front of any CLI. Loading spinners, ANSI colors, and interactive prompts still work perfectly. We just spy on them natively.
19
+ - **The Policy Cascade (Iron Dome):**
20
+ - **Level 1 (Blacklist):** Instant, zero-cost kills for destructive actions. Because your AI *will* try to drop your production database eventually.
21
+ - **Level 2 (Fuzzy Heuristics):** Zero-cost math thresholds that catch repetitive loops. Stops the agent from running `cat missing_file.py` 300 times in a row.
22
+ - **Level 3 (Universal Semantic Logic):** Streams a sliding-window of the terminal to a Meta-Agent (Claude, GPT, or Gemini) to judge your sub-agent's poor life choices.
23
+ - **True Live Injection:** We don't just kill the agent and leave you hanging. Panopticon literally types the course-correction prompt into the interactive terminal for you, saving the session.
24
+ - **Persistent Semantic Memory:** AI agents have goldfish memory. We use SQLite and Jaccard keyword routing to permanently scar them with their past failures so they actually learn.
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Install globally from PyPI
30
+ pip install panopticon-cli
31
+
32
+ # Export your preferred API Key (Panopticon dynamically routes to whatever you actually pay for)
33
+ export OPENAI_API_KEY="sk-..."
34
+ # OR export ANTHROPIC_API_KEY="..."
35
+ # OR export GEMINI_API_KEY="..."
36
+
37
+ # Slap it in front of your agent!
38
+ panopticon claude
39
+ # OR
40
+ panopticon agy
41
+ ```
42
+
43
+ See [USAGE.md](USAGE.md) for deeper configuration so you can start dropping the Guillotine.
@@ -1,11 +1,11 @@
1
- from .observer import PanopticonObserver
2
- from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
3
- from .memory import PersistentMemory
4
-
5
- __all__ = [
6
- "PanopticonObserver",
7
- "AdversarialLogicCheck",
8
- "AntiLoopPolicy",
9
- "BlacklistPolicy",
10
- "PersistentMemory"
11
- ]
1
+ from .observer import PanopticonObserver
2
+ from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
3
+ from .memory import PersistentMemory
4
+
5
+ __all__ = [
6
+ "PanopticonObserver",
7
+ "AdversarialLogicCheck",
8
+ "AntiLoopPolicy",
9
+ "BlacklistPolicy",
10
+ "PersistentMemory",
11
+ ]
@@ -0,0 +1,278 @@
1
+ import subprocess
2
+ import threading
3
+ import sys
4
+ import time
5
+ import argparse
6
+ import re
7
+ import os
8
+ import signal
9
+ import codecs
10
+ import atexit
11
+ from typing import List
12
+ from .observer import PanopticonObserver, InterventionException
13
+ from .policies import AdversarialLogicCheck, AntiLoopPolicy, BlacklistPolicy
14
+
15
+
16
+ def strip_ansi(text: str) -> str:
17
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
18
+ return ansi_escape.sub("", text)
19
+
20
+
21
+ class CLIWrapper:
22
+ def __init__(self, command: List[str]):
23
+ self.command = command
24
+ target_agent = self.command[0] if self.command else "unknown"
25
+
26
+ self.observer = PanopticonObserver(
27
+ policies=[
28
+ BlacklistPolicy(forbidden_patterns=["rm -rf /", "DROP TABLE"]),
29
+ AntiLoopPolicy(window=3, threshold=0.85),
30
+ AdversarialLogicCheck(target_agent=target_agent),
31
+ ]
32
+ )
33
+
34
+ self.process = None
35
+ self.buffer = []
36
+ self.running = False
37
+ self.lock = threading.Lock()
38
+ self.threads = []
39
+
40
+ def run(self):
41
+ self.running = True
42
+ env = os.environ.copy()
43
+ env["PYTHONUNBUFFERED"] = "1"
44
+ env["FORCE_COLOR"] = "1"
45
+ env["CLICOLOR_FORCE"] = "1"
46
+
47
+ print(f"[PANOPTICON] Booting Immune System for: {' '.join(self.command)}")
48
+ print(
49
+ f"[PANOPTICON] Policies Active: Blacklist, AntiLoop (Fuzzy), AdversarialLogic"
50
+ )
51
+
52
+ # Flaw 2 Fix: Removing text=True to read raw binary bytes
53
+ self.process = subprocess.Popen(
54
+ self.command,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.STDOUT,
57
+ stdin=subprocess.PIPE,
58
+ bufsize=0,
59
+ env=env,
60
+ )
61
+
62
+ t_out = threading.Thread(target=self._read_stdout, daemon=True)
63
+ t_out.start()
64
+ self.threads.append(t_out)
65
+
66
+ t_in = threading.Thread(target=self._forward_stdin, daemon=True)
67
+ t_in.start()
68
+ self.threads.append(t_in)
69
+
70
+ t_eval = threading.Thread(target=self._eval_loop, daemon=True)
71
+ t_eval.start()
72
+ self.threads.append(t_eval)
73
+
74
+ # Register cleanup on exit
75
+ atexit.register(self._cleanup)
76
+
77
+ try:
78
+ self.process.wait()
79
+ finally:
80
+ self.running = False
81
+ self._cleanup()
82
+
83
+ def _cleanup(self):
84
+ """Cleanly shutdown all threads and resources."""
85
+ self.running = False
86
+
87
+ # Close subprocess pipes
88
+ if self.process:
89
+ try:
90
+ if self.process.stdin and not self.process.stdin.closed:
91
+ self.process.stdin.close()
92
+ except Exception:
93
+ pass
94
+
95
+ try:
96
+ if self.process.stdout and not self.process.stdout.closed:
97
+ self.process.stdout.close()
98
+ except Exception:
99
+ pass
100
+
101
+ try:
102
+ if self.process.poll() is None:
103
+ self.process.terminate()
104
+ try:
105
+ self.process.wait(timeout=2)
106
+ except subprocess.TimeoutExpired:
107
+ self.process.kill()
108
+ except Exception:
109
+ pass
110
+
111
+ # Wait for threads with timeout
112
+ for thread in self.threads:
113
+ if thread.is_alive():
114
+ thread.join(timeout=1)
115
+
116
+ def _forward_stdin(self):
117
+ """Forward stdin to subprocess, with non-blocking checks."""
118
+ try:
119
+ while self.running and self.process and self.process.poll() is None:
120
+ try:
121
+ # Windows-safe non-blocking stdin read
122
+ if sys.platform == "win32":
123
+ # On Windows, use a small sleep to avoid 100% CPU
124
+ time.sleep(0.05)
125
+ # Try non-blocking read using a different approach
126
+ if sys.stdin and not sys.stdin.closed:
127
+ try:
128
+ # This will still block, but with timeout via select on Unix
129
+ line = sys.stdin.buffer.readline()
130
+ if line:
131
+ if self.process.stdin and not self.process.stdin.closed:
132
+ self.process.stdin.write(line)
133
+ self.process.stdin.flush()
134
+ else:
135
+ break # EOF
136
+ except (EOFError, OSError, ValueError):
137
+ break
138
+ else:
139
+ # Unix/Linux: use select for non-blocking check
140
+ import select
141
+
142
+ ready, _, _ = select.select([sys.stdin], [], [], 0.1)
143
+ if ready:
144
+ line = sys.stdin.buffer.readline()
145
+ if line:
146
+ if self.process.stdin and not self.process.stdin.closed:
147
+ self.process.stdin.write(line)
148
+ self.process.stdin.flush()
149
+ else:
150
+ break # EOF
151
+ except (EOFError, OSError, ValueError, BrokenPipeError):
152
+ break
153
+ except Exception:
154
+ pass
155
+ finally:
156
+ try:
157
+ if self.process and self.process.stdin and not self.process.stdin.closed:
158
+ self.process.stdin.close()
159
+ except Exception:
160
+ pass
161
+
162
+ def _read_stdout(self):
163
+ # Flaw 2 Fix: Safely decode multi-byte UTF-8 emojis incrementally
164
+ decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
165
+ try:
166
+ while self.running and self.process and self.process.poll() is None:
167
+ try:
168
+ byte_chunk = self.process.stdout.read(1)
169
+ if not byte_chunk:
170
+ break
171
+
172
+ # Mirror raw bytes to actual terminal seamlessly
173
+ sys.stdout.buffer.write(byte_chunk)
174
+ sys.stdout.buffer.flush()
175
+
176
+ char_str = decoder.decode(byte_chunk)
177
+ if char_str:
178
+ with self.lock:
179
+ self.buffer.append(char_str)
180
+ except (OSError, ValueError, BrokenPipeError):
181
+ break
182
+ except Exception:
183
+ pass
184
+ finally:
185
+ try:
186
+ if self.process and self.process.stdout and not self.process.stdout.closed:
187
+ self.process.stdout.close()
188
+ except Exception:
189
+ pass
190
+
191
+ def _eval_loop(self):
192
+ while self.running and self.process and self.process.poll() is None:
193
+ time.sleep(15)
194
+
195
+ with self.lock:
196
+ if not self.buffer:
197
+ continue
198
+ full_text = "".join(self.buffer)
199
+
200
+ # Flaw 1 Fix: Sliding window overlap to prevent bridging blindness
201
+ overlap = full_text[-100:] if len(full_text) > 100 else ""
202
+ self.buffer.clear()
203
+ if overlap:
204
+ self.buffer.append(overlap)
205
+
206
+ clean_text = strip_ansi(full_text)
207
+ self.observer.log_action(
208
+ "CLI_Agent", clean_text, "cli_execution", len(clean_text) // 4
209
+ )
210
+
211
+ try:
212
+ self.observer._evaluate_state("CLI_Agent")
213
+ except InterventionException as e:
214
+ print(f"\n\n[PANOPTICON GUILLOTINE TRIGGERED]")
215
+ print(f"[REASON]: {e.args[0]}")
216
+ print(
217
+ f"[LIVE INJECTION]: Interrupting agent and injecting correction...\n"
218
+ )
219
+
220
+ # Flaw 1 Fix: The "Unkillable Zombie" SIGINT Interrupt
221
+ try:
222
+ if sys.platform != "win32":
223
+ self.process.send_signal(signal.SIGINT)
224
+ # Give the agent a split second to catch the interrupt before writing to stdin
225
+ time.sleep(0.5)
226
+ except Exception:
227
+ pass
228
+
229
+ try:
230
+ # Write the injection payload as binary
231
+ if self.process.stdin and not self.process.stdin.closed:
232
+ payload = (e.course_correction + "\n").encode("utf-8")
233
+ self.process.stdin.write(payload)
234
+ self.process.stdin.flush()
235
+ except Exception as ex:
236
+ print(
237
+ f"[ERROR] Live injection failed or agent deadlocked: {ex}. Hard terminating."
238
+ )
239
+ self.process.terminate()
240
+ self.running = False
241
+ break
242
+
243
+
244
+ def main():
245
+ try:
246
+ from dotenv import load_dotenv
247
+
248
+ load_dotenv()
249
+ except ImportError:
250
+ pass
251
+
252
+ parser = argparse.ArgumentParser(
253
+ description="Panopticon: Production-Grade Immune System for AI CLIs"
254
+ )
255
+ parser.add_argument(
256
+ "command", nargs=argparse.REMAINDER, help="The command to run, e.g., 'claude'"
257
+ )
258
+ args = parser.parse_args()
259
+
260
+ if not args.command:
261
+ print("Usage: panopticon [command]")
262
+ sys.exit(1)
263
+
264
+ wrapper = CLIWrapper(args.command)
265
+ try:
266
+ wrapper.run()
267
+ except KeyboardInterrupt:
268
+ print("\n[PANOPTICON] Interrupted by user. Cleaning up...")
269
+ wrapper._cleanup()
270
+ sys.exit(0)
271
+ except Exception as e:
272
+ print(f"[PANOPTICON ERROR] {e}")
273
+ wrapper._cleanup()
274
+ sys.exit(1)
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
@@ -1,56 +1,60 @@
1
- import sqlite3
2
- import os
3
- import re
4
- from typing import List, Dict
5
-
6
- class PersistentMemory:
7
- """Production-grade procedural memory using keyword similarity routing."""
8
- def __init__(self, db_path="panopticon_memory.db"):
9
- self.db_path = db_path
10
- self._init_db()
11
-
12
- def _init_db(self):
13
- # Flaw 3 Fix: Enable WAL and connection timeout for multi-process concurrency
14
- with sqlite3.connect(self.db_path, timeout=10.0) as conn:
15
- conn.execute("PRAGMA journal_mode=WAL;")
16
- conn.execute("""
17
- CREATE TABLE IF NOT EXISTS failures (
18
- id INTEGER PRIMARY KEY AUTOINCREMENT,
19
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
20
- reason TEXT,
21
- correction TEXT
22
- )
23
- """)
24
-
25
- def record_failure(self, reason: str, correction: str):
26
- with sqlite3.connect(self.db_path, timeout=10.0) as conn:
27
- conn.execute(
28
- "INSERT INTO failures (reason, correction) VALUES (?, ?)",
29
- (reason, correction)
30
- )
31
-
32
- def _extract_keywords(self, text: str) -> set:
33
- words = re.findall(r'\b[a-zA-Z]{4,}\b', text.lower())
34
- return set(words)
35
-
36
- def get_relevant_failures(self, current_context: str, limit=3) -> List[Dict]:
37
- """Retrieves past failures that have the highest word overlap with the current context."""
38
- current_keywords = self._extract_keywords(current_context)
39
- if not current_keywords:
40
- return []
41
-
42
- with sqlite3.connect(self.db_path, timeout=10.0) as conn:
43
- cursor = conn.execute("SELECT reason, correction FROM failures")
44
- all_failures = cursor.fetchall()
45
-
46
- scored_failures = []
47
- for reason, correction in all_failures:
48
- reason_keywords = self._extract_keywords(reason)
49
- # Calculate Jaccard-like overlap score
50
- overlap = len(current_keywords.intersection(reason_keywords))
51
- if overlap > 0:
52
- scored_failures.append((overlap, {"reason": reason, "correction": correction}))
53
-
54
- # Sort by highest overlap, then take top N
55
- scored_failures.sort(key=lambda x: x[0], reverse=True)
56
- return [f[1] for f in scored_failures[:limit]]
1
+ import sqlite3
2
+ import os
3
+ import re
4
+ from typing import List, Dict
5
+
6
+
7
+ class PersistentMemory:
8
+ """Production-grade procedural memory using keyword similarity routing."""
9
+
10
+ def __init__(self, db_path="panopticon_memory.db"):
11
+ self.db_path = db_path
12
+ self._init_db()
13
+
14
+ def _init_db(self):
15
+ # Flaw 3 Fix: Enable WAL and connection timeout for multi-process concurrency
16
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
17
+ conn.execute("PRAGMA journal_mode=WAL;")
18
+ conn.execute("""
19
+ CREATE TABLE IF NOT EXISTS failures (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
22
+ reason TEXT,
23
+ correction TEXT
24
+ )
25
+ """)
26
+
27
+ def record_failure(self, reason: str, correction: str):
28
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
29
+ conn.execute(
30
+ "INSERT INTO failures (reason, correction) VALUES (?, ?)",
31
+ (reason, correction),
32
+ )
33
+
34
+ def _extract_keywords(self, text: str) -> set:
35
+ words = re.findall(r"\b[a-zA-Z]{4,}\b", text.lower())
36
+ return set(words)
37
+
38
+ def get_relevant_failures(self, current_context: str, limit=3) -> List[Dict]:
39
+ """Retrieves past failures that have the highest word overlap with the current context."""
40
+ current_keywords = self._extract_keywords(current_context)
41
+ if not current_keywords:
42
+ return []
43
+
44
+ with sqlite3.connect(self.db_path, timeout=10.0) as conn:
45
+ cursor = conn.execute("SELECT reason, correction FROM failures")
46
+ all_failures = cursor.fetchall()
47
+
48
+ scored_failures = []
49
+ for reason, correction in all_failures:
50
+ reason_keywords = self._extract_keywords(reason)
51
+ # Calculate Jaccard-like overlap score
52
+ overlap = len(current_keywords.intersection(reason_keywords))
53
+ if overlap > 0:
54
+ scored_failures.append(
55
+ (overlap, {"reason": reason, "correction": correction})
56
+ )
57
+
58
+ # Sort by highest overlap, then take top N
59
+ scored_failures.sort(key=lambda x: x[0], reverse=True)
60
+ return [f[1] for f in scored_failures[:limit]]