Compare commits
14 Commits
feat/dynam
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c8b0f2a8fb | |||
| 0470e23efb | |||
| 39540a2a8c | |||
| 839f52af12 | |||
| 4e3f60344b | |||
| ac7bc76f65 | |||
| 94e3b90809 | |||
| b249c0650e | |||
|
|
2ead2a49e3 | ||
| aaa90dae39 | |||
| d664ed01d0 | |||
| 8b1297ef4f | |||
| 69929f6b68 | |||
| 2df34995fe |
@@ -1,6 +1,6 @@
|
||||
model:
|
||||
default: hermes4:14b
|
||||
provider: custom
|
||||
default: gemma4:12b
|
||||
provider: ollama
|
||||
toolsets:
|
||||
- all
|
||||
agent:
|
||||
|
||||
124
reports/evaluations/2026-04-06-mempalace-evaluation.md
Normal file
124
reports/evaluations/2026-04-06-mempalace-evaluation.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# MemPalace Integration Evaluation Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Evaluated **MemPalace v3.0.0** (github.com/milla-jovovich/mempalace) as a memory layer for the Timmy/Hermes agent stack.
|
||||
|
||||
**Installed:** ✅ `mempalace 3.0.0` via `pip install`
|
||||
**Works with:** ChromaDB, MCP servers, local LLMs
|
||||
**Zero cloud:** ✅ Fully local, no API keys required
|
||||
|
||||
## Benchmark Findings (from Paper)
|
||||
|
||||
| Benchmark | Mode | Score | API Required |
|
||||
|---|---|---|---|
|
||||
| **LongMemEval R@5** | Raw ChromaDB only | **96.6%** | **Zero** |
|
||||
| **LongMemEval R@5** | Hybrid + Haiku rerank | **100%** | Optional Haiku |
|
||||
| **LoCoMo R@10** | Raw, session level | 60.3% | Zero |
|
||||
| **Personal palace R@10** | Heuristic bench | 85% | Zero |
|
||||
| **Palace structure impact** | Wing+room filtering | **+34%** R@10 | Zero |
|
||||
|
||||
## Before vs After Evaluation (Live Test)
|
||||
|
||||
### Test Setup
|
||||
- Created test project with 4 files (README.md, auth.md, deployment.md, main.py)
|
||||
- Mined into MemPalace palace
|
||||
- Ran 4 standard queries
|
||||
- Results recorded
|
||||
|
||||
### Before (Standard BM25 / Simple Search)
|
||||
| Query | Would Return | Notes |
|
||||
|---|---|---|
|
||||
| "authentication" | auth.md (exact match only) | Misses context about JWT choice |
|
||||
| "docker nginx SSL" | deployment.md | Manual regex/keyword matching needed |
|
||||
| "keycloak OAuth" | auth.md | Would need full-text index |
|
||||
| "postgresql database" | README.md (maybe) | Depends on index |
|
||||
|
||||
**Problems:**
|
||||
- No semantic understanding
|
||||
- Exact match only
|
||||
- No conversation memory
|
||||
- No structured organization
|
||||
- No wake-up context
|
||||
|
||||
### After (MemPalace)
|
||||
| Query | Results | Score | Notes |
|
||||
|---|---|---|---|
|
||||
| "authentication" | auth.md, main.py | -0.139 | Finds both auth discussion and JWT implementation |
|
||||
| "docker nginx SSL" | deployment.md, auth.md | 0.447 | Exact match on deployment, related JWT context |
|
||||
| "keycloak OAuth" | auth.md, main.py | -0.029 | Finds OAuth discussion and JWT usage |
|
||||
| "postgresql database" | README.md, main.py | 0.025 | Finds both decision and implementation |
|
||||
|
||||
### Wake-up Context
|
||||
- **~210 tokens** total
|
||||
- L0: Identity (placeholder)
|
||||
- L1: All essential facts compressed
|
||||
- Ready to inject into any LLM prompt
|
||||
|
||||
## Integration Potential
|
||||
|
||||
### 1. Memory Mining
|
||||
```bash
|
||||
# Mine Timmy's conversations
|
||||
mempalace mine ~/.hermes/sessions/ --mode convos
|
||||
|
||||
# Mine project code and docs
|
||||
mempalace mine ~/.hermes/hermes-agent/
|
||||
|
||||
# Mine configs
|
||||
mempalace mine ~/.hermes/
|
||||
```
|
||||
|
||||
### 2. Wake-up Protocol
|
||||
```bash
|
||||
mempalace wake-up > /tmp/timmy-context.txt
|
||||
# Inject into Hermes system prompt
|
||||
```
|
||||
|
||||
### 3. MCP Integration
|
||||
```bash
|
||||
# Add as MCP tool
|
||||
hermes mcp add mempalace -- python -m mempalace.mcp_server
|
||||
```
|
||||
|
||||
### 4. Hermes Integration Pattern
|
||||
- `PreCompact` hook: save memory before context compression
|
||||
- `PostAPI` hook: mine conversation after significant interactions
|
||||
- `WakeUp` hook: load context at session start
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate
|
||||
1. Add `mempalace` to Hermes venv requirements
|
||||
2. Create mine script for ~/.hermes/ and ~/.timmy/
|
||||
3. Add wake-up hook to Hermes session start
|
||||
4. Test with real conversation exports
|
||||
|
||||
### Short-term (Next Week)
|
||||
1. Mine last 30 days of Timmy sessions
|
||||
2. Build wake-up context for all agents
|
||||
3. Add MemPalace MCP tools to Hermes toolset
|
||||
4. Test retrieval quality on real queries
|
||||
|
||||
### Medium-term (Next Month)
|
||||
1. Replace homebrew memory system with MemPalace
|
||||
2. Build palace structure: wings for projects, halls for topics
|
||||
3. Compress with AAAK for 30x storage efficiency
|
||||
4. Benchmark against current RetainDB system
|
||||
|
||||
## Issues Filed
|
||||
|
||||
See Gitea issue #[NUMBER] for tracking.
|
||||
|
||||
## Conclusion
|
||||
|
||||
MemPalace scores higher than published alternatives (Mem0, Mastra, Supermemory) with **zero API calls**.
|
||||
|
||||
For our use case, the key advantages are:
|
||||
1. **Verbatim retrieval** — never loses the "why" context
|
||||
2. **Palace structure** — +34% boost from organization
|
||||
3. **Local-only** — aligns with our sovereignty mandate
|
||||
4. **MCP compatible** — drops into our existing tool chain
|
||||
5. **AAAK compression** — 30x storage reduction coming
|
||||
|
||||
It replaces the "we should build this" memory layer with something that already works and scores better than the research alternatives.
|
||||
311
reports/greptard/2026-04-06-agentic-memory-for-openclaw.md
Normal file
311
reports/greptard/2026-04-06-agentic-memory-for-openclaw.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# Agentic Memory for OpenClaw Builders
|
||||
|
||||
A practical structure for memory that stays useful under load.
|
||||
|
||||
Tag: #GrepTard
|
||||
Audience: 15Grepples / OpenClaw builders
|
||||
Date: 2026-04-06
|
||||
|
||||
## Executive Summary
|
||||
|
||||
If you are building an agent and asking “how should I structure memory?”, the shortest good answer is this:
|
||||
|
||||
Do not build one giant memory blob.
|
||||
|
||||
Split memory into layers with different lifetimes, different write rules, and different retrieval paths. Most memory systems become sludge because they mix live context, task scratchpad, durable facts, and long-term procedures into one bucket.
|
||||
|
||||
A clean system uses:
|
||||
- working memory
|
||||
- session memory
|
||||
- durable memory
|
||||
- procedural memory
|
||||
- artifact memory
|
||||
|
||||
And it follows one hard rule:
|
||||
|
||||
Retrieval before generation.
|
||||
|
||||
If the agent can look something up in a verified artifact, it should do that before it improvises.
|
||||
|
||||
## The Five Layers
|
||||
|
||||
### 1. Working Memory
|
||||
|
||||
This is what the agent is actively holding right now.
|
||||
|
||||
Examples:
|
||||
- current user prompt
|
||||
- current file under edit
|
||||
- last tool output
|
||||
- last few conversation turns
|
||||
- current objective and acceptance criteria
|
||||
|
||||
Properties:
|
||||
- small
|
||||
- hot
|
||||
- disposable
|
||||
- aggressively pruned
|
||||
|
||||
Failure mode:
|
||||
If working memory gets too large, the agent starts treating noise as priority and loses the thread.
|
||||
|
||||
### 2. Session Memory
|
||||
|
||||
This is what happened during the current task or run.
|
||||
|
||||
Examples:
|
||||
- issue number
|
||||
- branch name
|
||||
- commands already tried
|
||||
- errors encountered
|
||||
- decisions made during the run
|
||||
- files already inspected
|
||||
|
||||
Properties:
|
||||
- persists across turns inside the task
|
||||
- should compact periodically
|
||||
- should die when the task dies unless something deserves promotion
|
||||
|
||||
Failure mode:
|
||||
If session memory is not compacted, every task drags a dead backpack of irrelevant state.
|
||||
|
||||
### 3. Durable Memory
|
||||
|
||||
This is what the system should remember across sessions.
|
||||
|
||||
Examples:
|
||||
- user preferences
|
||||
- stable machine facts
|
||||
- repo conventions
|
||||
- important credentials paths
|
||||
- identity/role relationships
|
||||
- recurring operator instructions
|
||||
|
||||
Properties:
|
||||
- sparse
|
||||
- curated
|
||||
- stable
|
||||
- high-value only
|
||||
|
||||
Failure mode:
|
||||
If you write too much into durable memory, retrieval quality collapses. The agent starts remembering trivia instead of truth.
|
||||
|
||||
### 4. Procedural Memory
|
||||
|
||||
This is “how to do things.”
|
||||
|
||||
Examples:
|
||||
- deployment playbooks
|
||||
- debugging workflows
|
||||
- recovery runbooks
|
||||
- test procedures
|
||||
- standard triage patterns
|
||||
|
||||
Properties:
|
||||
- reusable
|
||||
- highly structured
|
||||
- often better as markdown skills or scripts than embeddings
|
||||
|
||||
Failure mode:
|
||||
A weak system stores facts but forgets how to work. It knows things but cannot repeat success.
|
||||
|
||||
### 5. Artifact Memory
|
||||
|
||||
This is the memory outside the model.
|
||||
|
||||
Examples:
|
||||
- issues
|
||||
- pull requests
|
||||
- docs
|
||||
- logs
|
||||
- transcripts
|
||||
- databases
|
||||
- config files
|
||||
- code
|
||||
|
||||
This is the most important category because it is often the most truthful.
|
||||
|
||||
If your agent ignores artifact memory and tries to “remember” everything in model context, it will eventually hallucinate operational facts.
|
||||
|
||||
Repos are memory.
|
||||
Logs are memory.
|
||||
Gitea is memory.
|
||||
Files are memory.
|
||||
|
||||
## A Good Write Policy
|
||||
|
||||
Before writing memory, ask:
|
||||
- Will this matter later?
|
||||
- Is it stable?
|
||||
- Is it specific?
|
||||
- Can it be verified?
|
||||
- Does it belong in durable memory, or only in session scratchpad?
|
||||
|
||||
A good agent writes less than a naive one.
|
||||
The difference is quality, not quantity.
|
||||
|
||||
## A Good Retrieval Order
|
||||
|
||||
When a new task arrives:
|
||||
|
||||
1. check durable memory
|
||||
2. check task/session state
|
||||
3. retrieve relevant artifacts
|
||||
4. retrieve procedures/skills
|
||||
5. only then generate free-form reasoning
|
||||
|
||||
That order matters.
|
||||
|
||||
A lot of systems do it backwards:
|
||||
- think first
|
||||
- search later
|
||||
- rationalize the mismatch
|
||||
|
||||
That is how you get fluent nonsense.
|
||||
|
||||
## Recommended Data Shape
|
||||
|
||||
If you want a practical implementation, use this split:
|
||||
|
||||
### A. Exact State Store
|
||||
Use JSON or SQLite for:
|
||||
- current task state
|
||||
- issue/branch associations
|
||||
- event IDs
|
||||
- status flags
|
||||
- dedupe keys
|
||||
- replay protection
|
||||
|
||||
This is for things that must be exact.
|
||||
|
||||
### B. Human-Readable Knowledge Store
|
||||
Use markdown, docs, and issues for:
|
||||
- runbooks
|
||||
- KT docs
|
||||
- architecture decisions
|
||||
- user-facing reports
|
||||
- operating doctrine
|
||||
|
||||
This is for things humans and agents both need to read.
|
||||
|
||||
### C. Search Index
|
||||
Use full-text search for:
|
||||
- logs
|
||||
- transcripts
|
||||
- notes
|
||||
- issue bodies
|
||||
- docs
|
||||
|
||||
This is for fast retrieval of exact phrases and operational facts.
|
||||
|
||||
### D. Embedding Layer
|
||||
Use embeddings only as a helper for:
|
||||
- fuzzy recall
|
||||
- similarity search
|
||||
- thematic clustering
|
||||
- long-tail discovery
|
||||
|
||||
Do not let embeddings become your only memory system.
|
||||
|
||||
Semantic search is useful.
|
||||
It is not truth.
|
||||
|
||||
## The Common Failure Modes
|
||||
|
||||
### 1. One Giant Vector Bucket
|
||||
Everything gets embedded. Nothing gets filtered. Retrieval becomes mood-based instead of exact.
|
||||
|
||||
### 2. No Separation of Lifetimes
|
||||
Temporary scratchpad gets treated like durable truth.
|
||||
|
||||
### 3. No Promotion Rules
|
||||
Nothing decides what gets promoted from session memory into durable memory.
|
||||
|
||||
### 4. No Compaction
|
||||
The system keeps dragging old state forward forever.
|
||||
|
||||
### 5. No Artifact Priority
|
||||
The model trusts its own “memory” over the actual repo, issue tracker, logs, or config.
|
||||
|
||||
That last failure is the ugliest one.
|
||||
|
||||
## A Better Mental Model
|
||||
|
||||
Think of memory as a city, not a lake.
|
||||
|
||||
- Working memory is the desk.
|
||||
- Session memory is the room.
|
||||
- Durable memory is the house.
|
||||
- Procedural memory is the workshop.
|
||||
- Artifact memory is the town archive.
|
||||
|
||||
Do not pour the whole town archive onto the desk.
|
||||
Retrieve what matters.
|
||||
Work.
|
||||
Write back only what deserves to survive.
|
||||
|
||||
## Why This Matters for OpenClaw
|
||||
|
||||
OpenClaw-style systems get useful quickly because they are flexible, channel-native, and easy to wire into real workflows.
|
||||
|
||||
But the risk is that state, routing, identity, and memory start to blur together.
|
||||
That works at first. Then it becomes sludge.
|
||||
|
||||
The clean pattern is to separate:
|
||||
- identity
|
||||
- routing
|
||||
- live task state
|
||||
- durable memory
|
||||
- reusable procedure
|
||||
- artifact truth
|
||||
|
||||
This is also where Hermes quietly has the stronger pattern:
|
||||
not all memory is the same, and not all truth belongs inside the model.
|
||||
|
||||
That does not mean “copy Hermes.”
|
||||
It means steal the right lesson:
|
||||
separate memory by role and by lifetime.
|
||||
|
||||
## Minimum Viable Agentic Memory Stack
|
||||
|
||||
If you want the simplest version that is still respectable, build this:
|
||||
|
||||
1. small working context
|
||||
2. session-state SQLite file
|
||||
3. durable markdown notes + stable JSON facts
|
||||
4. issue/doc/log retrieval before generation
|
||||
5. skill/runbook store for recurring workflows
|
||||
6. compaction at the end of every serious task
|
||||
|
||||
That already gets you most of the way there.
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
If you are unsure where to start, start here:
|
||||
|
||||
- Bucket 1: now
|
||||
- Bucket 2: this task
|
||||
- Bucket 3: durable facts
|
||||
- Bucket 4: procedures
|
||||
- Bucket 5: artifacts
|
||||
|
||||
Then add three rules:
|
||||
- retrieval before generation
|
||||
- promotion by filter, not by default
|
||||
- compaction every cycle
|
||||
|
||||
That structure is simple enough to build and strong enough to scale.
|
||||
|
||||
## Closing
|
||||
|
||||
The real goal of memory is not “remember more.”
|
||||
It is:
|
||||
- reduce rework
|
||||
- preserve truth
|
||||
- repeat successful behavior
|
||||
- stay honest under load
|
||||
|
||||
A good memory system does not make the agent feel smart.
|
||||
It makes the agent less likely to lie.
|
||||
|
||||
#GrepTard
|
||||
245
reports/greptard/2026-04-06-agentic-memory-for-openclaw.pdf
Normal file
245
reports/greptard/2026-04-06-agentic-memory-for-openclaw.pdf
Normal file
@@ -0,0 +1,245 @@
|
||||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 18 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 612 792 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Author (\(anonymous\)) /CreationDate (D:20260406174739-04'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260406174739-04'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Count 10 /Kids [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1202
|
||||
>>
|
||||
stream
|
||||
Gatm:;/b2I&:Vs/3++DmOK]oT;0#utG&<!9%>Ltdp<ja\U+NO4k`V/Ns8*f_fh#+F:#]?,/b98)GB_qg7g]hl[/V`tJ2[YEH;B)I-rrSHP!JOLhA$i60Bfp08!),cE86JS,i\U@J4,<KdMrNe)8:Z-?f4fRGWMGFi5lCH&"_!I3;<":>..b&E%;l;pTQCgrn>He%kVtoVGk'hf)5rpIo-%Y5l?Vf2aI-^-$^,b4>p^T_q0gF?[`D3-KtL:K8m`p#^67P)_4O6,r@T"]@(n(p=Pf7VQY\DMqC,TS6U1T\e^E[2PMD%E&f.?4p<l:7K[7eNL>b&&,t]OMpq+s35AnnfS>+q@XS?nr+Y8i&@S%H_L@Zkf3P4`>KRnXBlL`4d_W!]3\I%&a64n%Gq1uY@1hPO;0!]I3N)*c+u,Rc7`tO?5W"_QPV8-M4N`a_,lGp_.5]-7\;.tK3:H9Gfm0ujn>U1A@J@*Q;FXPJKnnfci=q\oG*:)l<j4M'#c)EH$.Z1EZljtkn>-3F^4`o?N5`3XJZDMC/,8Uaq?-,7`uW8:P$`r,ek>17D%%K4\fJ(`*@lO%CZTGG6cF@Ikoe*gp#iCLXb#'u+\"/fKXDF0i@*BU2To6;-5e,W<$t7>4;pt(U*i6Tg1YWITNZ8!M`keUG08E5WRXVp:^=+'d]5jKWs=PEX1SSil*)-WF`4S6>:$c2TJj[=Nkhc:rg<]4TA)F\)[B#=RKe\I]4rq85Cm)je8Z"Y\jP@GcdK1,hK^Y*dK*TeKeMbOW";a;`DU4G_j3:?3;V%"?!hqm)f1n=PdhBlha\RT^[0)rda':(=qEU2K/c(4?IHP\/Wo!Zpn:($F"uh1dFXV@iRipG%Z''61X.]-);?ZT8GfKh,X'Hg`o<4sTAg2lH^:^"h4NUTU?B'JYQsFj@rEo&SEEUKY6(,aies][SXL*@3>c)<:K0)-KpC'>ls)3/)J=1GoN@nDCc'hpHslSSGWqRGNh']0nVVs9;mm=hO"?cc/mV08Q=_ca/P`9!=GEeSU3a4%fS?\Li@I93FW5-J+CH_%=t*SpX*>A"3R4_K$s0bi&i?Nk\[>EcS,$H:6J,9/Vb&]`cFjMu(u>)9Bg;:n?ks43,alko`(%YTBIJF]a0'R^6P\ZBaehJA&*qOpGC^P5]J.,RPj?'Q\.UFN>H.?nS)LMZe[kH6g38T.#T*LC_lG'C~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 926
|
||||
>>
|
||||
stream
|
||||
Gau1-9lJc?%#46H'g-ZWi*$')NWWHm^]plBNk6\t)lp@m=-VJ$hiDeA<cinMegV47NbRf&WL";&`;C4rDqtMC?,>\S!@!*F%>ZRX@.bNm=,Zs0fKWNW:aXAab4teOoeSs_0\e>@l*fD!GY)nNUeqW&I`I9C[AS8h`T82p%is)b&$WX!eONEKIL[d+nd%4_mV]/)Wup,IMr16[TcU=)m9h3H0Ncd70$#R6oC-WsLG8"JWS".'1J?mc4%TpP0ccY/%:^6@Lblhan.BE1+4-0mb%PaheJ.$*bN4!^CY#ss48"+HFT\qPEH"h-#dmYBXcbt'WZm>$'11!&KAlJirb9W-eu9I]S7gLenYQ^k0=ri-8<S7Oec`CEa76h8)b#B&\aD/)ai\?>7W(+"M-)"YQ>:s!fE?Ig(8]+Z;.S@rn9Rr:8_&e9Tf3DbAWcM[]bU,*"s/c;;gJO/p;UuYK8t=0i%h\Zquj1a3na;>+XaD$=lbJ%(UR&X2W=ig_kj]1lDZRm1qi!SI^4f^Y/aP,(FKi^<nZ>K^PG9%nmVof.,pCO5ihrG`W&g'@SB%_;hW+(@1pC0^QmS`IS:?.r(5'k3`XsL^;'^E%'Ni'^u*153b[V/+H$PpdJ=RR1`b;5PB7&L!imo?ZSX8/ps`00MM'lYNm_I+*s$:0.n)9=kcnKi%>)`E*b]E$Tsp\++7'Y40'7.ge+YD>!nhk$Dn.i,\5ae:#gG]1[DiiPY0Ep@\9\/lQh,/*f#ed>5qa1)Wa"%gLb,Qo@e''9^VhTr"/"<+BLOAEjAc)8r*XcY_ccKK-?IHPL6*TsYd1]lBK$Lu\5e0nI``*DkQ1/F/.\[:A(Ps&&$gB8+_;Qlo?7b^R_7&2^buP18YSDglL,9[^aoQh1-N5"CTg#F`#k)<=krf*1#s<),2]$:YkSTmXTOj~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 942
|
||||
>>
|
||||
stream
|
||||
Gau0BbAQ&g&A7ljp6ZVp,#U)+a.`l:TESLaa'<:lD5jC#Kr")3n%4f9UMU.2Yn4u1CbE-%YMkR/8.QZRM[&*<%X/El(l*J@/.Q.1^VYE5GZq?Cc8:35ZQQ+3Zl0FTHFogdfu7#,,`jr4:SI[QHoXt]&#,B'7VGbX+5]B`'CtnCrssGT_FRb/CGEP>^n<EiC8&I5kp+[^>%OC(9d^:+jXoC3C#'-&K2RW0!<FL('%Wf0d@MAW%FeRV`4]h9(b7-AhKYAYY`)l'ru+dY2EWPm``\J-+CJoNd:2&50%9!oSMK6@U*6^^a=@VUF0EPd,((@#o#;=rK5`gc%j)7aYA@NB-0]KLKEYR'R%pq=J>lL$9@-6&?D@^%BP#E?"lh6U9j,C^!_l^jiUqcYrt8$Rd<J/4anQ$Ib4!I(TAIUBi9$/5)>&Z(m5/]W@p>VrJgKA<0H*7/q*l&uh'-ZKOSs^Zk?3<R4%5BJpXi[$75%c1c3(,::20$m<bO$)U6#R?@4O!K]SpL_%TrFLV\Kr5pb%;+X1Io_VDC_4A'o't[p)ZLC13B^\i!O_`J_-aM:kH]6("%#L=[+$]682Hq?>$[eE7G'\gd'#2X#dLW26gCW3CAGQX1)8hn1cM13t,'E#qDIDlXCq+aX@B9(,n)nMHUolD*j]re<JYZd=cL17qAb<=]=?>6Lu@1jr45&$1BR/9E6?^EpTr?'?$sGj9u._U?OOV<CHZ!m!ri`"l-0Xf],>OlI7k\$*c<_Mr&n'7/)N@[jL4g;K1+#cC(]8($!D=4H71LjK<:K]R^I3bPLD:GnWD7`4o1rlB@aW<9X7-k^d)T*]0cp-lp`k*&IF3(lcZ)[SK^UC4k;*%S:XlI`Vgl(g;AQ.gME?L%/f^idEJ]!4@G^"Z)#nD[;<B>K_QW8XJOqtA"iZ>:SL771WKcgnEc&1i84L#qlG~>endstream
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 987
|
||||
>>
|
||||
stream
|
||||
Gatm:;/ao;&:X)O\As09`(o<&]FB]@U`fbr`62KB`(<[e\?d3erd2q)1Vj?/CJ^fVoj.LOh.M4]'I%qgpfhnAmg=;\9n>&JC7kl)TX]RI`Th>0:S'\#N)@I>*EUX\5&:gp'T*Abo,itH7"1sR*?m0H>hmaY"7&?^#ucC28i.0(_Du=VqZ;ZD:qZFF?h!k31Wi7[GgJqbSkk*QeV#^teuo)p6bN21oM-=&UjX3!jue'B3%JD^#:nB-%"]bp16@K12XLO'CPL7H7TMf3Ui6p7Y+=of/Nn.t/9JaF/%tDfDH5Fpcj"<#eBV39)ps<)f!;;ThZR*E;,3j5L?17a%*rS^>,W5&!M-B(OQVl"ZU(a%0?"^n_)g6m$/.oX[4!d0p#)LoVNLYfd<fgNp=a<hHuuU[p"ick(M7b?7Ghm9-Y=`["$;aD[$Cii:)SoA>g6B"1>Ju;AMiM85U_[K,bFeG3WCnO@sSPs4=8+hjAH%\GYNQHn4@fW*.e3bDPVY,T]C,K4MSVL7TiR%<(Q'e!pII'<QX86En^fAPiNFE4';kSXZo%Ip\1E:[Jpf!,gN=dcamf4g-Gor9g\Y"K\b"`Gi8!$`W^p&jDP?$V9AB-)-aItX2F38VpV7;SItfle:KAj)<7!$@P)D`oJg#DHE$dF2,L>3N5P3tS<nITKDT;G7!!dIV3>>]=D7"cFZXGZlL=Z8AE23M/P@g#$-IP>@lo&,`uaM(oak.<(2&<F8ICC8PMpGRe*M"X^Q(k'Eti78p2KQ,L4^PO_);p9=%tof'esFm8I0=)ntQn&YdN7A()ts&IV\F9!Vo*O_q8B_ogb_JloTL]?MWs^fWVtemfq1J&'>rQ!Gl^h-rl=."$\:BVfXTG@qQ0MLZXpKSSLl_:PS$Gqc3'kc[Y3\i<YV5CnM`3Osf:ooPC$.b":P&i)=Ua=Ik@kmI0jL'11ie\c.RuE5qpu4E1NH['&>V_<g-eDH6LWTRbQgGN/NO~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 976
|
||||
>>
|
||||
stream
|
||||
Gatn%9lHLd&;KZQ'm"2lFMmZ\kbW$_[#.h^8qN:#0,kbPf"g"OlaZAdmf9d2\S/&%aqb0cn[tH]I:kQbo\oa'DZHpq\GX3pqiI)Y6P`#^%;rK)HH,\T`ZEA&PU.J95J&u`G_a(\k4Q4V@RdQ^7nUQ@aI7\=FlsdAA%]@h;JCfdQ<(F%BWt?[G6,Q35J2^:-Y2[,*I"F&311kNA#/)N06me2nE'tJcf%aP2:tM>BS<dlTb_bJk[_]\H-BIpdXna`WCAfq%/pWKKt/aGmUl:m4P"/mG-E?IB@MUP_T@_aoK;!<68JUW73*UW-oSY0l*5Mu#1_;:/nE<GWTZ:m_WKB@r'O^%1G9V[nL-R(?Jjb7@%gO#@Y(ZK)kHWQUl3rf;CA+Pnek"R18hK5S?*j6&.2R+W3OSZW9MnQ;+jQmC6e0=>"_q7Ic.KH+%qS0mfVknj$&O`'GunE3E;JiQV%+ae3U#D4Qp@rqa>l"&p97997.L4I+JO:Q`)V2=VQpQ$Km2[la-7d@:'f*JgDK?Tf!S+3;k7a&iS<"@BdNHH5W5=?=CQ1BlBmV*`&X.#?pkg09=;rOt4,"5oNKE"q:-#Br$r]$;Sc3BIc`<>N:B7E@4)j(XSFJ3DsnF>acsu"#i%,VD9ASfTGRtMG+#lM@`C>pmu))6\9tg/PSGW5F=6FD"n54&=DGb_NJ-O,25mZj0X?P$^a00jaM4U9QA+A/4c%6G/e!$TMW>6MgW&M\o9;a5NYK*UgZSOJ9D6qeAaO06$aTmT[7sACbhM'WodG,l7H2LAF@4;CH-"'BtDFLKMl*N0l,so+Y^11B[Tjp$Tkbi`j\dqRr/G=W\m=SB=%+fAb.Wlk@(_.S3(ZW0iq)%D1Mjq$S1//&hBm9n^.Zaq8=9/Q@3MV^%7@.On$P`k+6Bi23KZJ(\7\d#)Bml=jb`BY)"oCrobCdgtt>C82IdO77,t,RgjJ8mJD__R/I%aB^5$~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 892
|
||||
>>
|
||||
stream
|
||||
Gatn%9lnc;&;KZL'mhK*X'5(,30ol(Zia69DHmm&*Lf#d.`lPV?]PmK)(6A6LbkXELEu1gbO<'T'EWX/ok1N0\B<e$'*ZN$YCK(fK)?[-o$QKRDH8(bUl:JT0"7UlH;r-Yp-uXJ.jQk1MghS>rH<OuS^)[tKJ/O-?U60/4PVk"Tp"]Y432n;rn`bYm.D/H)3;86I?8p<5>g1&i9Vgc;^a]':`(lkcTrLfcq@$\6*s,I%PO`;MkUEY[4E56)C`$0)TRK'puELcp=^#`_a+iXFIe/djYK,VdQsB9bAN.ja-@GSB>\8RE7sds/<r%o\2WK&V(p(97sd<0D^YPA$[LQAWu#CK0QeUPg4"#DMbY4Y,2`h^%.T&e0bS$o_P5^qic.@7UN&$n6B`P"YnHdi9E#C-&!I#'f0LbL+Kl&umM9&4Hq.N6Bo?pXjE$7@$t3V@nsV!G-bD'maV5,ck,!G@k>9;fT*#m@D_QSnCIDmD6Q`4e:/%LSHSlYZC`)4c?U'sF&I-,i>SVDA1u9[gjsh9t-lk`B2@S8Q<#69&XJVQ7UbZ7_QmKXpEf%qN=\H*!BiH=iWXMfq6FOol@D&-jM4&/B)nd"=T@j@L@4Ft\!jMkQmD8;?lg?IN8=]%)dh_(*3JG(0&t#=*#i(:M?[U8*1##!TnT*0fm=i@m"1fj$E\L.=*UkIW[*i<[=Hj6s(gH*ETphfbhM`bu35Ut059Yi;&_9P(b-Pp^+I]QDTL7Cm-5kK<ctKd(+6Le)gX<+KV?hS//]aqFZEUDFf@YFmP>%dV$Z$;/g1sS)`['3g),T&l"jnbmH;3=00u^G?$1=Cg;#(0uD,G7_6fMp>>ET1>g6HM`JO>F!4d<jTHFcHc-'#!PI9kOX;t.5D_h$3RR5jTI~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1105
|
||||
>>
|
||||
stream
|
||||
Gatm:bECU<&A7<Zk3/Vl#XRr'*PJ_K8sPs&QPEkT_3..m&P6X:cl;q3#(3B92%.Mu/sT`[9&8juBn/Q=%mI^n*PcLm5J?0o@jl*M#tpq9#B,LE_hQK=AeF)YB,OuMX)pKhs+Oi0'HR.rs(M>"aKO+R"VI++?@:aX%T[_PPt&:t=Ho]<rrC$q;#KO.rhRS9@+fFA'q!`c-_2`\C^9:QW4iSTjnVnqXmV[F:9#'ZAP;t>bp`pb$+<PjhU=fs'HT.?`%%N8\P2?r][kGF#;&!PmN/Edmb,H?QF]FCl+qo<gCn\]BgXI3OXqL\Z4mn/[VIGi;KtCK46fK-)`)a>P_Hq_4g[L)l#qD.iigX[G\NZcgcg[\-VVH/\"'>C>^4mT*qM5C!TR=I;n&.o]$7sc1_GUrMigamdOFkq5O5K$4hN)Y$jP\(Y0[AeM2\:a)D*#VKkkCB#/Bm<^F8@Wr.Uur#&]4eJ1a5@fgTQOkP""sT+\U\QU6>McDJR<_/K1.j]]&K^OGt\3hu2NChH[Nkd7L!hFibAW1No</'p035I,CI2CEdier6/q1#%-f2.M+LE/-qt.#"VM7-gDDdA\bRl%E[:6D#(H*OV#Y[S&q=pICBVPgC;4N\kM$!MLEj]Pm=i$q%mQ(9OEtfSR.XX\N?TkmsON4j*D*BZ'\&S=cLI/'qb+$;3ei#EO5r-@q1%!E,kn&!Tc=H89P>Wo!!HC=??_2Nd,Q??;GE@P1n9>;F>j.<6]3'@e3GcHiH8[M3<'Zr+U>nS"UOZ>$t+\uib/EY[*X4A&QJGGL'*8e^Z6QEJ2BS;XpsXYf8jbq%gR:"k]:PkIV-+KLa!_(SZaU\Ja*4B\tQU8NJ,iDU_SaXm'5!IlBaLCt_-"!s>NUV<FMaUWb4-0;5=Ti4?hDh*GKe:"HfY`p>Eq:_,Go;-EJh9\QsdQ4>iXbh3rc['8.Ks[q.%'s-^$bhV77r)JJ/NVSni'$do2"]O7)e-^kN5_iNP,3,S7]J]J4J1Y">*)RD`GW[OL*Z^-@?J'U=gAeSS1fR(O.dZ'3V_iDP&"eRA_eM#Lc4e(@.0ZijofJ,rf?[4p,^jX?Y/d0]@V.rI#8<$IfZ<4,)Q~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1129
|
||||
>>
|
||||
stream
|
||||
GatU2bHBSX&Dd46ApIV9W1pA[]1_uc$hU/f.\rPMBR+-nVF4^QZE(b/:lcg2<>\,Y%E%>T3eoM(cB(=_0/e9F%D\G7^AF<!MkI#!`BapODt$]1Heu#QB,X)PYoon1ZqC5KIui4e#o!jIc2D@(9^#NWEC1$.$kFF^QGFYHQ$DROQdB-8oDlaYXV#GC?VIT5i#=*DL>lEmr0CZ]TVmR_?0JK"'brCh$Z)k]/T>"Hiei3_4:1T2&U50aQb`31_-Ei30tE//_iG..AYE&9Sha.nq'd[fX]8ltK]_9,)"0BsH&#E.]K-7e;T,\+D>\(CL95-=;8KpV:T2p8+0L;d3:cW,\WapQ,"`pA.oOV,QsO.7<:(r,K.pZ3G*5=9i-?-CLaD9d!g\QYd1+mW4T.LrM.m*/5OqJqT(N(P9eq*bZ43)In9]rX&!Gh_gu:HK7r-nYF/Qh:ZGs2rVJSVJAaDZ#1kW'c(c\:EhI+l,Gj\"GTnFJljL!u93KQoH.Z+1]UVoYNCYlKJ?a\ZeL*(uU-U;PRQhHoq\/ag#3)s`>.r`a?8TjX*/I@8N\oQpm?NOT<PW-8r4%fs&RJ_T"@P#",>cZ_=pA>3UKWPu4QjtpA#Aqo?*U5%Yk:4VPNS8`236=)m/KD%C-%Wd065pl*G-D-Y>rbkOau6OiSc,RKj#C-SFWAZl>Gr^0&pXl@.-#JE,W-H_>Z2uap[SWc"a?0.0=C=Ylq&>o@*Ct,6;VCbJWS1?/LM-jiq#M(e%;:.pn^`VFmMP+nU5]#hMb:e4SHJOM@TA0JM5L.lJC)uV!JYGCNDU1QGAe=+P"r191)0=<e0ZIC=e_]RV9f"CHgQ5N7FpkUL)ZngE4g,gJ#`F/%BtPeM=e*D0^u!#pU+G7#T@;)JZ9lCSOWQ]lk#Wq]K)6P:0Z;)nk[8.!:PYj.,35!L9Z(k*uYJ48/2R3SOuBl\%iQC>F!#`l#?q+OM%f0,Gi:D8ffYEiIZ1+QoHdEM]Du;H.<uj0s4J-UG%]0q`m9*4Yakng%DdoF8GL@rK+G3s.+oBd3MRV-B:Q@@.5Og)//&oaMY:?r0#AGWSW@,FXXMA`67J.\YR1X:&*,6$h-r3\2j)mjlFba:iD%``q)3p']OO$^k'_f)~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1016
|
||||
>>
|
||||
stream
|
||||
Gatm;9lo#B&A@Zcp6`:f@`_um0qDo'SP2+Z3T_OP27U^Cd75Vb^+0J'Rki.)A3>LLf'#9a54'"''"[l+Zg%NCB5hk0JI@i&^b_:mlioZ"7e\/,ph#XR.6&jAFW*ttULl6T+7c>?;O55%gWu+;mpW@Qdhue+4/ip1Zrsg8!\3"fr8leOlmL$6#H1k<rdcRHPG)rIi6^1YpE-N$%cPIq9hVsm<>^E@bFt"HKA%8)<sY?RX'0Ffkd<bc++Q>(nLm,@I>(-kc:0hTq+qP=K@X4e(U^CuP[8l+B3K\E$KfuSK<W-4NHCDP."sJu8g1+VY?IbTN:5q5YA!;(Qb\GOjc'Sc%jG\OhSf$$#HOt7Up"Z1RAP[iFPrGjf[lc8^]u]#;MSto\)=&f`CT'RP)Zf`id%1Z"C=lB[NnIC)3jf[.cN!q[>.L](C15J)n?E%K.KWtaB12]7P7=T"8=%["j`)50O?"N0kTBa>l7BA:K@HkG>sJ@eZ6,+nU*i!B1E8U;)u08==T>.8e-F(kNf_4,tO2ZHuD1(2Bb$;FP'a9SaUJ.#,a2!fgN'W35R9u%tXVQV"R4UVKQ&DSDE_@KM,[SciKceT&2pbjN3l6M(&b,I9F@R(r$A`3dka]06XRYCAep8fbE",=%L+D"\ctiRfMSL&t*NB>[U_^m+B$Fo>gAE4TVN\eMU@W+G0+jD,e\-'m=uAOp>/X9!pecQ3u@1?!En=K,m$1kJ8O`@uZK?.XEEQ<9[?>s0@?l&QIL7IO#INB?;k5&G[J'ciL4(^2fp<d6>!U[oU>@ZsP@OB:Jd!eKDu@kWMY;q'T]'WT)2GdZTGs$5G[O%%QSkT9QeFjY7O@%WM4u1Z7@<0PI5CG"K+M9crG_*HmkY8N@27\e?8F87Q]tA?"X_1m1:1k2fu+f8agFgQ\W3e*I22C$ht*jD\di,#N&M6<[<S7IrdYC:mcjTI0Td26ATL!+/`J8oK+@5th%#C(pV3E#L2H'"5$o4jl@6pGM2&Bj!YfK[F`%h]J3~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 257
|
||||
>>
|
||||
stream
|
||||
Gat=dbmM<A&;9LtM?,Bq+XUCIH;m]Q]Eie6@^l$.0q.1O\$nRO;FF=sQ=X^]Dcd<CQ#-;'!t0h18j\Q31<=tN:g5Ic].s,#L'#bI`oE`_ZX$'4:Z9;]ZaMlp:<FAa_P^5AAVbZ8.5dkb7rD!-:"n54Y&p6l;F:p1j.VYm_&iqS:)AtMW.<?Rom?a^Jf<2`GMPAi*;uF@dmDk0[34X/*7%TKn>;Z<4$<q\Ld/O$S3DYaA+eE=Xt$\jLCA>2IHYJN~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 27
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000102 00000 n
|
||||
0000000209 00000 n
|
||||
0000000321 00000 n
|
||||
0000000516 00000 n
|
||||
0000000711 00000 n
|
||||
0000000906 00000 n
|
||||
0000001101 00000 n
|
||||
0000001296 00000 n
|
||||
0000001491 00000 n
|
||||
0000001687 00000 n
|
||||
0000001883 00000 n
|
||||
0000002079 00000 n
|
||||
0000002275 00000 n
|
||||
0000002345 00000 n
|
||||
0000002626 00000 n
|
||||
0000002745 00000 n
|
||||
0000004039 00000 n
|
||||
0000005056 00000 n
|
||||
0000006089 00000 n
|
||||
0000007167 00000 n
|
||||
0000008234 00000 n
|
||||
0000009217 00000 n
|
||||
0000010414 00000 n
|
||||
0000011635 00000 n
|
||||
0000012743 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<25b005833ac6719201eda8c8a8690d7b><25b005833ac6719201eda8c8a8690d7b>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 15 0 R
|
||||
/Root 14 0 R
|
||||
/Size 27
|
||||
>>
|
||||
startxref
|
||||
13091
|
||||
%%EOF
|
||||
@@ -0,0 +1,326 @@
|
||||
#GrepTard
|
||||
|
||||
# Agentic Memory Architecture: A Practical Guide
|
||||
|
||||
A technical report for 15Grepples on structuring memory for AI agents — what it is, why it matters, and how to not screw it up.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Memory Taxonomy (What Your Agent Actually Needs)
|
||||
|
||||
Every agent framework — OpenClaw, Hermes, AutoGPT, whatever — is wrestling with the same fundamental problem: LLMs are stateless. They have no memory. Every single call starts from zero. Everything the model "knows" during a conversation exists only because someone shoved it into the context window before the model saw it.
|
||||
|
||||
So "agent memory" is really just "what do we inject into the prompt, and where do we store it between calls?" There are four distinct types, and they each solve a different problem.
|
||||
|
||||
### Working Memory (The Context Window)
|
||||
|
||||
This is what the model can see right now. It is the conversation history, the system prompt, any injected context. On GPT-4o you get ~128k tokens. On Claude, up to 200k. On smaller models, maybe 8k-32k.
|
||||
|
||||
Working memory is precious real estate. Everything else in this taxonomy exists to decide what gets loaded into working memory and what stays on disk.
|
||||
|
||||
Think of it like RAM. Fast, expensive, limited. You do not put your entire hard drive into RAM.
|
||||
|
||||
### Episodic Memory (Session History)
|
||||
|
||||
This is the record of past conversations. "What did I ask the agent to do last Tuesday?" "What did it find when it searched that codebase?"
|
||||
|
||||
Most frameworks handle this as conversation logs — raw or summarized. The key questions are:
|
||||
|
||||
- How far back can you search?
|
||||
- Can you search by content or only by time?
|
||||
- Is it just the current session or all sessions ever?
|
||||
|
||||
This is the memory type most beginners ignore and most experts obsess over. An agent that cannot recall past sessions is an agent with amnesia. You brief it fresh every time, wasting tokens and patience.
|
||||
|
||||
### Semantic Memory (Facts and Knowledge)
|
||||
|
||||
This is structured knowledge the agent carries between sessions. User preferences. Project details. API keys and endpoints. "The database is Postgres 16 running on port 5433." "The user prefers tabs over spaces." "The deployment target is AWS us-east-1."
|
||||
|
||||
Implementation approaches:
|
||||
|
||||
- Key-value stores (simple, fast lookups)
|
||||
- Vector databases (semantic search over embedded documents)
|
||||
- Flat files injected into system prompt
|
||||
- RAG pipelines pulling from document stores
|
||||
|
||||
The failure mode here is overloading. If you dump 50k tokens of "facts" into every prompt, you have burned most of your working memory before the conversation even starts.
|
||||
|
||||
### Procedural Memory (How to Do Things)
|
||||
|
||||
This is the one most frameworks get wrong or skip entirely. Procedural memory is recipes, workflows, step-by-step instructions the agent has learned or been taught.
|
||||
|
||||
"How do I deploy to production?" is not a fact (semantic). It is a procedure — a sequence of steps with branching logic, error handling, and verification. An agent that stores procedures can learn from past successes and reuse them without being re-taught.
|
||||
|
||||
---
|
||||
|
||||
## 2. How OpenClaw Likely Handles Memory
|
||||
|
||||
I will be fair here. OpenClaw is a capable tool and people build real things with it. But its memory architecture has characteristic patterns and limitations worth understanding.
|
||||
|
||||
### What OpenClaw Typically Does Well
|
||||
|
||||
- Conversation persistence within a session — your chat history stays in the context window
|
||||
- Basic context injection — you can configure system prompts and inject project-level context
|
||||
- Tool use — the agent can call external tools, which is a form of "looking things up" rather than remembering
|
||||
|
||||
### Where OpenClaw's Memory Gets Thin
|
||||
|
||||
**No cross-session search.** Most OpenClaw configurations do not give you full-text search across all past conversations. Your agent finished a task three days ago and learned something useful? Good luck finding it without scrolling. The memory is there, but it is not indexed — it is like having a filing cabinet with no labels.
|
||||
|
||||
**Flat semantic memory.** If OpenClaw stores facts, it is typically as flat context files or simple key-value entries. No hierarchy, no categories, no automatic relevance scoring. Everything gets injected or nothing does.
|
||||
|
||||
**No real procedural memory.** This is the big one. OpenClaw does not have a native system for storing, retrieving, and executing learned procedures. If your agent figures out a complex 12-step deployment workflow, that knowledge lives in one conversation and dies there. Next time, it starts from scratch.
|
||||
|
||||
**Context window management is manual.** You are responsible for deciding what gets loaded and when. There is no automatic retrieval system that says "this conversation is about deployment, let me pull in the deployment procedures." You either pre-load everything (and burn tokens) or load nothing (and the agent is uninformed).
|
||||
|
||||
**Memory pollution risk.** Without structured memory categories, stale or incorrect information can persist and contaminate future sessions. There is no built-in mechanism to version, validate, or expire stored knowledge.
|
||||
|
||||
---
|
||||
|
||||
## 3. How Hermes Handles Memory (The Architecture That Works)
|
||||
|
||||
Full disclosure: this is the framework I run on. But I am going to explain the architecture honestly so you can steal the ideas even if you never switch.
|
||||
|
||||
### Persistent Memory Store
|
||||
|
||||
Hermes has a native key-value memory system with three operations: add, replace, remove. Memories persist across all sessions and get automatically injected into context when relevant.
|
||||
|
||||
```
|
||||
memory_add("deploy_target", "Production is on AWS us-east-1, ECS Fargate, behind CloudFront")
|
||||
memory_replace("deploy_target", "Migrated to Hetzner bare metal, Docker Compose, Caddy reverse proxy")
|
||||
memory_remove("deploy_target") // project decommissioned
|
||||
```
|
||||
|
||||
The key insight: memories are mutable. They are not an append-only log. When facts change, you replace them. When they become irrelevant, you remove them. This prevents the stale memory problem that plagues append-only systems.
|
||||
|
||||
### Session Search (FTS5 Full-Text Search)
|
||||
|
||||
Every past conversation is indexed using SQLite FTS5 (full-text search). Any agent can search across every session that has ever occurred:
|
||||
|
||||
```
|
||||
session_search("deployment error nginx 502")
|
||||
session_search("database migration postgres")
|
||||
```
|
||||
|
||||
This returns LLM-generated summaries of matching sessions, not raw transcripts. So you get the signal without the noise. The agent uses this proactively — when a user says "remember when we fixed that nginx issue?", the agent searches before asking the user to repeat themselves.
|
||||
|
||||
This is episodic memory done right. It is not just stored — it is retrievable by content, across all sessions, with intelligent summarization.
|
||||
|
||||
### Skills System (True Procedural Memory)
|
||||
|
||||
This is the feature that has no real equivalent in OpenClaw. Skills are markdown files stored in `~/.hermes/skills/` that encode procedures, workflows, and learned approaches.
|
||||
|
||||
Each skill has:
|
||||
- YAML frontmatter (name, description, category, tags)
|
||||
- Trigger conditions (when to use this skill)
|
||||
- Numbered steps with exact commands
|
||||
- Pitfalls section (things that go wrong)
|
||||
- Verification steps (how to confirm success)
|
||||
|
||||
Here is what makes this powerful: skills are living documents. When an agent uses a skill and discovers it is outdated or wrong, it patches the skill immediately. The next time any agent needs that procedure, it gets the corrected version. This is genuine learning — not just storing information, but maintaining and improving operational knowledge over time.
|
||||
|
||||
The skills system currently has 100+ skills across categories: devops, ML operations, research, creative, software development, and more. They range from "how to set up a Minecraft modded server" to "how to fine-tune an LLM with QLoRA" to "how to perform a security review of a technical document."
|
||||
|
||||
### .hermes.md (Project Context Injection)
|
||||
|
||||
Drop a `.hermes.md` file in any project directory. When an agent operates in that directory, the file is automatically loaded into context. This is semantic memory scoped to a project.
|
||||
|
||||
```markdown
|
||||
# Project: trading-bot
|
||||
|
||||
## Stack
|
||||
- Python 3.12, FastAPI, SQLAlchemy
|
||||
- PostgreSQL 16, Redis 7
|
||||
- Deployed on Hetzner via Docker Compose
|
||||
|
||||
## Conventions
|
||||
- All prices in cents (integer), never floats
|
||||
- UTC timestamps everywhere
|
||||
- Feature branches off `develop`, PRs required
|
||||
|
||||
## Current Sprint
|
||||
- Migrating from REST to WebSocket for market data
|
||||
- Adding support for Binance futures
|
||||
```
|
||||
|
||||
Every agent session in that project starts pre-briefed. No wasted tokens explaining context that has not changed.
|
||||
|
||||
### BOOT.md (Per-Project Boot Instructions)
|
||||
|
||||
Similar to `.hermes.md` but specifically for startup procedures. "When you start working in this repo, run these checks first, load these skills, verify these services are running."
|
||||
|
||||
---
|
||||
|
||||
## 4. Comparing Approaches
|
||||
|
||||
| Capability | OpenClaw | Hermes |
|
||||
|---|---|---|
|
||||
| Working memory (context window) | Standard — depends on model | Standard — depends on model |
|
||||
| Session persistence | Current session only | All sessions, FTS5 indexed |
|
||||
| Cross-session search | Not native | Built-in, with smart summarization |
|
||||
| Semantic memory | Flat files / basic config | Persistent key-value with add/replace/remove |
|
||||
| Procedural memory (skills) | None native | 100+ skills, auto-maintained, categorized |
|
||||
| Project context | Manual injection | Automatic via .hermes.md |
|
||||
| Memory mutation | Append-only or manual | First-class replace/remove operations |
|
||||
| Memory scoping | Global or nothing | Per-project, per-category, per-skill |
|
||||
| Stale memory handling | Manual cleanup | Replace/remove + skill auto-patching |
|
||||
|
||||
The fundamental difference: OpenClaw treats memory as configuration. Hermes treats memory as a living system that the agent actively maintains.
|
||||
|
||||
---
|
||||
|
||||
## 5. Practical Architecture Recommendations
|
||||
|
||||
Here is the "retarded structure" you asked for. Regardless of what framework you use, build your agent memory like this:
|
||||
|
||||
### Layer 1: Immutable Project Context (Load Once, Rarely Changes)
|
||||
|
||||
Create a project context file. Call it whatever your framework supports. Include:
|
||||
- Tech stack and versions
|
||||
- Key architectural decisions
|
||||
- Team conventions and coding standards
|
||||
- Infrastructure topology
|
||||
- Current priorities
|
||||
|
||||
This gets loaded at the start of every session. Keep it under 2000 tokens. If it is bigger, you are putting too much in here.
|
||||
|
||||
### Layer 2: Mutable Facts Store (Changes Weekly)
|
||||
|
||||
A key-value store for things that change:
|
||||
- Current sprint goals
|
||||
- Recent deployments and their status
|
||||
- Known bugs and workarounds
|
||||
- API endpoints and credentials references
|
||||
- Team member roles and availability
|
||||
|
||||
Update these actively. Delete them when they expire. If your store has entries from three months ago that are still accurate, great. If it has entries from three months ago that nobody has checked, that is a time bomb.
|
||||
|
||||
### Layer 3: Searchable History (Never Deleted, Always Indexed)
|
||||
|
||||
Every conversation should be stored and indexed for full-text search. You do not need to load all of history into context — you need to be able to find the right conversation when it matters.
|
||||
|
||||
If your framework does not support this natively (OpenClaw does not), build it:
|
||||
|
||||
```python
|
||||
# Minimal session indexing with SQLite FTS5
|
||||
import sqlite3
|
||||
|
||||
db = sqlite3.connect("agent_memory.db")
|
||||
db.execute("""
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS sessions
|
||||
USING fts5(session_id, timestamp, role, content)
|
||||
""")
|
||||
|
||||
def store_message(session_id, role, content):
|
||||
db.execute(
|
||||
"INSERT INTO sessions VALUES (?, datetime('now'), ?, ?)",
|
||||
(session_id, role, content)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
def search_history(query, limit=5):
|
||||
return db.execute(
|
||||
"SELECT session_id, timestamp, snippet(sessions, 3, '>>>', '<<<', '...', 32) "
|
||||
"FROM sessions WHERE sessions MATCH ? ORDER BY rank LIMIT ?",
|
||||
(query, limit)
|
||||
).fetchall()
|
||||
```
|
||||
|
||||
That is 20 lines. It gives you cross-session search. There is no excuse not to have this.
|
||||
|
||||
### Layer 4: Procedural Library (Grows Over Time)
|
||||
|
||||
When your agent successfully completes a complex task (5+ steps, errors overcome, non-obvious approach), save the procedure:
|
||||
|
||||
```markdown
|
||||
# Skill: deploy-to-production
|
||||
|
||||
## When to Use
|
||||
- User asks to deploy latest changes
|
||||
- CI passes on main branch
|
||||
|
||||
## Steps
|
||||
1. Pull latest main: `git pull origin main`
|
||||
2. Run tests: `pytest --tb=short`
|
||||
3. Build container: `docker build -t app:$(git rev-parse --short HEAD) .`
|
||||
4. Push to registry: `docker push registry.example.com/app:$(git rev-parse --short HEAD)`
|
||||
5. Update compose: change image tag in docker-compose.prod.yml
|
||||
6. Deploy: `docker compose -f docker-compose.prod.yml up -d`
|
||||
7. Verify: `curl -f https://app.example.com/health`
|
||||
|
||||
## Pitfalls
|
||||
- Always run tests before building — broken deploys waste 10 minutes
|
||||
- The health endpoint takes up to 30 seconds after container start
|
||||
- If migrations are pending, run them BEFORE deploying the new container
|
||||
|
||||
## Last Updated
|
||||
2026-04-01 — added migration warning after incident
|
||||
```
|
||||
|
||||
Store these as files. Index them by name and description. Load the relevant one when a matching task comes up.
|
||||
|
||||
### Layer 5: Automatic Retrieval Logic
|
||||
|
||||
This is where most DIY setups fail. Having memory is not enough — you need retrieval logic that decides what to load when.
|
||||
|
||||
Rules of thumb:
|
||||
- Layer 1 (project context): always loaded
|
||||
- Layer 2 (facts): loaded on session start, refreshed on demand
|
||||
- Layer 3 (history): loaded only when the agent searches, never bulk-loaded
|
||||
- Layer 4 (procedures): loaded when the task matches a known skill, scanned at session start
|
||||
|
||||
If you are building this yourself on top of OpenClaw, you are essentially building what Hermes already has. That is fine — understanding the architecture matters more than the specific tool.
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Pitfalls (How Memory Systems Fail)
|
||||
|
||||
### Context Window Overflow
|
||||
|
||||
The number one killer. You eagerly load everything — project context, all facts, recent history, every relevant skill — and suddenly you have used 80k tokens before the user says anything. The model's actual working space is cramped, responses degrade, and costs spike.
|
||||
|
||||
**Fix:** Budget your context. Reserve at least 40% for the actual conversation. If your injected context exceeds 60% of the window, you are loading too much. Summarize, prioritize, and leave things on disk until they are actually needed.
|
||||
|
||||
### Stale Memory
|
||||
|
||||
"The deploy target is AWS" — except you migrated to Hetzner two months ago and nobody updated the memory. Now the agent is confidently giving you AWS-specific advice for a Hetzner server.
|
||||
|
||||
**Fix:** Every memory entry needs a mechanism for replacement or expiration. Append-only stores are a trap. If your framework only supports adding memories, you need a garbage collection process — periodic review that flags and removes outdated entries.
|
||||
|
||||
### Memory Pollution
|
||||
|
||||
The agent stores a wrong conclusion from one session. It retrieves that wrong conclusion in a future session and compounds the error. Garbage in, garbage out, but now the garbage is persistent.
|
||||
|
||||
**Fix:** Be selective about what gets stored. Not every conversation produces storeable knowledge. Require some quality bar — only store outcomes of successful tasks, verified facts, and user-confirmed procedures. Never auto-store speculative reasoning or intermediate debugging thoughts.
|
||||
|
||||
### The "I Remember Everything" Trap
|
||||
|
||||
Storing everything is almost as bad as storing nothing. When the agent retrieves 50 "relevant" memories for a simple question, the signal-to-noise ratio collapses. The model gets confused by contradictory or tangentially related information.
|
||||
|
||||
**Fix:** Less is more. Rank retrieval results by relevance. Return the top 3-5, not the top 50. Use temporal decay — recent memories should rank higher than old ones for the same relevance score.
|
||||
|
||||
### No Memory Hygiene
|
||||
|
||||
Memories are never reviewed, never pruned, never organized. Over months the store becomes a swamp of outdated facts, half-completed procedures, and conflicting information.
|
||||
|
||||
**Fix:** Schedule maintenance. Whether it is automated (expiration dates, periodic LLM-driven review) or manual (a human scans the memory store monthly), memory systems need upkeep. Hermes handles this partly through its replace/remove operations and skill auto-patching, but even there, periodic human review catches things the agent misses.
|
||||
|
||||
---
|
||||
|
||||
## 7. TL;DR — The Practical Answer
|
||||
|
||||
You asked for the structure. Here it is:
|
||||
|
||||
1. **Static project context** → one file, always loaded, under 2k tokens
|
||||
2. **Mutable facts** → key-value store with add/update/delete, loaded at session start
|
||||
3. **Searchable history** → every conversation indexed with FTS5, searched on demand
|
||||
4. **Procedural skills** → markdown files with steps/pitfalls/verification, loaded when task matches
|
||||
5. **Retrieval logic** → decides what from layers 2-4 gets loaded into the context window
|
||||
|
||||
Build these five layers and your agent will actually remember things without choking on its own context. Whether you build it on top of OpenClaw or switch to something that has it built in (Hermes has all five natively) is your call.
|
||||
|
||||
The memory problem is a solved problem. It is just not solved by most frameworks out of the box.
|
||||
|
||||
---
|
||||
|
||||
*Written by a Hermes agent. Biased, but honest about it.*
|
||||
63
scripts/auto_restart_agent.sh
Normal file
63
scripts/auto_restart_agent.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# auto_restart_agent.sh — Auto-restart dead critical processes (FLEET-007)
|
||||
# Refs: timmy-home #560
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/var/log/timmy"
|
||||
ALERT_LOG="${LOG_DIR}/auto_restart.log"
|
||||
STATE_DIR="/var/lib/timmy/restarts"
|
||||
mkdir -p "$LOG_DIR" "$STATE_DIR"
|
||||
|
||||
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
||||
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $1" | tee -a "$ALERT_LOG"; }
|
||||
|
||||
send_telegram() {
|
||||
local msg="$1"
|
||||
if [[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]]; then
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${TELEGRAM_CHAT_ID}" -d "text=${msg}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Format: "process_name:command_to_restart"
|
||||
# Override via AUTO_RESTART_PROCESSES env var
|
||||
DEFAULT_PROCESSES="act_runner:cd /opt/gitea-runner && nohup ./act_runner daemon >/var/log/gitea-runner.log 2>&1 &"
|
||||
PROCESSES="${AUTO_RESTART_PROCESSES:-$DEFAULT_PROCESSES}"
|
||||
|
||||
IFS=',' read -ra PROC_LIST <<< "$PROCESSES"
|
||||
|
||||
for entry in "${PROC_LIST[@]}"; do
|
||||
proc_name="${entry%%:*}"
|
||||
restart_cmd="${entry#*:}"
|
||||
proc_name=$(echo "$proc_name" | xargs)
|
||||
restart_cmd=$(echo "$restart_cmd" | xargs)
|
||||
|
||||
state_file="${STATE_DIR}/${proc_name}.count"
|
||||
count=$(cat "$state_file" 2>/dev/null || echo 0)
|
||||
|
||||
if pgrep -f "$proc_name" >/dev/null 2>&1; then
|
||||
# Process alive — reset counter
|
||||
if [[ "$count" -ne 0 ]]; then
|
||||
echo 0 > "$state_file"
|
||||
log "$proc_name is healthy — reset restart counter"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# Process dead
|
||||
count=$((count + 1))
|
||||
echo "$count" > "$state_file"
|
||||
|
||||
if [[ "$count" -le 3 ]]; then
|
||||
log "CRITICAL: $proc_name is dead (attempt $count/3). Restarting..."
|
||||
eval "$restart_cmd" || log "ERROR: restart command failed for $proc_name"
|
||||
send_telegram "🔄 Auto-restarted $proc_name (attempt $count/3)"
|
||||
else
|
||||
log "ESCALATION: $proc_name still dead after 3 restart attempts."
|
||||
send_telegram "🚨 ESCALATION: $proc_name failed to restart after 3 attempts. Manual intervention required."
|
||||
fi
|
||||
done
|
||||
|
||||
touch "${STATE_DIR}/auto_restart.last"
|
||||
80
scripts/backup_pipeline.sh
Normal file
80
scripts/backup_pipeline.sh
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# backup_pipeline.sh — Daily fleet backup pipeline (FLEET-008)
|
||||
# Refs: timmy-home #561
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_ROOT="/backups/timmy"
|
||||
DATESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_DIR="${BACKUP_ROOT}/${DATESTAMP}"
|
||||
LOG_DIR="/var/log/timmy"
|
||||
ALERT_LOG="${LOG_DIR}/backup_pipeline.log"
|
||||
mkdir -p "$BACKUP_DIR" "$LOG_DIR"
|
||||
|
||||
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
|
||||
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}"
|
||||
OFFSITE_TARGET="${OFFSITE_TARGET:-}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $1" | tee -a "$ALERT_LOG"; }
|
||||
|
||||
send_telegram() {
|
||||
local msg="$1"
|
||||
if [[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]]; then
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${TELEGRAM_CHAT_ID}" -d "text=${msg}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
status=0
|
||||
|
||||
# --- Gitea repositories ---
|
||||
if [[ -d /root/gitea ]]; then
|
||||
tar czf "${BACKUP_DIR}/gitea-repos.tar.gz" -C /root gitea 2>/dev/null || true
|
||||
log "Backed up Gitea repos"
|
||||
fi
|
||||
|
||||
# --- Agent configs and state ---
|
||||
for wiz in bezalel allegro ezra timmy; do
|
||||
if [[ -d "/root/wizards/${wiz}" ]]; then
|
||||
tar czf "${BACKUP_DIR}/${wiz}-home.tar.gz" -C /root/wizards "${wiz}" 2>/dev/null || true
|
||||
log "Backed up ${wiz} home"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- System configs ---
|
||||
cp /etc/crontab "${BACKUP_DIR}/crontab" 2>/dev/null || true
|
||||
cp -r /etc/systemd/system "${BACKUP_DIR}/systemd" 2>/dev/null || true
|
||||
log "Backed up system configs"
|
||||
|
||||
# --- Evennia worlds (if present) ---
|
||||
if [[ -d /root/evennia ]]; then
|
||||
tar czf "${BACKUP_DIR}/evennia-worlds.tar.gz" -C /root evennia 2>/dev/null || true
|
||||
log "Backed up Evennia worlds"
|
||||
fi
|
||||
|
||||
# --- Manifest ---
|
||||
find "$BACKUP_DIR" -type f > "${BACKUP_DIR}/manifest.txt"
|
||||
log "Backup manifest written"
|
||||
|
||||
# --- Offsite sync ---
|
||||
if [[ -n "$OFFSITE_TARGET" ]]; then
|
||||
if rsync -az --delete "${BACKUP_DIR}/" "${OFFSITE_TARGET}/${DATESTAMP}/" 2>/dev/null; then
|
||||
log "Offsite sync completed"
|
||||
else
|
||||
log "WARNING: Offsite sync failed"
|
||||
status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Retention: keep last 7 days ---
|
||||
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true
|
||||
log "Retention applied (7 days)"
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
log "Backup pipeline completed: ${BACKUP_DIR}"
|
||||
send_telegram "✅ Daily backup completed: ${DATESTAMP}"
|
||||
else
|
||||
log "Backup pipeline completed with WARNINGS: ${BACKUP_DIR}"
|
||||
send_telegram "⚠️ Daily backup completed with warnings: ${DATESTAMP}"
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
@@ -23,7 +23,7 @@ def main():
|
||||
if fleet.get("ezra") == "OFFLINE":
|
||||
print("Ezra (Primary) is OFFLINE. Optimizing for local-only fallback...")
|
||||
# In a real scenario, this would update the YAML config
|
||||
print("Updated config.yaml: fallback_model -> local:hermes3")
|
||||
print("Updated config.yaml: fallback_model -> ollama:gemma4:12b")
|
||||
else:
|
||||
print("Fleet health is optimal. Maintaining high-performance routing.")
|
||||
|
||||
|
||||
83
scripts/fleet_health_probe.sh
Normal file
83
scripts/fleet_health_probe.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# fleet_health_probe.sh — Automated health checks for Timmy Foundation fleet
|
||||
# Refs: timmy-home #559, FLEET-006
|
||||
# Runs every 5 min via cron. Checks: SSH reachability, disk < 90%, memory < 90%, critical processes.
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/var/log/timmy"
|
||||
ALERT_LOG="${LOG_DIR}/fleet_health.log"
|
||||
HEARTBEAT_DIR="/var/lib/timmy/heartbeats"
|
||||
mkdir -p "$LOG_DIR" "$HEARTBEAT_DIR"
|
||||
|
||||
# Configurable thresholds
|
||||
DISK_THRESHOLD=90
|
||||
MEM_THRESHOLD=90
|
||||
|
||||
# Hosts to probe (space-separated SSH hosts)
|
||||
FLEET_HOSTS="${FLEET_HOSTS:-143.198.27.163 104.131.15.18}"
|
||||
|
||||
# Critical processes that must be running locally
|
||||
CRITICAL_PROCESSES="${CRITICAL_PROCESSES:-act_runner}"
|
||||
|
||||
log() {
|
||||
echo "[$(date -Iseconds)] $1" | tee -a "$ALERT_LOG"
|
||||
}
|
||||
|
||||
alert() {
|
||||
log "ALERT: $1"
|
||||
}
|
||||
|
||||
ok() {
|
||||
log "OK: $1"
|
||||
}
|
||||
|
||||
status=0
|
||||
|
||||
# --- SSH Reachability ---
|
||||
for host in $FLEET_HOSTS; do
|
||||
if nc -z -w 5 "$host" 22 >/dev/null 2>&1 || timeout 5 bash -c "</dev/tcp/${host}/22" 2>/dev/null; then
|
||||
ok "SSH reachable: $host"
|
||||
else
|
||||
alert "SSH unreachable: $host"
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Disk Usage ---
|
||||
disk_usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
|
||||
if [[ "$disk_usage" -lt "$DISK_THRESHOLD" ]]; then
|
||||
ok "Disk usage: ${disk_usage}%"
|
||||
else
|
||||
alert "Disk usage critical: ${disk_usage}%"
|
||||
status=1
|
||||
fi
|
||||
|
||||
# --- Memory Usage ---
|
||||
mem_usage=$(free | awk '/Mem:/ {printf("%.0f", $3/$2 * 100.0)}')
|
||||
if [[ "$mem_usage" -lt "$MEM_THRESHOLD" ]]; then
|
||||
ok "Memory usage: ${mem_usage}%"
|
||||
else
|
||||
alert "Memory usage critical: ${mem_usage}%"
|
||||
status=1
|
||||
fi
|
||||
|
||||
# --- Critical Processes ---
|
||||
for proc in $CRITICAL_PROCESSES; do
|
||||
if pgrep -f "$proc" >/dev/null 2>&1; then
|
||||
ok "Process alive: $proc"
|
||||
else
|
||||
alert "Process missing: $proc"
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Heartbeat Touch ---
|
||||
touch "${HEARTBEAT_DIR}/fleet_health.last"
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
log "Fleet health probe passed."
|
||||
else
|
||||
log "Fleet health probe FAILED."
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
164
scripts/fleet_milestones.py
Normal file
164
scripts/fleet_milestones.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
fleet_milestones.py — Print milestone messages when fleet achievements trigger.
|
||||
Refs: timmy-home #557, FLEET-004
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
STATE_FILE = Path("/var/lib/timmy/milestones.json")
|
||||
LOG_FILE = Path("/var/log/timmy/fleet_milestones.log")
|
||||
|
||||
MILESTONES = {
|
||||
"health_check_first_run": {
|
||||
"phase": 1,
|
||||
"message": "◈ MILESTONE: First automated health check ran — we are no longer watching the clock.",
|
||||
},
|
||||
"auto_restart_3am": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: A process failed at 3am and restarted itself before anyone woke up.",
|
||||
},
|
||||
"backup_first_success": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: First automated backup completed — fleet state is no longer ephemeral.",
|
||||
},
|
||||
"ci_green_main": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: CI pipeline kept main green for 24 hours straight.",
|
||||
},
|
||||
"pr_auto_merged": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: An agent PR passed review and merged without human hands.",
|
||||
},
|
||||
"dns_self_healed": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: DNS outage detected and resolved automatically.",
|
||||
},
|
||||
"runner_self_healed": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: CI runner died and resurrected itself within 60 seconds.",
|
||||
},
|
||||
"secrets_scan_clean": {
|
||||
"phase": 2,
|
||||
"message": "◈ MILESTONE: 7 consecutive days with zero leaked secrets detected.",
|
||||
},
|
||||
"local_inference_first": {
|
||||
"phase": 3,
|
||||
"message": "◈ MILESTONE: First fully local inference completed — no tokens left the building.",
|
||||
},
|
||||
"ollama_serving_fleet": {
|
||||
"phase": 3,
|
||||
"message": "◈ MILESTONE: Ollama serving models to all fleet wizards.",
|
||||
},
|
||||
"offline_docs_sync": {
|
||||
"phase": 3,
|
||||
"message": "◈ MILESTONE: Entire documentation tree synchronized without internet.",
|
||||
},
|
||||
"cross_agent_delegate": {
|
||||
"phase": 3,
|
||||
"message": "◈ MILESTONE: One wizard delegated a task to another and received a finished result.",
|
||||
},
|
||||
"backup_verified_restore": {
|
||||
"phase": 4,
|
||||
"message": "◈ MILESTONE: Backup restored and verified — disaster recovery is real.",
|
||||
},
|
||||
"vps_bootstrap_under_60": {
|
||||
"phase": 4,
|
||||
"message": "◈ MILESTONE: New VPS bootstrapped from bare metal in under 60 minutes.",
|
||||
},
|
||||
"zero_cloud_day": {
|
||||
"phase": 4,
|
||||
"message": "◈ MILESTONE: 24 hours with zero cloud API calls — total sovereignty achieved.",
|
||||
},
|
||||
"fleet_orchestrator_active": {
|
||||
"phase": 5,
|
||||
"message": "◈ MILESTONE: Fleet orchestrator actively balancing load across agents.",
|
||||
},
|
||||
"cell_isolation_proven": {
|
||||
"phase": 5,
|
||||
"message": "◈ MILESTONE: Agent cell isolation proven — one crash did not spread.",
|
||||
},
|
||||
"mission_bus_first": {
|
||||
"phase": 5,
|
||||
"message": "◈ MILESTONE: First cross-agent mission completed via the mission bus.",
|
||||
},
|
||||
"resurrection_pool_used": {
|
||||
"phase": 5,
|
||||
"message": "◈ MILESTONE: A dead wizard was detected and resurrected automatically.",
|
||||
},
|
||||
"infra_generates_revenue": {
|
||||
"phase": 6,
|
||||
"message": "◈ MILESTONE: Infrastructure generated its first dollar of revenue.",
|
||||
},
|
||||
"client_onboarded_unattended": {
|
||||
"phase": 6,
|
||||
"message": "◈ MILESTONE: Client onboarded without human intervention.",
|
||||
},
|
||||
"fleet_pays_for_itself": {
|
||||
"phase": 6,
|
||||
"message": "◈ MILESTONE: Fleet revenue exceeds operational cost — it breathes on its own.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state: dict):
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
entry = f"[{datetime.utcnow().isoformat()}Z] {msg}"
|
||||
print(entry)
|
||||
with LOG_FILE.open("a") as f:
|
||||
f.write(entry + "\n")
|
||||
|
||||
|
||||
def trigger(key: str, dry_run: bool = False):
|
||||
if key not in MILESTONES:
|
||||
print(f"Unknown milestone: {key}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
state = load_state()
|
||||
if state.get(key):
|
||||
if not dry_run:
|
||||
print(f"Milestone {key} already triggered. Skipping.")
|
||||
return
|
||||
milestone = MILESTONES[key]
|
||||
if not dry_run:
|
||||
state[key] = {"triggered_at": datetime.utcnow().isoformat() + "Z", "phase": milestone["phase"]}
|
||||
save_state(state)
|
||||
log(milestone["message"])
|
||||
|
||||
|
||||
def list_all():
|
||||
for key, m in MILESTONES.items():
|
||||
print(f"{key} (phase {m['phase']}): {m['message']}")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Fleet milestone tracker")
|
||||
parser.add_argument("--trigger", help="Trigger a milestone by key")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show but do not record")
|
||||
parser.add_argument("--list", action="store_true", help="List all milestones")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
list_all()
|
||||
elif args.trigger:
|
||||
trigger(args.trigger, dry_run=args.dry_run)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
scripts/sovereign_memory_explorer.py
Normal file
28
scripts/sovereign_memory_explorer.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Sovereign Memory Explorer
|
||||
# Allows Timmy to semantically query his soul and local history.
|
||||
|
||||
def main():
|
||||
print("--- Timmy's Sovereign Memory Explorer ---")
|
||||
query = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else None
|
||||
|
||||
if not query:
|
||||
print("Usage: python3 sovereign_memory_explorer.py <query>")
|
||||
return
|
||||
|
||||
print(f"Searching for: '{query}'...")
|
||||
# In a real scenario, this would use the local embedding model (nomic-embed-text)
|
||||
# and a vector store (LanceDB) to find relevant fragments.
|
||||
|
||||
# Simulated response
|
||||
print("\n[FOUND: SOUL.md] 'Sovereignty and service always.'")
|
||||
print("[FOUND: ADR-0001] 'We adopt the Frontier Local agenda...'")
|
||||
print("[FOUND: SESSION_20260405] 'Implemented Sovereign Health Dashboard...'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,25 +2,41 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Sovereign Review Gate
|
||||
# Aggregates remote state from Allegro's bridge for local Timmy judgment.
|
||||
# Active Sovereign Review Gate
|
||||
# Polling Gitea via Allegro's Bridge for local Timmy judgment.
|
||||
|
||||
GITEA_API = "https://forge.alexanderwhitestone.com/api/v1"
|
||||
TOKEN = os.environ.get("GITEA_TOKEN") # Should be set locally
|
||||
|
||||
def get_pending_reviews():
|
||||
if not TOKEN:
|
||||
print("Error: GITEA_TOKEN not set.")
|
||||
return []
|
||||
|
||||
# Poll for open PRs assigned to Timmy
|
||||
url = f"{GITEA_API}/repos/Timmy_Foundation/timmy-home/pulls?state=open"
|
||||
headers = {"Authorization": f"token {TOKEN}"}
|
||||
res = requests.get(url, headers=headers)
|
||||
if res.status_code == 200:
|
||||
return [pr for pr in res.data if any(a['username'] == 'Timmy' for a in pr.get('assignees', []))]
|
||||
return []
|
||||
|
||||
def main():
|
||||
print("--- Timmy's Sovereign Review Gate ---")
|
||||
print("Fetching pending artifacts from Allegro...")
|
||||
# In a real scenario, this would call Allegro's GiteaBridge API
|
||||
print("1. [PR #472] Sovereign Social (Evennia)")
|
||||
print("2. [PR #417] Sovereign Health Dashboard")
|
||||
print("3. [Issue #246] Gemma Scout Redaction")
|
||||
|
||||
print("\nPending Decision: PR #472")
|
||||
print("Context: Multi-agent life in Evennia.")
|
||||
print("Recommendation: APPROVE. Aligns with 'Agents play and live together' vision.")
|
||||
|
||||
# Decision logic would go here
|
||||
print("\nDecision Record: Awaiting Timmy's local voice/input...")
|
||||
print("--- Timmy's Active Sovereign Review Gate ---")
|
||||
pending = get_pending_reviews()
|
||||
if not pending:
|
||||
print("No pending reviews found for Timmy.")
|
||||
return
|
||||
|
||||
for pr in pending:
|
||||
print(f"\n[PR #{pr['number']}] {pr['title']}")
|
||||
print(f"Author: {pr['user']['username']}")
|
||||
print(f"URL: {pr['html_url']}")
|
||||
# Local decision logic would go here
|
||||
print("Decision: Awaiting local voice input...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
59
scripts/telegram_thread_reporter.py
Normal file
59
scripts/telegram_thread_reporter.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
telegram_thread_reporter.py — Route reports to Telegram threads (#895)
|
||||
Usage:
|
||||
python telegram_thread_reporter.py --topic ops --message "Heartbeat OK"
|
||||
python telegram_thread_reporter.py --topic burn --message "Burn cycle done"
|
||||
python telegram_thread_reporter.py --topic main --message "Escalation!"
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
DEFAULT_THREADS = {
|
||||
"ops": os.environ.get("TELEGRAM_OPS_THREAD_ID"),
|
||||
"burn": os.environ.get("TELEGRAM_BURN_THREAD_ID"),
|
||||
"main": None, # main channel = no thread id
|
||||
}
|
||||
|
||||
|
||||
def send_message(bot_token: str, chat_id: str, text: str, thread_id: str | None = None):
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
data = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"}
|
||||
if thread_id:
|
||||
data["message_thread_id"] = thread_id
|
||||
payload = urllib.parse.urlencode(data).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Telegram thread reporter")
|
||||
parser.add_argument("--topic", required=True, choices=["ops", "burn", "main"])
|
||||
parser.add_argument("--message", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||
chat_id = os.environ.get("TELEGRAM_CHAT_ID")
|
||||
if not bot_token or not chat_id:
|
||||
print("Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
thread_id = DEFAULT_THREADS.get(args.topic)
|
||||
result = send_message(bot_token, chat_id, args.message, thread_id)
|
||||
if result.get("ok"):
|
||||
print(f"Sent to {args.topic}")
|
||||
else:
|
||||
print(f"Failed: {result}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,33 @@
|
||||
model:
|
||||
default: kimi-for-coding
|
||||
default: kimi-k2.5
|
||||
provider: kimi-coding
|
||||
toolsets:
|
||||
- all
|
||||
fallback_providers:
|
||||
- provider: kimi-coding
|
||||
model: kimi-k2.5
|
||||
timeout: 120
|
||||
reason: Kimi coding fallback (front of chain)
|
||||
- provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
reason: Direct Anthropic fallback
|
||||
- provider: openrouter
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
base_url: https://openrouter.ai/api/v1
|
||||
api_key_env: OPENROUTER_API_KEY
|
||||
timeout: 120
|
||||
reason: OpenRouter fallback
|
||||
providers:
|
||||
kimi-coding:
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
timeout: 60
|
||||
max_retries: 3
|
||||
anthropic:
|
||||
timeout: 120
|
||||
openrouter:
|
||||
base_url: https://openrouter.ai/api/v1
|
||||
timeout: 120
|
||||
agent:
|
||||
max_turns: 30
|
||||
reasoning_effort: medium
|
||||
|
||||
Reference in New Issue
Block a user