Created
March 12, 2026 14:08
-
-
Save jalehman/85d51a4ee95d15bb6831d9f335a623e1 to your computer and use it in GitHub Desktop.
Orville Ignore Sessions Review — lossless-claw #39 #pagedrop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Orville's Ignore Sessions — Code Review</title> | |
| <style> | |
| :root { | |
| --bg: #0d1117; | |
| --surface: #161b22; | |
| --border: #30363d; | |
| --text: #e6edf3; | |
| --muted: #8b949e; | |
| --accent: #58a6ff; | |
| --green: #3fb950; | |
| --red: #f85149; | |
| --yellow: #d29922; | |
| --orange: #db6d28; | |
| --code-bg: #1c2128; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.6; | |
| padding: 2rem; | |
| max-width: 900px; | |
| margin: 0 auto; | |
| } | |
| h1 { font-size: 1.8rem; margin-bottom: 0.5rem; } | |
| h2 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: 0.75rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.4rem; } | |
| h3 { font-size: 1.1rem; margin-top: 1.2rem; margin-bottom: 0.5rem; color: var(--text); } | |
| p, li { color: var(--text); margin-bottom: 0.5rem; } | |
| .subtitle { color: var(--muted); font-size: 0.95rem; margin-bottom: 2rem; } | |
| code { | |
| font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; | |
| font-size: 0.85rem; | |
| background: var(--code-bg); | |
| padding: 0.15em 0.4em; | |
| border-radius: 4px; | |
| } | |
| pre { | |
| background: var(--code-bg); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| overflow-x: auto; | |
| margin: 0.75rem 0 1rem 0; | |
| font-size: 0.85rem; | |
| line-height: 1.5; | |
| } | |
| pre code { background: none; padding: 0; } | |
| .badge { | |
| display: inline-block; | |
| padding: 0.15em 0.6em; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| margin-right: 0.3rem; | |
| } | |
| .badge-green { background: rgba(63,185,80,0.15); color: var(--green); } | |
| .badge-yellow { background: rgba(210,153,34,0.15); color: var(--yellow); } | |
| .badge-red { background: rgba(248,81,73,0.15); color: var(--red); } | |
| .badge-blue { background: rgba(88,166,255,0.15); color: var(--accent); } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1.2rem; | |
| margin: 0.75rem 0; | |
| } | |
| .stat-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 0.75rem; | |
| margin: 1rem 0; | |
| } | |
| .stat { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 0.8rem; | |
| text-align: center; | |
| } | |
| .stat-value { font-size: 1.4rem; font-weight: 700; color: var(--accent); } | |
| .stat-label { font-size: 0.8rem; color: var(--muted); margin-top: 0.2rem; } | |
| ul { padding-left: 1.5rem; } | |
| .finding { | |
| border-left: 3px solid var(--yellow); | |
| padding: 0.8rem 1rem; | |
| margin: 0.75rem 0; | |
| background: rgba(210,153,34,0.05); | |
| border-radius: 0 8px 8px 0; | |
| } | |
| .finding.good { border-left-color: var(--green); background: rgba(63,185,80,0.05); } | |
| .finding.concern { border-left-color: var(--orange); background: rgba(219,109,40,0.05); } | |
| .finding.blocker { border-left-color: var(--red); background: rgba(248,81,73,0.05); } | |
| .finding-title { font-weight: 600; margin-bottom: 0.3rem; } | |
| .flow-diagram { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| align-items: center; | |
| margin: 1rem 0; | |
| font-size: 0.9rem; | |
| } | |
| .flow-step { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 0.5rem 0.8rem; | |
| } | |
| .flow-arrow { color: var(--muted); font-weight: bold; } | |
| .flow-step.skip { border-color: var(--red); opacity: 0.6; text-decoration: line-through; } | |
| .flow-step.pass { border-color: var(--green); } | |
| table { width: 100%; border-collapse: collapse; margin: 0.75rem 0; } | |
| th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; } | |
| th { color: var(--muted); font-weight: 600; } | |
| .vs-table { margin: 1rem 0; } | |
| .vs-table td:first-child { font-weight: 600; width: 35%; } | |
| .vs-table .check { color: var(--green); } | |
| .vs-table .x { color: var(--red); } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🦅 Orville — Ignore Sessions</h1> | |
| <p class="subtitle">Commit <code>ef23d74</code> on branch <code>orville-c0d39638-lcm-ignore-sessions</code> · lossless-claw issue #39</p> | |
| <div class="stat-grid"> | |
| <div class="stat"><div class="stat-value">515</div><div class="stat-label">lines added</div></div> | |
| <div class="stat"><div class="stat-value">2</div><div class="stat-label">lines removed</div></div> | |
| <div class="stat"><div class="stat-value">14</div><div class="stat-label">files changed</div></div> | |
| <div class="stat"><div class="stat-value">258</div><div class="stat-label">tests pass</div></div> | |
| </div> | |
| <h2>What It Does</h2> | |
| <p>Completely excludes matching sessions from LCM. No reading, no writing, no DB interaction whatsoever. The session is invisible to LCM — it won't create conversations, store messages, participate in compaction, or get assembled context. Use case: cron jobs, automation, CI sessions that would just pollute the memory graph with noise.</p> | |
| <div class="card"> | |
| <h3>Configuration</h3> | |
| <pre><code>// Plugin config (openclaw.json → plugins.entries.lossless-claw.config) | |
| { | |
| "ignoreSessionPatterns": ["agent:*:cron:*", "agent:main:subagent:batch-**"] | |
| } | |
| // Or via env var | |
| LCM_IGNORE_SESSION_PATTERNS="agent:*:cron:*,agent:main:subagent:batch-**"</code></pre> | |
| <p style="margin-top: 0.5rem; color: var(--muted);"> | |
| <code>*</code> matches within a single <code>:</code>-delimited segment · | |
| <code>**</code> matches across segment boundaries | |
| </p> | |
| </div> | |
| <h2>How It Works</h2> | |
| <h3>Pattern Matching (new file: <code>session-patterns.ts</code>)</h3> | |
| <p>23-line module. Globs compiled to RegExp via a clever replace chain: escape specials → swap <code>**</code> for sentinel → replace <code>*</code> with <code>[^:]*</code> → restore sentinel as <code>.*</code>. More compact than Prince's character-by-character approach, same result.</p> | |
| <pre><code>// The entire compiler: | |
| const escaped = pattern | |
| .replace(/[.+^${}()|[\]\\]/g, "\\$&") | |
| .replace(/\*\*/g, "\u0000") // protect ** from next step | |
| .replace(/\*/g, "[^:]*") // single * = within segment | |
| .replace(/\u0000/g, ".*"); // ** = cross segments | |
| return new RegExp(`^${escaped}$`);</code></pre> | |
| <h3>Engine Guard — Every Lifecycle Method</h3> | |
| <p>Private <code>shouldIgnoreSession(sessionId)</code> checks the compiled patterns. Called at the top of each method:</p> | |
| <table> | |
| <tr><th>Method</th><th>Ignored Behavior</th><th>Return</th></tr> | |
| <tr><td><code>bootstrap()</code></td><td>Skip import</td><td><code>{ bootstrapped: false, reason: "session excluded by pattern" }</code></td></tr> | |
| <tr><td><code>ingest()</code></td><td>Skip persistence</td><td><code>{ ingested: false }</code></td></tr> | |
| <tr><td><code>ingestBatch()</code></td><td>Skip persistence</td><td><code>{ ingestedCount: 0 }</code></td></tr> | |
| <tr><td><code>afterTurn()</code></td><td>Skip everything</td><td><code>void</code> (early return)</td></tr> | |
| <tr><td><code>assemble()</code></td><td><strong>Pass-through live messages</strong></td><td><code>{ messages: liveMessages, estimatedTokens: 0 }</code></td></tr> | |
| <tr><td><code>compact()</code></td><td>Skip compaction</td><td><code>{ ok: true, compacted: false, reason: "session excluded" }</code></td></tr> | |
| <tr><td><code>prepareSubagentSpawn()</code></td><td>No expansion grant</td><td><code>undefined</code></td></tr> | |
| <tr><td><code>onSubagentEnded()</code></td><td>Skip grant cleanup</td><td><code>void</code> (early return)</td></tr> | |
| </table> | |
| <h3>Data Flow for an Ignored Session</h3> | |
| <div class="flow-diagram"> | |
| <div class="flow-step skip">bootstrap</div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-step skip">ingest</div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-step skip">assemble (pass-through)</div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-step skip">afterTurn</div> | |
| <div class="flow-arrow">→</div> | |
| <div class="flow-step skip">compact</div> | |
| </div> | |
| <p style="color: var(--muted); font-size: 0.85rem;">Everything skipped. assemble returns live messages as-is without DB access — same as Prince.</p> | |
| <h2>Review Findings</h2> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Simpler pattern compiler</div> | |
| <p>23 lines vs Prince's 44. The sentinel-based replace chain is elegant and correct. Both produce identical regex output for the same input patterns.</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Solid test coverage</div> | |
| <p>268 new lines of engine tests. Each lifecycle method tested with both ignored and included sessions side by side. Pattern compilation and matching tested separately. Expansion grant tests verify grants aren't cleaned up for ignored children (correct — if we didn't create it, we shouldn't touch it).</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Broader test fixture updates</div> | |
| <p>Added <code>ignoreSessionPatterns: []</code> to 6 test files (config, engine, expansion, lcm-expand-query-tool, lcm-expand-tool, lcm-tools, summarize). Prince only updated 3. Orville's more thorough here — fewer chances of a test breaking if the field becomes required.</p> | |
| </div> | |
| <div class="finding concern"> | |
| <div class="finding-title"><span class="badge badge-yellow">NOTE</span> Guards on sessionId, not sessionKey</div> | |
| <p>Orville's <code>shouldIgnoreSession()</code> takes <code>sessionId</code> (the UUID-like runtime identifier), not <code>sessionKey</code> (the structured <code>agent:main:cron:*</code> key). This works for <code>bootstrap/ingest/ingestBatch/afterTurn/compact</code> where OpenClaw passes session IDs that happen to look like session keys. But it's semantically matching session <em>IDs</em> against patterns designed for session <em>keys</em>.</p> | |
| <p>In contrast, Prince explicitly accepts <code>sessionKey</code> parameters and falls back to <code>runtimeContext.sessionKey</code>. This is more correct — session keys are the structured identifiers, session IDs are UUIDs.</p> | |
| <p><strong>In practice:</strong> OpenClaw currently passes the session key as the session ID in most paths, so this works. But it's a latent correctness issue if that ever changes.</p> | |
| </div> | |
| <div class="finding concern"> | |
| <div class="finding-title"><span class="badge badge-yellow">NOTE</span> prepareSubagentSpawn checks both parent AND child</div> | |
| <p>Orville suppresses grants when <em>either</em> the parent or child matches an ignore pattern. Prince only checks the parent. Orville's approach is arguably more correct — if the child is ignored, there's no point creating a grant for it. But it's a behavioral difference to be aware of.</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Documentation</div> | |
| <p>Comprehensive README section with pattern rules, examples for both env var and plugin config, and clear explanation of what "ignored" means. Better documented than Prince's equivalent section.</p> | |
| </div> | |
| <h2>Prince vs Orville — Side by Side</h2> | |
| <table class="vs-table"> | |
| <tr><th></th><th>Prince (Stateless)</th><th>Orville (Ignore)</th></tr> | |
| <tr><td>Semantics</td><td>Read LCM, don't write</td><td>LCM doesn't exist</td></tr> | |
| <tr><td>assemble()</td><td>Returns live msgs (no DB)</td><td>Returns live msgs (no DB)</td></tr> | |
| <tr><td>Matching key</td><td><code>sessionKey</code> (explicit param)</td><td><code>sessionId</code> (existing param)</td></tr> | |
| <tr><td>runtimeContext fallback</td><td class="check">✓ afterTurn/compact</td><td class="x">✗ not used</td></tr> | |
| <tr><td>Core plumbing needed</td><td>Yes (sessionKey in bootstrap/ingest/assemble)</td><td>No (uses existing sessionId)</td></tr> | |
| <tr><td>Pattern compiler</td><td>44 lines (char-by-char)</td><td>23 lines (replace chain)</td></tr> | |
| <tr><td>Test fixtures updated</td><td>3 files</td><td>6 files</td></tr> | |
| <tr><td>prepareSubagentSpawn</td><td>Checks parent only</td><td>Checks parent + child</td></tr> | |
| <tr><td>Config name</td><td><code>statelessSessionPatterns</code></td><td><code>ignoreSessionPatterns</code></td></tr> | |
| </table> | |
| <div class="card"> | |
| <h3>Key Insight</h3> | |
| <p>Despite the different names, <strong>both implementations currently do the same thing</strong> — skip all writes and return live messages for assemble. The "stateless sessions can read LCM" distinction I described earlier <strong>doesn't actually exist in Prince's code</strong>. Prince's <code>assemble()</code> returns <code>{ messages: params.messages, estimatedTokens: 0 }</code> for stateless sessions — no DB lookup, no LCM context. Identical to Orville.</p> | |
| <p>The only real differences are:</p> | |
| <ul> | |
| <li><strong>sessionKey vs sessionId</strong> — Prince is more correct but needs core plumbing</li> | |
| <li><strong>Naming</strong> — "stateless" implies read-only access, "ignore" implies total exclusion. The names promise different things, but the code delivers the same behavior.</li> | |
| </ul> | |
| </div> | |
| <h2>Verdict</h2> | |
| <div class="card" style="border-color: var(--green);"> | |
| <p><span class="badge badge-green">READY TO MERGE</span> Clean, well-tested, well-documented. The sessionId vs sessionKey concern is minor and works in practice today.</p> | |
| <p style="margin-top: 0.75rem;"><strong>Recommendation:</strong> Merge <em>one</em> of these two. Given they do the same thing:</p> | |
| <ul style="margin-top: 0.5rem;"> | |
| <li><strong>Orville's advantages:</strong> Simpler pattern compiler, more test fixtures updated, no core plumbing dependency, better docs, "ignore" naming is more honest about what the code does</li> | |
| <li><strong>Prince's advantages:</strong> sessionKey matching is more architecturally correct, runtimeContext fallback is forward-thinking</li> | |
| </ul> | |
| <p style="margin-top: 0.5rem;">If you want to ship today with zero core changes: <strong>Orville</strong>. If you want the cleaner long-term architecture: <strong>Prince</strong> (but needs the sessionKey plumbing PR first).</p> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment