Created
March 12, 2026 14:03
-
-
Save jalehman/761ff9981ba66ad0318580e17dc4a383 to your computer and use it in GitHub Desktop.
Prince Stateless 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>Prince's Stateless 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; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🏇 Prince — Stateless Sessions</h1> | |
| <p class="subtitle">Commit <code>b7d75f3</code> on branch <code>prince-6259b4c3-lcm-stateless-sessions</code> · lossless-claw issue #39</p> | |
| <div class="stat-grid"> | |
| <div class="stat"><div class="stat-value">582</div><div class="stat-label">lines added</div></div> | |
| <div class="stat"><div class="stat-value">13</div><div class="stat-label">lines removed</div></div> | |
| <div class="stat"><div class="stat-value">10</div><div class="stat-label">files changed</div></div> | |
| <div class="stat"><div class="stat-value">261</div><div class="stat-label">tests pass</div></div> | |
| </div> | |
| <h2>What It Does</h2> | |
| <p>Lets you mark sessions as <strong>stateless</strong> — they can <strong>read</strong> LCM context (assembled summaries) but don't <strong>write</strong> to it (no ingest, no compaction, no subagent grants). Use case: subagents, cron jobs, or ephemeral sessions that need conversation history but shouldn't pollute the memory graph.</p> | |
| <div class="card"> | |
| <h3>Configuration</h3> | |
| <pre><code>// Plugin config (openclaw.json → plugins.entries.lossless-claw.config) | |
| { | |
| "statelessSessionPatterns": ["agent:main:cron:*", "agent:**:subagent:*"] | |
| } | |
| // Or via env var | |
| LCM_STATELESS_SESSION_PATTERNS="agent:main:cron:*, agent:**:subagent:*"</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>Glob patterns are compiled to RegExp once at engine construction. <code>*</code> becomes <code>[^:]*</code>, <code>**</code> becomes <code>.*</code>. Clean, 44-line module.</p> | |
| <h3>Engine Guard — Every Lifecycle Method</h3> | |
| <p>Each method checks <code>isStatelessSession(sessionKey)</code> early and returns a no-op result if matched:</p> | |
| <table> | |
| <tr><th>Method</th><th>Stateless Behavior</th><th>Return</th></tr> | |
| <tr><td><code>bootstrap()</code></td><td>Skip import</td><td><code>{ bootstrapped: false, reason: "stateless session" }</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 ingest + compaction</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: "stateless" }</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 a Stateless 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 pass">assemble (live msgs only)</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;">Crossed out = skipped entirely. Green = still runs but returns live messages without DB access.</p> | |
| <h2>Review Findings</h2> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Clean guard pattern</div> | |
| <p>Every lifecycle method gets the same early-return guard. Consistent, hard to miss a path. The <code>isStatelessSession()</code> method is public for testing.</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Solid test coverage</div> | |
| <p>296 new lines of engine tests covering all 8 lifecycle methods, plus positive/negative matching, pattern compilation, regex escaping, and expansion grant suppression. Each test verifies both the stateless skip AND the stateful path still works.</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Config precedence</div> | |
| <p>Env var (<code>LCM_STATELESS_SESSION_PATTERNS</code>, comma-separated) wins over plugin config (array). Follows the existing three-tier precedence pattern. <code>toStrArray()</code> handles both string and array inputs with trim + empty filter.</p> | |
| </div> | |
| <div class="finding concern"> | |
| <div class="finding-title"><span class="badge badge-yellow">NOTE</span> assemble() returns estimatedTokens: 0</div> | |
| <p>For stateless sessions, <code>assemble()</code> returns <code>{ messages: params.messages, estimatedTokens: 0 }</code>. The live messages clearly have <em>some</em> token count, so the caller can't rely on this field for budgeting. Not a bug — the caller already has the messages and can count tokens itself — but worth noting if any downstream code uses <code>estimatedTokens</code> for decisions.</p> | |
| </div> | |
| <div class="finding concern"> | |
| <div class="finding-title"><span class="badge badge-yellow">NOTE</span> Core plumbing dependency</div> | |
| <p>The README explicitly documents this: <em>"This plugin-side support is additive, but it requires the core OpenClaw <code>sessionKey</code> plumbing change to be fully effective."</em> Currently, <code>bootstrap()</code>, <code>ingest()</code>, <code>ingestBatch()</code>, and <code>assemble()</code> only work if the caller passes <code>sessionKey</code>. The <code>afterTurn()</code> and <code>compact()</code> methods fall back to <code>runtimeContext.sessionKey</code>, which OpenClaw already provides.</p> | |
| <p>In practice: <strong>afterTurn/compact work today</strong> (runtimeContext exists). bootstrap/ingest/assemble need a small OpenClaw core change to pass sessionKey through.</p> | |
| </div> | |
| <div class="finding good"> | |
| <div class="finding-title"><span class="badge badge-green">GOOD</span> Subagent grant handling</div> | |
| <p>Correctly suppresses expansion grants when the <em>parent</em> is stateless (no context to share), and skips grant cleanup when the <em>child</em> is stateless (no grant to clean up). Test covers the full lifecycle including verifying stateful grants survive stateless child cleanup.</p> | |
| </div> | |
| <h2>Overlap with Orville (Ignore Sessions)</h2> | |
| <div class="card"> | |
| <p><strong>Both share <code>session-patterns.ts</code></strong> — same glob-to-regex compiler, same normalization. They'll conflict on merge.</p> | |
| <p><strong>Key difference:</strong> Stateless sessions still get <code>assemble()</code> (read-only LCM context). Ignore sessions skip everything — the session doesn't interact with LCM at all, not even for reading.</p> | |
| <p><strong>Recommendation:</strong> These are complementary features. Merge one first, then rebase the other to reuse the shared module. Or merge just the session-patterns module from one branch first.</p> | |
| </div> | |
| <h2>Verdict</h2> | |
| <div class="card" style="border-color: var(--green);"> | |
| <p><span class="badge badge-green">READY TO MERGE</span> Clean implementation, comprehensive tests, well-documented. The <code>estimatedTokens: 0</code> note is cosmetic. The core plumbing dependency is clearly documented and doesn't block the plugin-side work.</p> | |
| <p style="color: var(--muted); margin-top: 0.5rem;">One thing to decide: merge Prince (stateless) or Orville (ignore) first, since they share session-patterns.ts.</p> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment