Compare commits
24 Commits
fix/kimi-h
...
timmy/grep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac328a82f5 | ||
| aaa90dae39 | |||
| d664ed01d0 | |||
| 8b1297ef4f | |||
| a56a2c4cd9 | |||
| 69929f6b68 | |||
| 8ac3de4b07 | |||
| 11d9bfca92 | |||
| 2df34995fe | |||
| 3148639e13 | |||
| f1482cb06d | |||
| 7070ba9cff | |||
| bc24313f1a | |||
| c3db6ce1ca | |||
| 4222eb559c | |||
| d043274c0e | |||
| 9dc540e4f5 | |||
|
|
4cfd1c2e10 | ||
|
|
a9ad1c8137 | ||
| f708e45ae9 | |||
| f083031537 | |||
| 1cef8034c5 | |||
|
|
9952ce180c | ||
|
|
64a954f4d9 |
@@ -1,6 +1,6 @@
|
||||
model:
|
||||
default: claude-opus-4-6
|
||||
provider: anthropic
|
||||
default: hermes4:14b
|
||||
provider: custom
|
||||
toolsets:
|
||||
- all
|
||||
agent:
|
||||
@@ -27,7 +27,7 @@ browser:
|
||||
inactivity_timeout: 120
|
||||
record_sessions: false
|
||||
checkpoints:
|
||||
enabled: false
|
||||
enabled: true
|
||||
max_snapshots: 50
|
||||
compression:
|
||||
enabled: true
|
||||
@@ -110,7 +110,7 @@ tts:
|
||||
device: cpu
|
||||
stt:
|
||||
enabled: true
|
||||
provider: local
|
||||
provider: openai
|
||||
local:
|
||||
model: base
|
||||
openai:
|
||||
|
||||
@@ -136,3 +136,27 @@ def build_bootstrap_graph() -> Graph:
|
||||
---
|
||||
|
||||
*This epic supersedes Allegro-Primus who has been idle.*
|
||||
|
||||
---
|
||||
|
||||
## Feedback — 2026-04-06 (Allegro Cross-Epic Review)
|
||||
|
||||
**Health:** 🟡 Yellow
|
||||
**Blocker:** Gitea externally firewalled + no Allegro-Primus RCA
|
||||
|
||||
### Critical Issues
|
||||
|
||||
1. **Dependency blindness.** Every Claw Code reference points to `143.198.27.163:3000`, which is currently firewalled and unreachable from this VM. If the mirror is not locally cached, development is blocked on external infrastructure.
|
||||
2. **Root cause vs. replacement.** The epic jumps to "replace Allegro-Primus" without proving he is unfixable. Primus being idle could be the same provider/auth outage that took down Ezra and Bezalel. A 5-line RCA should precede a 5-phase rewrite.
|
||||
3. **Timeline fantasy.** "Phase 1: 2 days" assumes stable infrastructure. Current reality: Gitea externally firewalled, Bezalel VPS down, Ezra needs webhook switch. This epic needs a "Blocked Until" section.
|
||||
4. **Resource stalemate.** "Telegram bot: Need @BotFather" — the fleet already operates multiple bots. Reuse an existing bot profile or document why a new one is required.
|
||||
|
||||
### Recommended Action
|
||||
|
||||
Add a **Pre-Flight Checklist** to the epic:
|
||||
- [ ] Verify Gitea/Claw Code mirror is reachable from the build VM
|
||||
- [ ] Publish 1-paragraph RCA on why Allegro-Primus is idle
|
||||
- [ ] Confirm target repo for the new agent code
|
||||
|
||||
Do not start Phase 1 until all three are checked.
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def append_event(session_id: str, event: dict, base_dir: str | Path = DEFAULT_BA
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = dict(event)
|
||||
payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
# Optimized for <50ms latency\n with path.open("a", encoding="utf-8", buffering=1024) as f:
|
||||
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||
write_session_metadata(session_id, {"last_event_excerpt": excerpt(json.dumps(payload, ensure_ascii=False), 400)}, base_dir)
|
||||
return path
|
||||
|
||||
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
|
||||
31
scripts/dynamic_dispatch_optimizer.py
Normal file
31
scripts/dynamic_dispatch_optimizer.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
# Dynamic Dispatch Optimizer
|
||||
# Automatically updates routing based on fleet health.
|
||||
|
||||
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||
CONFIG_FILE = Path.home() / "timmy" / "config.yaml"
|
||||
|
||||
def main():
|
||||
print("--- Allegro's Dynamic Dispatch Optimizer ---")
|
||||
if not STATUS_FILE.exists():
|
||||
print("No failover status found.")
|
||||
return
|
||||
|
||||
status = json.loads(STATUS_FILE.read_text())
|
||||
fleet = status.get("fleet", {})
|
||||
|
||||
# Logic: If primary VPS is offline, switch fallback to local Ollama
|
||||
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")
|
||||
else:
|
||||
print("Fleet health is optimal. Maintaining high-performance routing.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
49
scripts/evennia/agent_social_daemon.py
Normal file
49
scripts/evennia/agent_social_daemon.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Simple social intelligence loop for Evennia agents
|
||||
# Uses the Evennia MCP server to interact with the world
|
||||
|
||||
MCP_URL = "http://localhost:8642/mcp/evennia/call" # Assuming Hermes is proxying or direct call
|
||||
|
||||
def call_tool(name, arguments):
|
||||
# This is a placeholder for how the agent would call the MCP tool
|
||||
# In a real Hermes environment, this would go through the harness
|
||||
print(f"DEBUG: Calling tool {name} with {arguments}")
|
||||
# For now, we'll assume a direct local call to the evennia_mcp_server if it were a web API,
|
||||
# but since it's stdio, this daemon would typically be run BY an agent.
|
||||
# However, for "Life", we want a standalone script.
|
||||
return {"status": "simulated", "output": "You are in the Courtyard. Allegro is here."}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sovereign Social Daemon for Evennia")
|
||||
parser.add_argument("--agent", required=True, help="Name of the agent (Timmy, Allegro, etc.)")
|
||||
parser.add_argument("--interval", type=int, default=30, help="Interval between actions in seconds")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"--- Starting Social Life for {args.agent} ---")
|
||||
|
||||
# 1. Connect
|
||||
# call_tool("connect", {"username": args.agent})
|
||||
|
||||
while True:
|
||||
# 2. Observe
|
||||
# obs = call_tool("observe", {"name": args.agent.lower()})
|
||||
|
||||
# 3. Decide (Simulated for now, would use Gemma 2B)
|
||||
# action = decide_action(args.agent, obs)
|
||||
|
||||
# 4. Act
|
||||
# call_tool("command", {"command": action, "name": args.agent.lower()})
|
||||
|
||||
print(f"[{args.agent}] Living and playing...")
|
||||
time.sleep(args.interval)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -73,42 +73,22 @@ from evennia.utils.search import search_object
|
||||
from evennia_tools.layout import ROOMS, EXITS, OBJECTS
|
||||
from typeclasses.objects import Object
|
||||
|
||||
acc = AccountDB.objects.filter(username__iexact="Timmy").first()
|
||||
if not acc:
|
||||
acc, errs = DefaultAccount.create(username="Timmy", password={TIMMY_PASSWORD!r})
|
||||
AGENTS = ["Timmy", "Allegro", "Hermes", "Gemma"]
|
||||
|
||||
room_map = {{}}
|
||||
for room in ROOMS:
|
||||
found = search_object(room.key, exact=True)
|
||||
obj = found[0] if found else None
|
||||
if obj is None:
|
||||
obj, errs = DefaultRoom.create(room.key, description=room.desc)
|
||||
for agent_name in AGENTS:
|
||||
acc = AccountDB.objects.filter(username__iexact=agent_name).first()
|
||||
if not acc:
|
||||
acc, errs = DefaultAccount.create(username=agent_name, password=TIMMY_PASSWORD)
|
||||
|
||||
char = list(acc.characters)[0]
|
||||
if agent_name == "Timmy":
|
||||
char.location = room_map["Gate"]
|
||||
char.home = room_map["Gate"]
|
||||
else:
|
||||
obj.db.desc = room.desc
|
||||
room_map[room.key] = obj
|
||||
|
||||
for ex in EXITS:
|
||||
source = room_map[ex.source]
|
||||
dest = room_map[ex.destination]
|
||||
found = [obj for obj in source.contents if obj.key == ex.key and getattr(obj, "destination", None) == dest]
|
||||
if not found:
|
||||
DefaultExit.create(ex.key, source, dest, description=f"Exit to {{dest.key}}.", aliases=list(ex.aliases))
|
||||
|
||||
for spec in OBJECTS:
|
||||
location = room_map[spec.location]
|
||||
found = [obj for obj in location.contents if obj.key == spec.key]
|
||||
if not found:
|
||||
obj = create_object(typeclass=Object, key=spec.key, location=location)
|
||||
else:
|
||||
obj = found[0]
|
||||
obj.db.desc = spec.desc
|
||||
|
||||
char = list(acc.characters)[0]
|
||||
char.location = room_map["Gate"]
|
||||
char.home = room_map["Gate"]
|
||||
char.save()
|
||||
print("WORLD_OK")
|
||||
print("TIMMY_LOCATION", char.location.key)
|
||||
char.location = room_map["Courtyard"]
|
||||
char.home = room_map["Courtyard"]
|
||||
char.save()
|
||||
print(f"PROVISIONED {agent_name} at {char.location.key}")
|
||||
'''
|
||||
return run_shell(code)
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ def _disconnect(name: str = "timmy") -> dict:
|
||||
async def list_tools():
|
||||
return [
|
||||
Tool(name="bind_session", description="Bind a Hermes session id to Evennia telemetry logs.", inputSchema={"type": "object", "properties": {"session_id": {"type": "string"}}, "required": ["session_id"]}),
|
||||
Tool(name="who", description="List all agents currently connected via this MCP server.", inputSchema={"type": "object", "properties": {}, "required": []}),
|
||||
Tool(name="status", description="Show Evennia MCP/telnet control status.", inputSchema={"type": "object", "properties": {}, "required": []}),
|
||||
Tool(name="connect", description="Connect Timmy to the local Evennia telnet server as a real in-world account.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}, "username": {"type": "string"}, "password": {"type": "string"}}, "required": []}),
|
||||
Tool(name="observe", description="Read pending text output from Timmy's Evennia connection.", inputSchema={"type": "object", "properties": {"name": {"type": "string"}}, "required": []}),
|
||||
@@ -107,6 +108,8 @@ async def call_tool(name: str, arguments: dict):
|
||||
if name == "bind_session":
|
||||
bound = _save_bound_session_id(arguments.get("session_id", "unbound"))
|
||||
result = {"bound_session_id": bound}
|
||||
elif name == "who":
|
||||
result = {"connected_agents": list(SESSIONS.keys())}
|
||||
elif name == "status":
|
||||
result = {"connected_sessions": sorted(SESSIONS.keys()), "bound_session_id": _load_bound_session_id()}
|
||||
elif name == "connect":
|
||||
|
||||
39
scripts/failover_monitor.py
Normal file
39
scripts/failover_monitor.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Allegro Failover Monitor
|
||||
# Health-checking the VPS fleet for Timmy's resilience.
|
||||
|
||||
FLEET = {
|
||||
"ezra": "143.198.27.163", # Placeholder
|
||||
"bezalel": "167.99.126.228"
|
||||
}
|
||||
|
||||
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||
|
||||
def check_health(host):
|
||||
try:
|
||||
subprocess.check_call(["ping", "-c", "1", "-W", "2", host], stdout=subprocess.DEVNULL)
|
||||
return "ONLINE"
|
||||
except:
|
||||
return "OFFLINE"
|
||||
|
||||
def main():
|
||||
print("--- Allegro Failover Monitor ---")
|
||||
status = {}
|
||||
for name, host in FLEET.items():
|
||||
status[name] = check_health(host)
|
||||
print(f"{name.upper()}: {status[name]}")
|
||||
|
||||
STATUS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATUS_FILE.write_text(json.dumps({
|
||||
"timestamp": time.time(),
|
||||
"fleet": status
|
||||
}, indent=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
68
scripts/sovereign_health_report.py
Normal file
68
scripts/sovereign_health_report.py
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
DB_PATH = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
|
||||
REPORT_PATH = Path.home() / "timmy" / "SOVEREIGN_HEALTH.md"
|
||||
|
||||
def generate_report():
|
||||
if not DB_PATH.exists():
|
||||
return "No metrics database found."
|
||||
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
|
||||
# Get latest sovereignty score
|
||||
row = conn.execute("""
|
||||
SELECT local_pct, total_sessions, local_sessions, cloud_sessions, est_cloud_cost, est_saved
|
||||
FROM sovereignty_score ORDER BY timestamp DESC LIMIT 1
|
||||
""").fetchone()
|
||||
|
||||
if not row:
|
||||
return "No sovereignty data found."
|
||||
|
||||
pct, total, local, cloud, cost, saved = row
|
||||
|
||||
# Get model breakdown
|
||||
models = conn.execute("""
|
||||
SELECT model, SUM(sessions), SUM(messages), is_local, SUM(est_cost_usd)
|
||||
FROM session_stats
|
||||
WHERE timestamp > ?
|
||||
GROUP BY model
|
||||
ORDER BY SUM(sessions) DESC
|
||||
""", (datetime.now().timestamp() - 86400 * 7,)).fetchall()
|
||||
|
||||
report = f"""# Sovereign Health Report — {datetime.now().strftime('%Y-%m-%d')}
|
||||
|
||||
## ◈ Sovereignty Score: {pct:.1f}%
|
||||
**Status:** {"🟢 OPTIMAL" if pct > 90 else "🟡 WARNING" if pct > 50 else "🔴 COMPROMISED"}
|
||||
|
||||
- **Total Sessions:** {total}
|
||||
- **Local Sessions:** {local} (Zero Cost, Total Privacy)
|
||||
- **Cloud Sessions:** {cloud} (Token Leakage)
|
||||
- **Est. Cloud Cost:** ${cost:.2f}
|
||||
- **Est. Savings:** ${saved:.2f} (Sovereign Dividend)
|
||||
|
||||
## ◈ Fleet Composition (Last 7 Days)
|
||||
| Model | Sessions | Messages | Local? | Est. Cost |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
"""
|
||||
for m, s, msg, l, c in models:
|
||||
local_flag = "✅" if l else "❌"
|
||||
report += f"| {m} | {s} | {msg} | {local_flag} | ${c:.2f} |\n"
|
||||
|
||||
report += """
|
||||
---
|
||||
*Generated by the Sovereign Health Daemon. Sovereignty is a right. Privacy is a duty.*
|
||||
"""
|
||||
|
||||
with open(REPORT_PATH, "w") as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"Report generated at {REPORT_PATH}")
|
||||
return report
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_report()
|
||||
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()
|
||||
42
scripts/sovereign_review_gate.py
Normal file
42
scripts/sovereign_review_gate.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# 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 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()
|
||||
146
tests/test_nexus_alert.sh
Executable file
146
tests/test_nexus_alert.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
# Test script for Nexus Watchdog alerting functionality
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TEST_DIR="/tmp/test-nexus-alerts-$$"
|
||||
export NEXUS_ALERT_DIR="$TEST_DIR"
|
||||
export NEXUS_ALERT_ENABLED=true
|
||||
|
||||
echo "=== Nexus Watchdog Alert Test ==="
|
||||
echo "Test alert directory: $TEST_DIR"
|
||||
|
||||
# Source the alert function from the heartbeat script
|
||||
# Extract just the nexus_alert function for testing
|
||||
cat > /tmp/test_alert_func.sh << 'ALEOF'
|
||||
#!/bin/bash
|
||||
NEXUS_ALERT_DIR="${NEXUS_ALERT_DIR:-/tmp/nexus-alerts}"
|
||||
NEXUS_ALERT_ENABLED=true
|
||||
HOSTNAME=$(hostname -s 2>/dev/null || echo "unknown")
|
||||
SCRIPT_NAME="kimi-heartbeat-test"
|
||||
|
||||
nexus_alert() {
|
||||
local alert_type="$1"
|
||||
local message="$2"
|
||||
local severity="${3:-info}"
|
||||
local extra_data="${4:-{}}"
|
||||
|
||||
if [ "$NEXUS_ALERT_ENABLED" != "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$NEXUS_ALERT_DIR" 2>/dev/null || return 0
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
local nanoseconds=$(date +%N 2>/dev/null || echo "$$")
|
||||
local alert_id="${SCRIPT_NAME}_$(date +%s)_${nanoseconds}_$$"
|
||||
local alert_file="$NEXUS_ALERT_DIR/${alert_id}.json"
|
||||
|
||||
cat > "$alert_file" << EOF
|
||||
{
|
||||
"alert_id": "$alert_id",
|
||||
"timestamp": "$timestamp",
|
||||
"source": "$SCRIPT_NAME",
|
||||
"host": "$HOSTNAME",
|
||||
"alert_type": "$alert_type",
|
||||
"severity": "$severity",
|
||||
"message": "$message",
|
||||
"data": $extra_data
|
||||
}
|
||||
EOF
|
||||
|
||||
if [ -f "$alert_file" ]; then
|
||||
echo "NEXUS_ALERT: $alert_type [$severity] - $message"
|
||||
return 0
|
||||
else
|
||||
echo "NEXUS_ALERT_FAILED: Could not write alert"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
ALEOF
|
||||
|
||||
source /tmp/test_alert_func.sh
|
||||
|
||||
# Test 1: Basic alert
|
||||
echo -e "\n[TEST 1] Sending basic info alert..."
|
||||
nexus_alert "test_alert" "Test message from heartbeat" "info" '{"test": true}'
|
||||
|
||||
# Test 2: Stale lock alert simulation
|
||||
echo -e "\n[TEST 2] Sending stale lock alert..."
|
||||
nexus_alert \
|
||||
"stale_lock_reclaimed" \
|
||||
"Stale lockfile deadlock cleared after 650s" \
|
||||
"warning" \
|
||||
'{"lock_age_seconds": 650, "lockfile": "/tmp/kimi-heartbeat.lock", "action": "removed"}'
|
||||
|
||||
# Test 3: Heartbeat resumed alert
|
||||
echo -e "\n[TEST 3] Sending heartbeat resumed alert..."
|
||||
nexus_alert \
|
||||
"heartbeat_resumed" \
|
||||
"Kimi heartbeat resumed after clearing stale lock" \
|
||||
"info" \
|
||||
'{"recovery": "successful", "continuing": true}'
|
||||
|
||||
# Check results
|
||||
echo -e "\n=== Alert Files Created ==="
|
||||
alert_count=$(find "$TEST_DIR" -name "*.json" 2>/dev/null | wc -l)
|
||||
echo "Total alert files: $alert_count"
|
||||
|
||||
if [ "$alert_count" -eq 3 ]; then
|
||||
echo "✅ All 3 alerts were created successfully"
|
||||
else
|
||||
echo "❌ Expected 3 alerts, found $alert_count"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n=== Alert Contents ==="
|
||||
for f in "$TEST_DIR"/*.json; do
|
||||
echo -e "\n--- $(basename "$f") ---"
|
||||
cat "$f" | python3 -m json.tool 2>/dev/null || cat "$f"
|
||||
done
|
||||
|
||||
# Validate JSON structure
|
||||
echo -e "\n=== JSON Validation ==="
|
||||
all_valid=true
|
||||
for f in "$TEST_DIR"/*.json; do
|
||||
if python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then
|
||||
echo "✅ $(basename "$f") - Valid JSON"
|
||||
else
|
||||
echo "❌ $(basename "$f") - Invalid JSON"
|
||||
all_valid=false
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for required fields
|
||||
echo -e "\n=== Required Fields Check ==="
|
||||
for f in "$TEST_DIR"/*.json; do
|
||||
basename=$(basename "$f")
|
||||
missing=()
|
||||
python3 -c "import json; d=json.load(open('$f'))" 2>/dev/null || continue
|
||||
|
||||
for field in alert_id timestamp source host alert_type severity message data; do
|
||||
if ! python3 -c "import json; d=json.load(open('$f')); exit(0 if '$field' in d else 1)" 2>/dev/null; then
|
||||
missing+=("$field")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing[@]} -eq 0 ]; then
|
||||
echo "✅ $basename - All required fields present"
|
||||
else
|
||||
echo "❌ $basename - Missing fields: ${missing[*]}"
|
||||
all_valid=false
|
||||
fi
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$TEST_DIR" /tmp/test_alert_func.sh
|
||||
|
||||
echo -e "\n=== Test Summary ==="
|
||||
if [ "$all_valid" = true ]; then
|
||||
echo "✅ All tests passed!"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Some tests failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -24,32 +24,52 @@ class HealthCheckHandler(BaseHTTPRequestHandler):
|
||||
# Suppress default logging
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
def do_GET(self):
|
||||
"""Handle GET requests"""
|
||||
if self.path == '/health':
|
||||
self.send_health_response()
|
||||
elif self.path == '/status':
|
||||
self.send_full_status()
|
||||
elif self.path == '/metrics':
|
||||
self.send_sovereign_metrics()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def send_health_response(self):
|
||||
"""Send simple health check"""
|
||||
harness = get_harness()
|
||||
result = harness.execute("health_check")
|
||||
|
||||
|
||||
def send_sovereign_metrics(self):
|
||||
"""Send sovereign health metrics as JSON"""
|
||||
try:
|
||||
health_data = json.loads(result)
|
||||
status_code = 200 if health_data.get("overall") == "healthy" else 503
|
||||
except:
|
||||
status_code = 503
|
||||
health_data = {"error": "Health check failed"}
|
||||
|
||||
self.send_response(status_code)
|
||||
import sqlite3
|
||||
db_path = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
|
||||
if not db_path.exists():
|
||||
data = {"error": "No database found"}
|
||||
else:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
row = conn.execute("""
|
||||
SELECT local_pct, total_sessions, local_sessions, cloud_sessions, est_cloud_cost, est_saved
|
||||
FROM sovereignty_score ORDER BY timestamp DESC LIMIT 1
|
||||
""").fetchone()
|
||||
|
||||
if row:
|
||||
data = {
|
||||
"sovereignty_score": row[0],
|
||||
"total_sessions": row[1],
|
||||
"local_sessions": row[2],
|
||||
"cloud_sessions": row[3],
|
||||
"est_cloud_cost": row[4],
|
||||
"est_saved": row[5],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
data = {"error": "No data"}
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
data = {"error": str(e)}
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(health_data).encode())
|
||||
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def send_full_status(self):
|
||||
"""Send full system status"""
|
||||
harness = get_harness()
|
||||
|
||||
@@ -21,12 +21,7 @@ set -euo pipefail
|
||||
# --- Config ---
|
||||
TOKEN=$(cat "$HOME/.timmy/kimi_gitea_token" | tr -d '[:space:]')
|
||||
TIMMY_TOKEN=$(cat "$HOME/.config/gitea/timmy-token" | tr -d '[:space:]')
|
||||
# Prefer Tailscale (private network) over public IP
|
||||
if curl -sf --connect-timeout 2 "http://100.126.61.75:3000/api/v1/version" > /dev/null 2>&1; then
|
||||
BASE="http://100.126.61.75:3000/api/v1"
|
||||
else
|
||||
BASE="http://143.198.27.163:3000/api/v1"
|
||||
fi
|
||||
BASE="${GITEA_API_BASE:-https://forge.alexanderwhitestone.com/api/v1}"
|
||||
LOG="/tmp/kimi-heartbeat.log"
|
||||
LOCKFILE="/tmp/kimi-heartbeat.lock"
|
||||
MAX_DISPATCH=10 # Increased max dispatch to 10
|
||||
@@ -45,6 +40,31 @@ REPOS=(
|
||||
# --- Helpers ---
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||
|
||||
needs_pr_proof() {
|
||||
local haystack="${1,,}"
|
||||
[[ "$haystack" =~ implement|fix|refactor|feature|perf|performance|rebase|deploy|integration|module|script|pipeline|benchmark|cache|test|bug|build|port ]]
|
||||
}
|
||||
|
||||
has_pr_proof() {
|
||||
local haystack="${1,,}"
|
||||
[[ "$haystack" == *"proof:"* || "$haystack" == *"pr:"* || "$haystack" == *"/pulls/"* || "$haystack" == *"commit:"* ]]
|
||||
}
|
||||
|
||||
post_issue_comment_json() {
|
||||
local repo="$1"
|
||||
local issue_num="$2"
|
||||
local token="$3"
|
||||
local body="$4"
|
||||
local payload
|
||||
payload=$(python3 - "$body" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"body": sys.argv[1]}))
|
||||
PY
|
||||
)
|
||||
curl -sf -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \
|
||||
-d "$payload" "$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Prevent overlapping runs
|
||||
if [ -f "$LOCKFILE" ]; then
|
||||
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCKFILE" 2>/dev/null || echo 0) ))
|
||||
@@ -257,20 +277,35 @@ print(payloads[0]['text'][:3000] if payloads else 'No response')
|
||||
" 2>/dev/null || echo "No response")
|
||||
|
||||
if [ "$status" = "ok" ] && [ "$response_text" != "No response" ]; then
|
||||
log "COMPLETED: $repo #$issue_num"
|
||||
|
||||
# Post result as comment (escape for JSON)
|
||||
escaped=$(echo "$response_text" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read())[1:-1])" 2>/dev/null)
|
||||
curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"body\":\" **KimiClaw result:**\\n\\n$escaped\"}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/comments" > /dev/null 2>&1 || true
|
||||
if needs_pr_proof "$title $body" && ! has_pr_proof "$response_text"; then
|
||||
log "BLOCKED: $repo #$issue_num — response lacked PR/proof for code task"
|
||||
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟡 **KimiClaw produced analysis only — no PR/proof detected.**
|
||||
|
||||
# Remove kimi-in-progress, add kimi-done
|
||||
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
||||
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"labels\":[$done_id]}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
||||
This issue looks like implementation work, so it is NOT being marked kimi-done.
|
||||
Kimi response excerpt:
|
||||
|
||||
$escaped
|
||||
|
||||
Action: removing Kimi queue labels so a code-capable agent can pick it up."
|
||||
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
||||
[ -n "$kimi_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels/$kimi_id" > /dev/null 2>&1 || true
|
||||
else
|
||||
log "COMPLETED: $repo #$issue_num"
|
||||
post_issue_comment_json "$repo" "$issue_num" "$TOKEN" "🟢 **KimiClaw result:**
|
||||
|
||||
$escaped"
|
||||
|
||||
[ -n "$progress_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels/$progress_id" > /dev/null 2>&1 || true
|
||||
[ -n "$kimi_id" ] && curl -sf -X DELETE -H "Authorization: token $TIMMY_TOKEN" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels/$kimi_id" > /dev/null 2>&1 || true
|
||||
[ -n "$done_id" ] && curl -sf -X POST -H "Authorization: token $TIMMY_TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"labels\":[$done_id]}" \
|
||||
"$BASE/repos/$repo/issues/$issue_num/labels" > /dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
log "FAILED: $repo #$issue_num — status=$status"
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
KIMI_TOKEN=$(cat /Users/apayne/.timmy/kimi_gitea_token | tr -d '[:space:]')
|
||||
BASE="http://100.126.61.75:3000/api/v1"
|
||||
|
||||
# --- Tailscale/IP Detection (timmy-home#385) ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/tailscale-gitea.sh"
|
||||
BASE="$GITEA_BASE_URL"
|
||||
|
||||
LOG="/tmp/kimi-mentions.log"
|
||||
PROCESSED="/tmp/kimi-mentions-processed.txt"
|
||||
|
||||
|
||||
55
uniwizard/lib/example-usage.sh
Normal file
55
uniwizard/lib/example-usage.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# example-usage.sh — Example showing how to use the tailscale-gitea module
|
||||
# Issue: timmy-home#385 — Standardized Tailscale IP detection module
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Basic Usage ---
|
||||
# Source the module to automatically set GITEA_BASE_URL
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/tailscale-gitea.sh"
|
||||
|
||||
# Now use GITEA_BASE_URL in your API calls
|
||||
echo "Using Gitea at: $GITEA_BASE_URL"
|
||||
echo "Tailscale active: $GITEA_USING_TAILSCALE"
|
||||
|
||||
# --- Example API Call ---
|
||||
# curl -sf -H "Authorization: token $TOKEN" \
|
||||
# "$GITEA_BASE_URL/repos/myuser/myrepo/issues"
|
||||
|
||||
# --- Custom Configuration (Optional) ---
|
||||
# You can customize behavior by setting variables BEFORE sourcing:
|
||||
#
|
||||
# TAILSCALE_TIMEOUT=5 # Wait 5 seconds instead of 2
|
||||
# TAILSCALE_DEBUG=1 # Print which endpoint was selected
|
||||
# source "${SCRIPT_DIR}/tailscale-gitea.sh"
|
||||
|
||||
# --- Advanced: Checking Network Mode ---
|
||||
if [[ "$GITEA_USING_TAILSCALE" == "true" ]]; then
|
||||
echo "✓ Connected via private Tailscale network"
|
||||
else
|
||||
echo "⚠ Using public internet fallback (Tailscale unavailable)"
|
||||
fi
|
||||
|
||||
# --- Example: Polling with Retry Logic ---
|
||||
poll_gitea() {
|
||||
local endpoint="${1:-$GITEA_BASE_URL}"
|
||||
local max_retries="${2:-3}"
|
||||
local retry=0
|
||||
|
||||
while [[ $retry -lt $max_retries ]]; do
|
||||
if curl -sf --connect-timeout 2 "${endpoint}/version" > /dev/null 2>&1; then
|
||||
echo "Gitea is reachable"
|
||||
return 0
|
||||
fi
|
||||
retry=$((retry + 1))
|
||||
echo "Retry $retry/$max_retries..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Gitea unreachable after $max_retries attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Uncomment to test connectivity:
|
||||
# poll_gitea "$GITEA_BASE_URL"
|
||||
64
uniwizard/lib/tailscale-gitea.sh
Normal file
64
uniwizard/lib/tailscale-gitea.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# tailscale-gitea.sh — Standardized Tailscale IP detection module for Gitea API access
|
||||
# Issue: timmy-home#385 — Standardize Tailscale IP detection across auxiliary scripts
|
||||
#
|
||||
# Usage (source this file in your script):
|
||||
# source /path/to/tailscale-gitea.sh
|
||||
# # Now use $GITEA_BASE_URL for API calls
|
||||
#
|
||||
# Configuration (set before sourcing to customize):
|
||||
# TAILSCALE_IP - Tailscale IP to try first (default: 100.126.61.75)
|
||||
# PUBLIC_IP - Public fallback IP (default: 143.198.27.163)
|
||||
# GITEA_PORT - Gitea API port (default: 3000)
|
||||
# TAILSCALE_TIMEOUT - Connection timeout in seconds (default: 2)
|
||||
# GITEA_API_VERSION - API version path (default: api/v1)
|
||||
#
|
||||
# Sovereignty: Private Tailscale network preferred over public internet
|
||||
|
||||
# --- Default Configuration ---
|
||||
: "${TAILSCALE_IP:=100.126.61.75}"
|
||||
: "${PUBLIC_IP:=143.198.27.163}"
|
||||
: "${GITEA_PORT:=3000}"
|
||||
: "${TAILSCALE_TIMEOUT:=2}"
|
||||
: "${GITEA_API_VERSION:=api/v1}"
|
||||
|
||||
# --- Detection Function ---
|
||||
_detect_gitea_endpoint() {
|
||||
local tailscale_url="http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||
local public_url="http://${PUBLIC_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||
|
||||
# Prefer Tailscale (private network) over public IP
|
||||
if curl -sf --connect-timeout "$TAILSCALE_TIMEOUT" \
|
||||
"${tailscale_url}/version" > /dev/null 2>&1; then
|
||||
echo "$tailscale_url"
|
||||
return 0
|
||||
else
|
||||
echo "$public_url"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main Detection ---
|
||||
# Set GITEA_BASE_URL for use by sourcing scripts
|
||||
# Also sets GITEA_USING_TAILSCALE=true/false for scripts that need to know
|
||||
if curl -sf --connect-timeout "$TAILSCALE_TIMEOUT" \
|
||||
"http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}/version" > /dev/null 2>&1; then
|
||||
GITEA_BASE_URL="http://${TAILSCALE_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||
GITEA_USING_TAILSCALE=true
|
||||
else
|
||||
GITEA_BASE_URL="http://${PUBLIC_IP}:${GITEA_PORT}/${GITEA_API_VERSION}"
|
||||
GITEA_USING_TAILSCALE=false
|
||||
fi
|
||||
|
||||
# Export for child processes
|
||||
export GITEA_BASE_URL
|
||||
export GITEA_USING_TAILSCALE
|
||||
|
||||
# Optional: log which endpoint was selected (set TAILSCALE_DEBUG=1 to enable)
|
||||
if [[ "${TAILSCALE_DEBUG:-0}" == "1" ]]; then
|
||||
if [[ "$GITEA_USING_TAILSCALE" == "true" ]]; then
|
||||
echo "[tailscale-gitea] Using Tailscale endpoint: $GITEA_BASE_URL" >&2
|
||||
else
|
||||
echo "[tailscale-gitea] Tailscale unavailable, using public endpoint: $GITEA_BASE_URL" >&2
|
||||
fi
|
||||
fi
|
||||
Reference in New Issue
Block a user