Skip to content

Instantly share code, notes, and snippets.

@jalehman
Created March 12, 2026 14:03
Show Gist options
  • Select an option

  • Save jalehman/761ff9981ba66ad0318580e17dc4a383 to your computer and use it in GitHub Desktop.

Select an option

Save jalehman/761ff9981ba66ad0318580e17dc4a383 to your computer and use it in GitHub Desktop.
Prince Stateless Sessions Review — lossless-claw #39 #pagedrop
<!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> &middot; 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 &middot;
<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