errlore 0.1.2__tar.gz → 0.1.3__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 (56) hide show
  1. {errlore-0.1.2 → errlore-0.1.3}/CHANGELOG.md +17 -0
  2. {errlore-0.1.2 → errlore-0.1.3}/PKG-INFO +28 -2
  3. {errlore-0.1.2 → errlore-0.1.3}/README.md +27 -1
  4. {errlore-0.1.2 → errlore-0.1.3}/pyproject.toml +1 -1
  5. {errlore-0.1.2 → errlore-0.1.3}/site/index.html +23 -3
  6. {errlore-0.1.2 → errlore-0.1.3}/site/llms.txt +4 -3
  7. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/__init__.py +1 -1
  8. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/io/jsonl_writer.py +7 -8
  9. {errlore-0.1.2 → errlore-0.1.3}/tests/test_regressions.py +56 -0
  10. {errlore-0.1.2 → errlore-0.1.3}/.github/workflows/ci.yml +0 -0
  11. {errlore-0.1.2 → errlore-0.1.3}/.gitignore +0 -0
  12. {errlore-0.1.2 → errlore-0.1.3}/LICENSE +0 -0
  13. {errlore-0.1.2 → errlore-0.1.3}/SECURITY.md +0 -0
  14. {errlore-0.1.2 → errlore-0.1.3}/benchmarks/bench_error_reduction.py +0 -0
  15. {errlore-0.1.2 → errlore-0.1.3}/benchmarks/bench_retrieval.py +0 -0
  16. {errlore-0.1.2 → errlore-0.1.3}/benchmarks/results/error_reduction/report.md +0 -0
  17. {errlore-0.1.2 → errlore-0.1.3}/examples/anthropic_agent.py +0 -0
  18. {errlore-0.1.2 → errlore-0.1.3}/examples/claude-code/README.md +0 -0
  19. {errlore-0.1.2 → errlore-0.1.3}/examples/claude-code/errlore_posttooluse.py +0 -0
  20. {errlore-0.1.2 → errlore-0.1.3}/examples/claude-code/errlore_sessionstart.py +0 -0
  21. {errlore-0.1.2 → errlore-0.1.3}/examples/claude-code/settings.json.example +0 -0
  22. {errlore-0.1.2 → errlore-0.1.3}/examples/langchain_agent.py +0 -0
  23. {errlore-0.1.2 → errlore-0.1.3}/examples/openai_agent.py +0 -0
  24. {errlore-0.1.2 → errlore-0.1.3}/integrations/openwebui/README.md +0 -0
  25. {errlore-0.1.2 → errlore-0.1.3}/integrations/openwebui/errlore_feedback_action.py +0 -0
  26. {errlore-0.1.2 → errlore-0.1.3}/integrations/openwebui/errlore_memory_filter.py +0 -0
  27. {errlore-0.1.2 → errlore-0.1.3}/site/demo.gif +0 -0
  28. {errlore-0.1.2 → errlore-0.1.3}/site/demo_script.py +0 -0
  29. {errlore-0.1.2 → errlore-0.1.3}/site/og.png +0 -0
  30. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/errmem/__init__.py +0 -0
  31. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/errmem/classifier.py +0 -0
  32. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/errmem/injector.py +0 -0
  33. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/errmem/patterns.py +0 -0
  34. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/errmem/tracker.py +0 -0
  35. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/facade.py +0 -0
  36. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/io/__init__.py +0 -0
  37. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/io/jsonl_index.py +0 -0
  38. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/io/repair.py +0 -0
  39. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/lessons/__init__.py +0 -0
  40. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/lessons/models.py +0 -0
  41. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/lessons/store.py +0 -0
  42. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/py.typed +0 -0
  43. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/retrieval/__init__.py +0 -0
  44. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/retrieval/backend.py +0 -0
  45. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/retrieval/index.py +0 -0
  46. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/sanitize.py +0 -0
  47. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/trust/__init__.py +0 -0
  48. {errlore-0.1.2 → errlore-0.1.3}/src/errlore/trust/engine.py +0 -0
  49. {errlore-0.1.2 → errlore-0.1.3}/tests/conftest.py +0 -0
  50. {errlore-0.1.2 → errlore-0.1.3}/tests/test_errmem.py +0 -0
  51. {errlore-0.1.2 → errlore-0.1.3}/tests/test_facade.py +0 -0
  52. {errlore-0.1.2 → errlore-0.1.3}/tests/test_io.py +0 -0
  53. {errlore-0.1.2 → errlore-0.1.3}/tests/test_lessons.py +0 -0
  54. {errlore-0.1.2 → errlore-0.1.3}/tests/test_openwebui_integration.py +0 -0
  55. {errlore-0.1.2 → errlore-0.1.3}/tests/test_retrieval.py +0 -0
  56. {errlore-0.1.2 → errlore-0.1.3}/tests/test_trust.py +0 -0
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2026-07-06
9
+
10
+ ### Fixed
11
+ - Read-cache poisoning race in `JSONLWriter.read_all`: an append landing
12
+ mid-parse cached pre-append records under the post-append mtime/size, so
13
+ later reads returned stale data and `atomic_update` could silently drop
14
+ the concurrent record. Now caches only when the file is unchanged across
15
+ the parse (pre/post stat match). Found by pre-launch adversarial audit;
16
+ regression test added (Bug 3).
17
+ - Removed dead `JSONLWriter._invalidate_cache`.
18
+
19
+ ### Changed
20
+ - README: added the error-reduction A/B benchmark section (96 paired tasks,
21
+ 63 -> 20 failures, McNemar p=1.8e-09; knowledge-gap 46/48 -> 0/48,
22
+ capability-gap honestly worse at 17/48 -> 20/48), so the PyPI page shows
23
+ the headline evidence, not just the retrieval table.
24
+
8
25
  ## [0.1.2] - 2026-07-06
9
26
 
10
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errlore
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server.
5
5
  Project-URL: Homepage, https://errlore.com
6
6
  Project-URL: Repository, https://github.com/Ma4etaSS/errlore
@@ -91,12 +91,38 @@ mem.report_outcome(inj, success=True)
91
91
  print(mem.stats())
92
92
  # {'errors_total': 1, 'errors_resolved': 1, 'errors_unresolved': 0,
93
93
  # 'lessons_total': 1, 'lessons_applied': 1, 'pending_injections': 0,
94
- # 'trust': {'gpt-5.5': 0.55}}
94
+ # 'trust': {'gpt-5.5': 0.5522...}}
95
95
  ```
96
96
 
97
97
  No API keys needed. errlore itself never calls any LLM -- it manages local
98
98
  JSONL files and does text matching. LLM calls are yours to make (or not).
99
99
 
100
+ ## Does it actually reduce errors?
101
+
102
+ Yes -- for the class of errors memory can fix. Paired A/B benchmark
103
+ (`benchmarks/bench_error_reduction.py`): the same model (claude-haiku-4-5)
104
+ runs 96 tasks twice, with and without errlore injection. Deterministic
105
+ validators, no LLM judges; raw outputs committed in
106
+ [benchmarks/results/error_reduction/](benchmarks/results/error_reduction/).
107
+
108
+ | arm | failures | fail rate |
109
+ |-----|----------|-----------|
110
+ | A: plain | 63/96 | 65.6% |
111
+ | B: with errlore | 20/96 | 20.8% |
112
+
113
+ Exact McNemar over all 96 pairs: p = 1.8e-09 (49 pairs fixed, 6 broken).
114
+ Split by error class:
115
+
116
+ - **Knowledge-gap errors** (workspace conventions: date formats, ID
117
+ normalization, rounding rules, CSV column order): **46/48 -> 0/48, a 100%
118
+ reduction.** The model didn't know the convention; a lesson told it.
119
+ - **Capability-gap errors** (letter counting, string reversal): 17/48 ->
120
+ 20/48 -- errlore did **not** help and slightly hurt. Memory fixes what the
121
+ model doesn't know, not what it can't do.
122
+
123
+ Reproduce: `python benchmarks/bench_error_reduction.py` (needs an Anthropic
124
+ API key; the task families and validators ship in the repo).
125
+
100
126
  ## How it works
101
127
 
102
128
  errlore runs three reinforcement loops around your agent:
@@ -57,12 +57,38 @@ mem.report_outcome(inj, success=True)
57
57
  print(mem.stats())
58
58
  # {'errors_total': 1, 'errors_resolved': 1, 'errors_unresolved': 0,
59
59
  # 'lessons_total': 1, 'lessons_applied': 1, 'pending_injections': 0,
60
- # 'trust': {'gpt-5.5': 0.55}}
60
+ # 'trust': {'gpt-5.5': 0.5522...}}
61
61
  ```
62
62
 
63
63
  No API keys needed. errlore itself never calls any LLM -- it manages local
64
64
  JSONL files and does text matching. LLM calls are yours to make (or not).
65
65
 
66
+ ## Does it actually reduce errors?
67
+
68
+ Yes -- for the class of errors memory can fix. Paired A/B benchmark
69
+ (`benchmarks/bench_error_reduction.py`): the same model (claude-haiku-4-5)
70
+ runs 96 tasks twice, with and without errlore injection. Deterministic
71
+ validators, no LLM judges; raw outputs committed in
72
+ [benchmarks/results/error_reduction/](benchmarks/results/error_reduction/).
73
+
74
+ | arm | failures | fail rate |
75
+ |-----|----------|-----------|
76
+ | A: plain | 63/96 | 65.6% |
77
+ | B: with errlore | 20/96 | 20.8% |
78
+
79
+ Exact McNemar over all 96 pairs: p = 1.8e-09 (49 pairs fixed, 6 broken).
80
+ Split by error class:
81
+
82
+ - **Knowledge-gap errors** (workspace conventions: date formats, ID
83
+ normalization, rounding rules, CSV column order): **46/48 -> 0/48, a 100%
84
+ reduction.** The model didn't know the convention; a lesson told it.
85
+ - **Capability-gap errors** (letter counting, string reversal): 17/48 ->
86
+ 20/48 -- errlore did **not** help and slightly hurt. Memory fixes what the
87
+ model doesn't know, not what it can't do.
88
+
89
+ Reproduce: `python benchmarks/bench_error_reduction.py` (needs an Anthropic
90
+ API key; the task families and validators ship in the repo).
91
+
66
92
  ## How it works
67
93
 
68
94
  errlore runs three reinforcement loops around your agent:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "errlore"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -168,10 +168,13 @@ mem.resolve(err, <span class="c-str">"added validation"</span>,
168
168
  lesson=<span class="c-str">"For date extraction, demand ISO-8601 and verify against source"</span>)
169
169
 
170
170
  <span class="c-com"># 3. next similar task — memory speaks up</span>
171
- inj = mem.inject_for(<span class="c-str">"extract dates from contract"</span>, model=<span class="c-str">"gpt-5.5"</span>)
172
- prompt = task + inj.text
171
+ task = <span class="c-str">"extract dates from contract"</span>
172
+ inj = mem.inject_for(task, model=<span class="c-str">"gpt-5.5"</span>, task_type=<span class="c-str">"extraction"</span>)
173
+ prompt = task + <span class="c-str">"\n\n"</span> + inj.text
173
174
  <span class="c-out"># [LESSONS FROM PAST FAILURES]
174
- # - hallucinated dates -> For date extraction, demand ISO-8601...
175
+ # - UnclassifiedError: hallucinated dates -> For date extraction,
176
+ # demand ISO-8601 and verify against source
177
+ #
175
178
  # KNOWN ISSUES:
176
179
  # - Past error on similar task: hallucinated dates</span>
177
180
 
@@ -180,6 +183,23 @@ mem.report_outcome(inj, success=<span class="c-kw">True</span>)</pre>
180
183
  </div>
181
184
  </section>
182
185
 
186
+ <section>
187
+ <h2>Measured, not promised</h2>
188
+ <p style="color:var(--dim);margin-bottom:18px">Paired A/B: the same model
189
+ (claude-haiku-4-5) runs 96 tasks with and without errlore injection. Deterministic
190
+ validators, no LLM judges; raw outputs committed to the repo. Exact McNemar over
191
+ all 96 pairs: p&nbsp;=&nbsp;1.8e-09.</p>
192
+ <table>
193
+ <tr><th>error class</th><th>plain</th><th>with errlore</th></tr>
194
+ <tr><td>knowledge-gap (workspace conventions)</td><td>46/48</td><td><b class="zero">0/48</b></td></tr>
195
+ <tr><td>capability-gap (model skill limits)</td><td>17/48</td><td>20/48</td></tr>
196
+ </table>
197
+ <p class="note">The honest half is the second row: errlore did <i>not</i> help on
198
+ things the model can't do (letter counting, string reversal) — and even slightly hurt.
199
+ Memory fixes what the model doesn't know, not what it can't do.
200
+ Reproduce it yourself: <span class="mono">python benchmarks/bench_error_reduction.py</span>.</p>
201
+ </section>
202
+
183
203
  <section>
184
204
  <h2>Finds lessons by meaning, not keywords</h2>
185
205
  <p style="color:var(--dim);margin-bottom:18px">Optional local embeddings
@@ -12,9 +12,10 @@ closes the loop: it tracks whether an injected lesson actually helped
12
12
  (reinforce/decay) and which model to trust per task domain.
13
13
 
14
14
  Proof: paired A/B (deterministic validators, raw outputs committed):
15
- repeated workspace-convention errors 46/48 -> 0/48 (100% reduction,
16
- exact McNemar p=1.8e-09). Capability-gap errors (e.g. letter counting)
17
- are honestly NOT improved. See the benchmark in the repository.
15
+ 63 -> 20 failures across all 96 paired tasks (exact McNemar p=1.8e-09);
16
+ repeated workspace-convention errors 46/48 -> 0/48 (100% reduction).
17
+ Capability-gap errors (e.g. letter counting) are honestly NOT improved
18
+ (17/48 -> 20/48). See the benchmark in the repository.
18
19
 
19
20
  ## Links
20
21
  - [Website](https://errlore.com)
@@ -1,6 +1,6 @@
1
1
  """errlore -- memory for AI agents that learns from failures."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
4
4
 
5
5
  from errlore.facade import AgentMemory, Injection
6
6
  from errlore.lessons.store import LessonStore
@@ -285,11 +285,6 @@ class JSONLWriter:
285
285
  self.atomic_rewrite(path, new_entries)
286
286
  return new_entries
287
287
 
288
- def _invalidate_cache(self, path: Path) -> None:
289
- """Drop the read cache entry for *path*."""
290
- with self._cache_lock:
291
- self._read_cache.pop(str(path), None)
292
-
293
288
  def read_all(self, path: Path) -> list[dict[str, object]]:
294
289
  """Read all valid records from a JSONL file.
295
290
 
@@ -330,13 +325,17 @@ class JSONLWriter:
330
325
  except json.JSONDecodeError:
331
326
  logger.warning("Skipping corrupted line in %s", path.name)
332
327
 
333
- # Re-stat after parse to store a consistent snapshot.
328
+ # Cache only if the file is unchanged since the pre-parse stat.
329
+ # An append landing mid-parse would otherwise store the pre-append
330
+ # records under the post-append mtime/size — a poisoned cache that
331
+ # makes atomic_update silently drop the concurrent record.
334
332
  try:
335
333
  st2 = path.stat()
336
334
  except OSError:
337
335
  return records
338
336
 
339
- with self._cache_lock:
340
- self._read_cache[key] = (st2.st_mtime_ns, st2.st_size, records)
337
+ if st2.st_mtime_ns == st.st_mtime_ns and st2.st_size == st.st_size:
338
+ with self._cache_lock:
339
+ self._read_cache[key] = (st.st_mtime_ns, st.st_size, records)
341
340
 
342
341
  return [dict(r) for r in records]
@@ -5,6 +5,9 @@ Bug 1: TrustEngine state silently reset on process restart (facade used the
5
5
  Bug 2: Lost updates under concurrency -- resolve_error/reinforce/decay_unused
6
6
  did read -> atomic_rewrite outside a shared lock, so records appended
7
7
  by other threads between the read and the rewrite were wiped.
8
+ Bug 3: read_all cached a pre-append snapshot under the post-append stat when
9
+ an append landed mid-parse, poisoning the cache; atomic_update then
10
+ silently deleted the concurrent record.
8
11
  """
9
12
 
10
13
  from __future__ import annotations
@@ -112,3 +115,56 @@ class TestNoLostUpdatesUnderConcurrency:
112
115
  ]
113
116
  seed_row = next(r for r in rows if r["id"] == seed_id)
114
117
  assert seed_row["applied_count"] == n_new
118
+
119
+
120
+ class TestReadCacheNotPoisonedByMidParseAppend:
121
+ """Bug 3: read_all cached pre-append records under the post-append stat.
122
+
123
+ An append landing between the parse loop and the re-stat made read_all
124
+ store stale records keyed by the fresh mtime/size; every later read
125
+ returned stale data and atomic_update silently deleted the concurrent
126
+ record. Fixed by caching only when pre-parse stat == post-parse stat.
127
+ """
128
+
129
+ def test_append_during_parse_does_not_poison_cache(
130
+ self, data_dir: Path, monkeypatch: object
131
+ ) -> None:
132
+ import io as _io
133
+
134
+ import errlore.io.jsonl_writer as jw
135
+
136
+ writer = jw.JSONLWriter()
137
+ path = data_dir / "records.jsonl"
138
+ writer.append(path, {"id": 1})
139
+
140
+ real_open = open
141
+ raced = {"done": False}
142
+
143
+ def racing_open(file: object, mode: str = "r", *args: object, **kwargs: object): # type: ignore[no-untyped-def]
144
+ # Intercept only read_all's read of our file, exactly once:
145
+ # snapshot the current content, append a record behind the
146
+ # parser's back, then hand the parser the stale snapshot.
147
+ if str(file) == str(path) and mode == "r" and not raced["done"]:
148
+ raced["done"] = True
149
+ content = path.read_text(encoding="utf-8")
150
+ with real_open(path, "a", encoding="utf-8") as af:
151
+ af.write(json.dumps({"id": 2}) + "\n")
152
+ return _io.StringIO(content)
153
+ return real_open(file, mode, *args, **kwargs) # type: ignore[arg-type]
154
+
155
+ monkeypatch.setattr(jw, "open", racing_open, raising=False) # type: ignore[attr-defined]
156
+
157
+ # The racy read itself legitimately sees the pre-append snapshot...
158
+ first = writer.read_all(path)
159
+ assert [r["id"] for r in first] == [1]
160
+
161
+ # ...but it must NOT have been cached: the next read sees both.
162
+ second = writer.read_all(path)
163
+ assert [r["id"] for r in second] == [1, 2], (
164
+ "read cache was poisoned by a mid-parse append"
165
+ )
166
+
167
+ # And atomic_update must not delete the concurrently appended record.
168
+ result = writer.atomic_update(path, lambda entries: entries)
169
+ assert result is not None
170
+ assert [r["id"] for r in result] == [1, 2]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes