create-walle 0.9.27 → 0.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
  4. package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
  5. package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
  6. package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
  7. package/template/claude-task-manager/lib/session-messages-projection.js +195 -3
  8. package/template/claude-task-manager/lib/ttl-memo.js +61 -0
  9. package/template/claude-task-manager/public/index.html +845 -12
  10. package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
  11. package/template/claude-task-manager/public/js/session-phase.js +370 -0
  12. package/template/claude-task-manager/public/js/setup.js +74 -1
  13. package/template/claude-task-manager/public/js/stream-view.js +56 -2
  14. package/template/claude-task-manager/server.js +518 -67
  15. package/template/package.json +1 -1
  16. package/template/wall-e/agent.js +54 -1
  17. package/template/wall-e/api-walle.js +12 -1
  18. package/template/wall-e/brain.js +32 -3
  19. package/template/wall-e/embeddings.js +192 -17
  20. package/template/wall-e/http/model-admin.js +109 -0
  21. package/template/wall-e/lib/event-loop-monitor.js +2 -2
  22. package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
  23. package/template/wall-e/lib/scheduler.js +74 -7
  24. package/template/wall-e/lib/worker-thread-pool.js +49 -3
  25. package/template/wall-e/llm/ollama-library.js +126 -0
  26. package/template/wall-e/llm/ollama.js +13 -0
  27. package/template/wall-e/llm/provider-backpressure.js +134 -0
  28. package/template/wall-e/llm/provider-health-state.js +24 -0
  29. package/template/wall-e/loops/backfill.js +43 -16
  30. package/template/wall-e/loops/initiative.js +1 -0
  31. package/template/wall-e/loops/think.js +12 -2
  32. package/template/wall-e/skills/skill-fallback.js +34 -1
  33. package/template/wall-e/skills/skill-planner.js +60 -2
  34. package/template/wall-e/telemetry.js +42 -7
  35. package/template/wall-e/workers/runtime-worker.js +9 -1
  36. package/template/website/index.html +5 -0
package/README.md CHANGED
@@ -16,6 +16,7 @@ A web dashboard for running and managing AI coding sessions across multiple prov
16
16
  - **Code & Doc Review** — Review git diffs and Markdown docs side by side, add anchored comments, and send feedback into an agent session or queue
17
17
  - **Model Registry** — Manage providers (Anthropic, OpenAI, Google, DeepSeek, Ollama, LM Studio, MLX, and CLI subscription providers), compare pricing, switch models per session
18
18
  - **Session Insights** — Analyze patterns across sessions to optimize prompts and workflows
19
+ - **Reliable Setup & Updates** — Install and update checks keep native dependencies aligned with your active Node.js runtime, with optional Node pinning for multi-version machines
19
20
 
20
21
  ### Wall-E (Personal Digital Twin)
21
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.9.27",
3
+ "version": "0.9.28",
4
4
  "description": "CTM + Wall-E — AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, remote phone and tablet access, code/doc review, and an agent that learns from Slack, email & calendar.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -0,0 +1,554 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>CTM Session Status — Redesign Proposal</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0f1115; --panel:#171a21; --panel2:#1d212b; --ink:#e6e9ef; --muted:#9aa3b2;
10
+ --line:#2a2f3a; --accent:#5b9cff; --good:#3ecf8e; --warn:#ffb454; --bad:#ff6b6b;
11
+ --run:#3ecf8e; --needs:#ffb454; --idle:#7d869c; --resume:#5b9cff; --review:#b388ff;
12
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
13
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
14
+ }
15
+ *{box-sizing:border-box}
16
+ html{scroll-behavior:smooth}
17
+ body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.6;font-size:16px}
18
+ .wrap{max-width:1080px;margin:0 auto;padding:0 24px 120px}
19
+ header.hero{padding:56px 24px 40px;border-bottom:1px solid var(--line);background:
20
+ radial-gradient(900px 300px at 20% -10%, rgba(91,156,255,.18), transparent 60%),
21
+ radial-gradient(700px 260px at 90% -20%, rgba(179,136,255,.14), transparent 60%);}
22
+ .hero .wrap{padding-bottom:0}
23
+ .eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:0 0 12px}
24
+ h1{font-size:38px;line-height:1.15;margin:0 0 12px;font-weight:700;letter-spacing:-.02em}
25
+ .sub{font-size:18px;color:var(--muted);max-width:760px;margin:0}
26
+ .meta{margin-top:20px;display:flex;gap:10px;flex-wrap:wrap}
27
+ .pill{font-family:var(--mono);font-size:12px;border:1px solid var(--line);background:var(--panel);
28
+ padding:5px 10px;border-radius:999px;color:var(--muted)}
29
+ nav.toc{position:sticky;top:0;z-index:20;background:rgba(15,17,21,.88);backdrop-filter:blur(8px);
30
+ border-bottom:1px solid var(--line);padding:10px 0}
31
+ nav.toc .wrap{padding-top:0;padding-bottom:0;display:flex;gap:6px;flex-wrap:wrap}
32
+ nav.toc a{font-size:13px;color:var(--muted);text-decoration:none;padding:6px 10px;border-radius:8px;white-space:nowrap}
33
+ nav.toc a:hover{color:var(--ink);background:var(--panel)}
34
+ section{padding-top:48px}
35
+ h2{font-size:26px;margin:0 0 6px;letter-spacing:-.01em}
36
+ h2 .num{font-family:var(--mono);color:var(--accent);font-size:18px;margin-right:10px}
37
+ h3{font-size:18px;margin:28px 0 8px;color:var(--ink)}
38
+ p{margin:12px 0}
39
+ .lead{font-size:17px;color:#cbd2de}
40
+ code,kbd{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;border:1px solid var(--line)}
41
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin:18px 0}
42
+ .card.tight{padding:14px 18px}
43
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
44
+ .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
45
+ @media(max-width:780px){.grid2,.grid3{grid-template-columns:1fr}}
46
+ .callout{border-left:3px solid var(--accent);background:var(--panel2);padding:14px 18px;border-radius:0 10px 10px 0;margin:16px 0}
47
+ .callout.bad{border-color:var(--bad)} .callout.good{border-color:var(--good)} .callout.warn{border-color:var(--warn)}
48
+ .callout h4{margin:0 0 6px;font-size:14px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
49
+ .tag{display:inline-block;font-family:var(--mono);font-size:11px;padding:2px 8px;border-radius:6px;border:1px solid var(--line);color:var(--muted)}
50
+ .tag.run{color:var(--run);border-color:rgba(62,207,142,.4)}
51
+ .tag.needs{color:var(--needs);border-color:rgba(255,180,84,.4)}
52
+ .tag.idle{color:var(--idle)}
53
+ .tag.resume{color:var(--resume);border-color:rgba(91,156,255,.4)}
54
+ .tag.review{color:var(--review);border-color:rgba(179,136,255,.4)}
55
+ .tag.srv{color:#7fd3ff;border-color:rgba(127,211,255,.3)}
56
+ .tag.cli{color:#ffd27f;border-color:rgba(255,210,127,.3)}
57
+ table{width:100%;border-collapse:collapse;font-size:13.5px;margin:12px 0}
58
+ th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
59
+ th{color:var(--muted);font-weight:600;font-size:12px;letter-spacing:.04em;text-transform:uppercase;position:sticky;top:42px;background:var(--panel)}
60
+ tbody tr:hover{background:var(--panel2)}
61
+ td code{font-size:12px;white-space:nowrap}
62
+ details{border:1px solid var(--line);border-radius:12px;background:var(--panel);margin:16px 0;overflow:hidden}
63
+ details>summary{cursor:pointer;padding:14px 18px;font-weight:600;list-style:none;display:flex;align-items:center;gap:10px}
64
+ details>summary::-webkit-details-marker{display:none}
65
+ details>summary::before{content:"▶";color:var(--accent);font-size:11px;transition:transform .15s}
66
+ details[open]>summary::before{transform:rotate(90deg)}
67
+ details .body{padding:0 18px 16px;border-top:1px solid var(--line)}
68
+ details .body .scroll{max-height:520px;overflow:auto;margin-top:12px}
69
+ .legend{display:flex;gap:14px;flex-wrap:wrap;margin:10px 0;font-size:13px;color:var(--muted)}
70
+ .legend span{display:inline-flex;align-items:center;gap:6px}
71
+ .dot{width:10px;height:10px;border-radius:3px;display:inline-block}
72
+ .svgwrap{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px;margin:18px 0;overflow:auto}
73
+ svg{display:block;max-width:100%;height:auto}
74
+ .ref{font-family:var(--mono);font-size:12px;color:var(--muted)}
75
+ ul.clean{margin:10px 0;padding-left:20px} ul.clean li{margin:6px 0}
76
+ .kicker{font-family:var(--mono);font-size:12px;color:var(--accent);text-transform:uppercase;letter-spacing:.14em;margin:0 0 4px}
77
+ .footnote{color:var(--muted);font-size:13px}
78
+ a.src{color:var(--accent);text-decoration:none;border-bottom:1px dotted rgba(91,156,255,.5)}
79
+ a.src:hover{border-bottom-style:solid}
80
+ hr.sep{border:0;border-top:1px solid var(--line);margin:40px 0}
81
+ .phase{display:flex;gap:14px;margin:14px 0;align-items:flex-start}
82
+ .phase .badge{flex:0 0 auto;font-family:var(--mono);font-weight:700;font-size:13px;width:40px;height:40px;border-radius:10px;
83
+ display:grid;place-items:center;background:var(--panel2);border:1px solid var(--line);color:var(--accent)}
84
+ .phase .pbody{flex:1}
85
+ .phase h4{margin:2px 0 4px;font-size:16px}
86
+ .num-stat{display:flex;gap:18px;flex-wrap:wrap;margin:8px 0}
87
+ .num-stat .n{font-size:30px;font-weight:700;line-height:1;color:var(--bad)}
88
+ .num-stat .l{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
89
+ .num-stat>div{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;min-width:130px}
90
+ </style>
91
+ </head>
92
+ <body>
93
+
94
+ <header class="hero">
95
+ <div class="wrap">
96
+ <p class="eyebrow">CTM · Design Proposal · 2026-06-18</p>
97
+ <h1>Session Status, Redesigned</h1>
98
+ <p class="sub">A live agent showed as <strong style="color:var(--needs)">Needs You</strong> while the server correctly knew it was <strong style="color:var(--run)">Running</strong>. This document explains why that keeps happening, surveys how mature systems model the same problem, and proposes a single-source-of-truth redesign that ends the patch-on-patch cycle.</p>
99
+ <div class="meta">
100
+ <span class="pill">Status model</span>
101
+ <span class="pill">Server SSOT + conditions</span>
102
+ <span class="pill">Event-first</span>
103
+ <span class="pill">Proposal — review before implementation</span>
104
+ </div>
105
+ </div>
106
+ </header>
107
+
108
+ <nav class="toc">
109
+ <div class="wrap">
110
+ <a href="#summary">1 · The bug</a>
111
+ <a href="#current">2 · Current architecture</a>
112
+ <a href="#root">3 · Root cause</a>
113
+ <a href="#priorart">4 · Prior art</a>
114
+ <a href="#redesign">5 · Proposed redesign</a>
115
+ <a href="#mapping">6 · Heuristic → condition</a>
116
+ <a href="#migration">7 · Migration</a>
117
+ <a href="#risks">8 · Risks</a>
118
+ </div>
119
+ </nav>
120
+
121
+ <div class="wrap">
122
+
123
+ <!-- ============================================================ 1 -->
124
+ <section id="summary">
125
+ <h2><span class="num">01</span>The bug, exactly</h2>
126
+ <p class="lead">A Claude Code session in plan mode launched two background <code>Explore</code> subagents and went quiet on its own PTY while it waited for them. The sidebar filed it under <strong style="color:var(--needs)">NEEDS YOU</strong>. The server's own resolver, queried read-only, said the opposite:</p>
127
+
128
+ <div class="card tight">
129
+ <div class="ref">GET /api/sessions/standup &nbsp;→&nbsp; session 2def4b45…</div>
130
+ <pre style="margin:8px 0 0;font-family:var(--mono);font-size:13px;color:#cbd2de;overflow:auto">{ "status": "running", "confidence": "high", "lastAgentOutputAt": &lt;~now&gt; }</pre>
131
+ </div>
132
+
133
+ <div class="callout bad">
134
+ <h4>The one-sentence diagnosis</h4>
135
+ <p style="margin:0">The server produced a correct, fresh, authoritative verdict — and the <strong>client threw it away</strong> and re-derived “idle” from the absence of recent PTY bytes. The session wasn't stuck. The <em>status pipeline</em> was.</p>
136
+ </div>
137
+
138
+ <p>This is not a one-off. It is the signature of a model that has been patched repeatedly: each new symptom got a new override with a new time-window, layered on top of the last. The numbers below come from reading the live resolvers end to end.</p>
139
+
140
+ <div class="num-stat">
141
+ <div><div class="n">40+</div><div class="l">distinct status heuristics</div></div>
142
+ <div><div class="n">5+</div><div class="l">engines that each re-derive status</div></div>
143
+ <div><div class="n">8</div><div class="l">TTL windows (client) + more on server</div></div>
144
+ <div><div class="n">4</div><div class="l">separate Codex “Working” detectors</div></div>
145
+ </div>
146
+
147
+ <p class="footnote">Every claim in this document is anchored to a specific file and line, cited inline as <span class="ref">file:line</span>. The proposal in §5–§7 changes <em>no production code</em> — it is the design to align on first.</p>
148
+ </section>
149
+
150
+ <!-- ============================================================ 2 -->
151
+ <section id="current">
152
+ <h2><span class="num">02</span>Current architecture</h2>
153
+ <p class="lead">Status is computed independently in at least five places. Each consumes overlapping raw signals, each re-derives a verdict, and the later stages are wired to <em>override</em> the earlier ones.</p>
154
+
155
+ <div class="legend">
156
+ <span><span class="dot" style="background:#7fd3ff"></span> server-side engine</span>
157
+ <span><span class="dot" style="background:#ffd27f"></span> client-side engine</span>
158
+ <span><span class="dot" style="background:var(--bad)"></span> override seam (where a correct verdict gets discarded)</span>
159
+ </div>
160
+
161
+ <div class="svgwrap">
162
+ <svg viewBox="0 0 1000 470" role="img" aria-label="Current data flow across five status engines">
163
+ <defs>
164
+ <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#9aa3b2"/></marker>
165
+ <marker id="arrR" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#ff6b6b"/></marker>
166
+ </defs>
167
+ <!-- raw signals -->
168
+ <rect x="20" y="30" width="200" height="410" rx="12" fill="#1d212b" stroke="#2a2f3a"/>
169
+ <text x="120" y="55" text-anchor="middle" fill="#9aa3b2" font-size="12" font-family="monospace">RAW SIGNALS</text>
170
+ <g font-size="11" fill="#cbd2de" font-family="monospace">
171
+ <text x="36" y="84">PTY bytes / silence</text>
172
+ <text x="36" y="112">Claude hooks (Stop,</text>
173
+ <text x="36" y="126">SubagentStop, Notif.)</text>
174
+ <text x="36" y="154">Codex turn detector</text>
175
+ <text x="36" y="182">Codex TUI “Working”</text>
176
+ <text x="36" y="210">SessionStateBus</text>
177
+ <text x="36" y="238">telemetry-receiver</text>
178
+ <text x="36" y="266">Wall-E work-state</text>
179
+ <text x="36" y="294">turn open/close marks</text>
180
+ <text x="36" y="322">restore lifecycle</text>
181
+ <text x="36" y="350">waiting-for-input</text>
182
+ <text x="36" y="378">last input keystroke</text>
183
+ <text x="36" y="406">activation snapshot</text>
184
+ </g>
185
+
186
+ <!-- server engines -->
187
+ <g>
188
+ <rect x="270" y="40" width="230" height="78" rx="10" fill="#13202b" stroke="#2f5066"/>
189
+ <text x="385" y="66" text-anchor="middle" fill="#7fd3ff" font-size="13" font-weight="bold">① _standupLiveStatus…</text>
190
+ <text x="385" y="86" text-anchor="middle" fill="#9aa3b2" font-size="11" font-family="monospace">server.js:10976</text>
191
+ <text x="385" y="104" text-anchor="middle" fill="#9aa3b2" font-size="11">11-branch cascade</text>
192
+ </g>
193
+ <g>
194
+ <rect x="270" y="135" width="230" height="64" rx="10" fill="#13202b" stroke="#2f5066"/>
195
+ <text x="385" y="160" text-anchor="middle" fill="#7fd3ff" font-size="13" font-weight="bold">② classifySessionStandup</text>
196
+ <text x="385" y="180" text-anchor="middle" fill="#9aa3b2" font-size="11" font-family="monospace">lib/session-standup.js</text>
197
+ </g>
198
+ <g>
199
+ <rect x="270" y="216" width="230" height="64" rx="10" fill="#13202b" stroke="#2f5066"/>
200
+ <text x="385" y="241" text-anchor="middle" fill="#7fd3ff" font-size="13" font-weight="bold">③ 3s heartbeat broadcast</text>
201
+ <text x="385" y="261" text-anchor="middle" fill="#9aa3b2" font-size="11" font-family="monospace">server.js:~30398</text>
202
+ </g>
203
+
204
+ <!-- client engines -->
205
+ <g>
206
+ <rect x="560" y="120" width="240" height="86" rx="10" fill="#241f12" stroke="#6a5526"/>
207
+ <text x="680" y="146" text-anchor="middle" fill="#ffd27f" font-size="13" font-weight="bold">④ resolveSessionStatus</text>
208
+ <text x="680" y="166" text-anchor="middle" fill="#9aa3b2" font-size="11" font-family="monospace">session-status-precedence.js</text>
209
+ <text x="680" y="184" text-anchor="middle" fill="#9aa3b2" font-size="11">~20 inputs · 8 TTLs · 15-step cascade</text>
210
+ </g>
211
+ <g>
212
+ <rect x="560" y="232" width="240" height="78" rx="10" fill="#241f12" stroke="#6a5526"/>
213
+ <text x="680" y="258" text-anchor="middle" fill="#ffd27f" font-size="13" font-weight="bold">⑤ getSessionStatus — Path B</text>
214
+ <text x="680" y="278" text-anchor="middle" fill="#9aa3b2" font-size="11" font-family="monospace">index.html:37963 (legacy dup)</text>
215
+ <text x="680" y="296" text-anchor="middle" fill="#9aa3b2" font-size="11">near-identical fallback ladder</text>
216
+ </g>
217
+
218
+ <!-- render -->
219
+ <rect x="850" y="150" width="130" height="120" rx="10" fill="#1d212b" stroke="#2a2f3a"/>
220
+ <text x="915" y="200" text-anchor="middle" fill="#e6e9ef" font-size="13" font-weight="bold">Sidebar</text>
221
+ <text x="915" y="222" text-anchor="middle" fill="#9aa3b2" font-size="11">lane + chip</text>
222
+
223
+ <!-- flows raw->engines -->
224
+ <line x1="220" y1="120" x2="270" y2="80" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
225
+ <line x1="220" y1="200" x2="270" y2="167" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
226
+ <line x1="220" y1="250" x2="270" y2="248" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
227
+ <line x1="220" y1="320" x2="560" y2="170" stroke="#9aa3b2" stroke-width="1" opacity=".5" marker-end="url(#arr)"/>
228
+ <line x1="220" y1="380" x2="560" y2="270" stroke="#9aa3b2" stroke-width="1" opacity=".5" marker-end="url(#arr)"/>
229
+
230
+ <!-- server -> client (push) -->
231
+ <line x1="500" y1="79" x2="560" y2="150" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
232
+ <line x1="500" y1="248" x2="560" y2="180" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
233
+
234
+ <!-- OVERRIDE seam: client discards server -->
235
+ <path d="M680,206 C680,220 680,224 680,232" stroke="#ff6b6b" stroke-width="2.2" fill="none" marker-end="url(#arrR)"/>
236
+ <text x="694" y="224" fill="#ff6b6b" font-size="10" font-family="monospace">override</text>
237
+
238
+ <!-- client -> render -->
239
+ <line x1="800" y1="165" x2="850" y2="200" stroke="#9aa3b2" stroke-width="1.4" marker-end="url(#arr)"/>
240
+
241
+ <!-- the killer seam annotation -->
242
+ <rect x="540" y="40" width="320" height="58" rx="8" fill="rgba(255,107,107,.08)" stroke="#ff6b6b" stroke-dasharray="4 3"/>
243
+ <text x="700" y="62" text-anchor="middle" fill="#ff6b6b" font-size="11.5" font-weight="bold">THE DEFINING ANTI-PATTERN</text>
244
+ <text x="700" y="80" text-anchor="middle" fill="#cbd2de" font-size="10.5">client takes server's fresh status, runs 4 “is-server-stale?”</text>
245
+ <text x="700" y="93" text-anchor="middle" fill="#cbd2de" font-size="10.5">checks, then discards it — precedence.js:134–157</text>
246
+ </svg>
247
+ </div>
248
+
249
+ <h3>The override block, verbatim</h3>
250
+ <p>The server pushes a <code>serverLiveStatus</code> that is <em>fresh</em> (received this second) and <em>authoritative</em>. The client's first move is to look for reasons to ignore it:</p>
251
+ <div class="card tight">
252
+ <pre style="margin:0;font-family:var(--mono);font-size:12.5px;color:#cbd2de;overflow:auto">// session-status-precedence.js:134–157
253
+ const serverLiveRunningIsStale = serverLive.cls === 'running' &amp;&amp; …eventAge &gt; maxAge;
254
+ const serverLiveWaitingIsStale = serverLive.cls === 'waiting' &amp;&amp; waitingIsStale;
255
+ const serverLiveIdleShouldYieldToWaiting = serverLive.cls === 'idle' &amp;&amp; waitingForInput;
256
+ const serverLiveIsStaleNonRunning = serverLive.cls !== 'running' &amp;&amp; ( terminalBusy || streamRunning
257
+ || codexRunningHold || providerRunningHold || activationPreservedRunning );
258
+ if (!serverLiveRunningIsStale &amp;&amp; !serverLiveWaitingIsStale
259
+ &amp;&amp; !serverLiveIsStaleNonRunning &amp;&amp; !serverLiveIdleShouldYieldToWaiting) {
260
+ return serverLive; // ← the ONLY path that trusts the server
261
+ }
262
+ // …otherwise fall through and re-derive from local PTY/stream/hold signals</pre>
263
+ </div>
264
+ <p>The exact same four-way override is then <strong>duplicated a second time</strong> inline in the client as “Path B” at <a class="src" href="#">index.html:37977–38004</a>, used whenever the precedence module is unavailable. Two copies of the same fragile logic, maintained by hand.</p>
265
+
266
+ <details>
267
+ <summary>Full heuristic inventory — 40+ decision points across both engines <span class="ref">(click to expand)</span></summary>
268
+ <div class="body">
269
+ <p class="footnote">Consolidated from the two live cascades: client <code>resolveSessionStatus</code> (precedence.js:75–232) and server <code>_standupLiveStatusWithReasonForSession</code> (server.js:10976–11039), plus the four Codex “Working” detectors and the PTY authority gate. Ordered by evaluation precedence within each engine.</p>
270
+ <div class="scroll">
271
+ <table>
272
+ <thead><tr><th>#</th><th>Heuristic / branch</th><th>Symptom it patched</th><th>Where</th><th>Side</th></tr></thead>
273
+ <tbody>
274
+ <tr><td>1</td><td><code>exited</code> short-circuit</td><td>dead PTY shown active</td><td>precedence.js:78</td><td><span class="tag cli">client</span></td></tr>
275
+ <tr><td>2</td><td><code>metaType==='walle'</code> generating/idle</td><td>chat tabs have no PTY</td><td>precedence.js:79</td><td><span class="tag cli">client</span></td></tr>
276
+ <tr><td>3</td><td>serverLive fresh + <code>resuming</code> → return</td><td>restore flicker</td><td>precedence.js:133</td><td><span class="tag cli">client</span></td></tr>
277
+ <tr><td>4</td><td><code>serverLiveRunningIsStale</code> override</td><td>lost stop-hook pins Running forever</td><td>precedence.js:134</td><td><span class="tag cli">client</span></td></tr>
278
+ <tr><td>5</td><td><code>serverLiveWaitingIsStale</code> override</td><td>30-min stale prompt won't clear</td><td>precedence.js:138</td><td><span class="tag cli">client</span></td></tr>
279
+ <tr><td>6</td><td><code>serverLiveIdleShouldYieldToWaiting</code></td><td>idle heartbeat masks a real prompt</td><td>precedence.js:139</td><td><span class="tag cli">client</span></td></tr>
280
+ <tr><td>7</td><td><code>serverLiveIsStaleNonRunning</code> (5 sub-clauses)</td><td>idle heartbeat masks visible busy TUI</td><td>precedence.js:144</td><td><span class="tag cli">client</span></td></tr>
281
+ <tr><td>8</td><td>authoritative + <code>working</code> → running</td><td>hook/OTEL turn start</td><td>precedence.js:192</td><td><span class="tag cli">client</span></td></tr>
282
+ <tr><td>9</td><td>authoritative + <code>waitingForInput</code> → waiting</td><td>Notification hook</td><td>precedence.js:193</td><td><span class="tag cli">client</span></td></tr>
283
+ <tr><td>10</td><td>authoritative + <code>codexRunningHold</code></td><td>Codex bursts go quiet between writes</td><td>precedence.js:194</td><td><span class="tag cli">client</span></td></tr>
284
+ <tr><td>11</td><td>authoritative + <code>providerRunningHold</code></td><td>generic provider burst gap</td><td>precedence.js:195</td><td><span class="tag cli">client</span></td></tr>
285
+ <tr><td>12</td><td>authoritative + <code>newerTerminalBusy</code></td><td>idle hook vs visible “Working” footer</td><td>precedence.js:196</td><td><span class="tag cli">client</span></td></tr>
286
+ <tr><td>13</td><td>authoritative + <code>newerRunningEvidence</code> (4 inputs)</td><td>stale stop-hook vs newer PTY/stream</td><td>precedence.js:201</td><td><span class="tag cli">client</span></td></tr>
287
+ <tr><td>14</td><td>authoritative-idle default</td><td>trust fresh stop-hook</td><td>precedence.js:207</td><td><span class="tag cli">client</span></td></tr>
288
+ <tr><td>15</td><td><code>serverWorking</code> (10s TTL, vs waiting)</td><td>server-observed working signal</td><td>precedence.js:211</td><td><span class="tag cli">client</span></td></tr>
289
+ <tr><td>16</td><td><code>waitingForInput</code> → waiting</td><td>blocking prompt</td><td>precedence.js:212</td><td><span class="tag cli">client</span></td></tr>
290
+ <tr><td>17</td><td><code>activeTurnOpen</code> (≤45-min quiet) → running</td><td>long silent turn (THE subagent case)</td><td>precedence.js:213</td><td><span class="tag cli">client</span></td></tr>
291
+ <tr><td>18</td><td><code>streamSettlesLocalBusy</code> → stream</td><td>stream settles a local-only busy guess</td><td>precedence.js:214</td><td><span class="tag cli">client</span></td></tr>
292
+ <tr><td>19</td><td><code>terminalBusyStatus</code> → running</td><td>Codex “Working” on screen</td><td>precedence.js:215</td><td><span class="tag cli">client</span></td></tr>
293
+ <tr><td>20</td><td><code>codexRunningHold</code> (15s) → running</td><td>Codex inter-burst gap</td><td>precedence.js:216</td><td><span class="tag cli">client</span></td></tr>
294
+ <tr><td>21</td><td><code>providerRunningHold</code> → running</td><td>generic inter-burst gap</td><td>precedence.js:217</td><td><span class="tag cli">client</span></td></tr>
295
+ <tr><td>22</td><td><code>pendingNonRunningStatus</code> demotion guard</td><td>queued demotion vs newer running</td><td>precedence.js:218</td><td><span class="tag cli">client</span></td></tr>
296
+ <tr><td>23</td><td><code>streamFresh</code> (60s) → stream</td><td>SessionStream status</td><td>precedence.js:224</td><td><span class="tag cli">client</span></td></tr>
297
+ <tr><td>24</td><td><code>recentOutput</code> (5s) → running</td><td>any recent PTY byte</td><td>precedence.js:225</td><td><span class="tag cli">client</span></td></tr>
298
+ <tr><td>25</td><td><code>restoreResuming</code> → resuming</td><td>restart in flight</td><td>precedence.js:226</td><td><span class="tag cli">client</span></td></tr>
299
+ <tr><td>26</td><td><code>activationPreservedRunning</code></td><td>tab-switch must not demote</td><td>precedence.js:227</td><td><span class="tag cli">client</span></td></tr>
300
+ <tr><td>27</td><td><code>waiting-fallback</code> (no recent output)</td><td>second-chance waiting</td><td>precedence.js:228</td><td><span class="tag cli">client</span></td></tr>
301
+ <tr><td>28</td><td><code>recentInput</code> (3s) → running</td><td>optimistic keystroke echo</td><td>precedence.js:230</td><td><span class="tag cli">client</span></td></tr>
302
+ <tr><td>29</td><td><code>default-idle</code></td><td>nothing matched</td><td>precedence.js:231</td><td><span class="tag cli">client</span></td></tr>
303
+ <tr><td>30</td><td>walle <code>pending-permission</code> → waiting</td><td>coding permission card</td><td>server.js:10982</td><td><span class="tag srv">server</span></td></tr>
304
+ <tr><td>31</td><td>walle waiting-input / generating / idle</td><td>chat tab lifecycle</td><td>server.js:10983</td><td><span class="tag srv">server</span></td></tr>
305
+ <tr><td>32</td><td>codex <code>pty-activity:over-waiting</code></td><td>Codex types past a stale prompt</td><td>server.js:10998</td><td><span class="tag srv">server</span></td></tr>
306
+ <tr><td>33</td><td><code>waiting:&lt;reason&gt;</code> blocking vs not</td><td>approval vs cosmetic prompt</td><td>server.js:11004</td><td><span class="tag srv">server</span></td></tr>
307
+ <tr><td>34</td><td>bus <code>waiting_input</code> + current evidence</td><td>StateBus prompt w/ grace window</td><td>server.js:11014</td><td><span class="tag srv">server</span></td></tr>
308
+ <tr><td>35</td><td><code>restore:starting</code> → resuming</td><td>startup restore</td><td>server.js:11018</td><td><span class="tag srv">server</span></td></tr>
309
+ <tr><td>36</td><td>bus <code>running</code> → running</td><td>StateBus busy</td><td>server.js:11020</td><td><span class="tag srv">server</span></td></tr>
310
+ <tr><td>37</td><td><code>recentPtyActivity</code> (authority-gated) → running</td><td>fresh bytes that pass the gate</td><td>server.js:11021</td><td><span class="tag srv">server</span></td></tr>
311
+ <tr><td>38</td><td><code>_serverSessionHasOpenTurn</code> → running</td><td>turn-open marker (hook/detector)</td><td>server.js:11022</td><td><span class="tag srv">server</span></td></tr>
312
+ <tr><td>39</td><td><code>startup-hold</code> window → running</td><td>just-restored grace</td><td>server.js:11023</td><td><span class="tag srv">server</span></td></tr>
313
+ <tr><td>40</td><td>bus resuming / passthrough</td><td>misc StateBus states</td><td>server.js:11024</td><td><span class="tag srv">server</span></td></tr>
314
+ <tr><td>41</td><td><code>idle:authority-gated-bytes</code></td><td>bytes arrived but gate vetoed Running</td><td>server.js:11034</td><td><span class="tag srv">server</span></td></tr>
315
+ <tr><td>42</td><td><code>_ptyActivityMayDriveRunningForSession</code> gate</td><td>cosmetic repaint ≠ real work</td><td>server.js:10718</td><td><span class="tag srv">server</span></td></tr>
316
+ <tr><td>43–46</td><td>4× Codex “Working” detectors (anchored, garbled, footer, terminal-buffer scan)</td><td>reflow-split status line, ps-cipher garble, etc.</td><td>index.html:~16346 + status-detector</td><td><span class="tag cli">client</span></td></tr>
317
+ </tbody>
318
+ </table>
319
+ </div>
320
+ </div>
321
+ </details>
322
+ </section>
323
+
324
+ <!-- ============================================================ 3 -->
325
+ <section id="root">
326
+ <h2><span class="num">03</span>Root-cause analysis</h2>
327
+ <p class="lead">Three structural choices, compounding, make every fix a patch on a patch.</p>
328
+
329
+ <div class="grid3">
330
+ <div class="card">
331
+ <p class="kicker">Cause 1</p>
332
+ <h3 style="margin-top:0">Status is inferred, not observed</h3>
333
+ <p>The base signal is <strong>PTY silence</strong> — “no bytes for N ms ⇒ probably idle.” But a real agent goes silent for legitimate reasons: waiting on a subagent, thinking, blocked on a network call. Every false-idle then needs a counter-heuristic (a “hold”, a “busy footer scan”) to re-assert running.</p>
334
+ </div>
335
+ <div class="card">
336
+ <p class="kicker">Cause 2</p>
337
+ <h3 style="margin-top:0">Five engines, no owner</h3>
338
+ <p>Server and client <em>both</em> derive a verdict from overlapping raw inputs, and the client is wired to override the server. There is no single function that owns “what is the phase?” — so a fix in one engine silently disagrees with the other four.</p>
339
+ </div>
340
+ <div class="card">
341
+ <p class="kicker">Cause 3</p>
342
+ <h3 style="margin-top:0">Time-windows age out independently</h3>
343
+ <p>8 client TTLs (<code>10s</code> serverLive, <code>60s</code> stream, <code>5s</code> output, <code>15s</code> codexHold, <code>45min</code> turn, <code>30min</code> waiting…) plus server holds. Whichever expires first flips the verdict, producing the flicker between Running ⇄ Needs You ⇄ Idle.</p>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="callout bad">
348
+ <h4>Why the reported bug happened</h4>
349
+ <p>The subagent case hits all three at once: the parent's PTY is silent (Cause 1), the server's <code>activeTurnOpen</code>/hook verdict said <em>running</em> but the client's override block re-derived from local silence (Cause 2), and the <code>recentOutput</code> 5s window had long expired while the <code>activeTurnOpen</code> 45-min window <em>should</em> have held — except the client reached its override path before consulting it (Cause 3). The verdict existed and was correct; the pipeline routed around it.</p>
350
+ </div>
351
+
352
+ <p>The deeper tell: <strong>every heuristic in the §2 inventory is a reaction to a previous heuristic's failure.</strong> #10–#13 exist to undo #14's over-eager idle. #7 exists to undo #4–#6. #32 exists to undo #33. The system is a stack of mutually-correcting overrides — which is exactly what “patch on patch” means, made literal.</p>
353
+ </section>
354
+
355
+ <!-- ============================================================ 4 -->
356
+ <section id="priorart">
357
+ <h2><span class="num">04</span>How mature systems model this</h2>
358
+ <p class="lead">The “derive one phase from many independent observed conditions, computed once, owned by one authority” pattern is well-established. Four references shaped this proposal.</p>
359
+
360
+ <div class="grid2">
361
+ <div class="card">
362
+ <h3 style="margin-top:0">Kubernetes — phase is <em>derived</em>, conditions are the truth</h3>
363
+ <p>A Pod's <code>status.phase</code> is a coarse summary <strong>computed from</strong> a set of independent <code>conditions[]</code> (<code>PodScheduled</code>, <code>ContainersReady</code>, <code>Ready</code>), each a <code>{type, status, lastTransitionTime, reason}</code> record. The phase is never a separate state machine you mutate — it falls out of the conditions. This is the core borrowed idea.</p>
364
+ <p class="footnote">kubernetes.io · Pod Lifecycle — Pod conditions &amp; phase.</p>
365
+ </div>
366
+ <div class="card">
367
+ <h3 style="margin-top:0">systemd — two-level state, events over polling</h3>
368
+ <p>Units carry a high-level <code>ActiveState</code> (active/activating/failed…) and a granular <code>SubState</code>. Transitions are driven by <strong>events</strong> from the process lifecycle, not by sampling. The lesson: a small derived top-level state plus a richer substate, fed by lifecycle events.</p>
369
+ <p class="footnote">freedesktop.org · systemd unit state model.</p>
370
+ </div>
371
+ <div class="card">
372
+ <h3 style="margin-top:0">tmux — activity/silence are <em>alerts</em>, not state</h3>
373
+ <p>tmux's <code>monitor-activity</code> / <code>monitor-silence</code> flag a window as “something happened” — they are notifications layered <em>over</em> the real pane state, never the state itself. The lesson: treat PTY output/silence as <em>evidence feeding a condition</em>, never as the phase.</p>
374
+ <p class="footnote">man tmux · monitor-activity, monitor-silence.</p>
375
+ </div>
376
+ <div class="card">
377
+ <h3 style="margin-top:0">Claude Code hooks — authoritative turn boundaries</h3>
378
+ <p><code>Stop</code> / <code>SubagentStop</code> mark a turn (or subagent turn) ending; <code>Notification</code> fires when the agent is awaiting input. These are <strong>ground-truth lifecycle events</strong> — far stronger than inferring “done” from silence. CTM already runs hooks mode by default and has <code>telemetry-receiver.js</code> to ingest them.</p>
379
+ <p class="footnote">docs.claude.com · Claude Code hooks reference.</p>
380
+ </div>
381
+ </div>
382
+
383
+ <div class="callout good">
384
+ <h4>The synthesis these point to</h4>
385
+ <p style="margin:0"><strong>Event-driven conditions + a single derive function + a reconciliation backstop.</strong> Lifecycle events set authoritative conditions; one pure function derives the phase; a periodic reconcile sweeps up missed events. Silence-inference survives only as the lowest-priority fallback condition — never as an override.</p>
386
+ </div>
387
+ </section>
388
+
389
+ <!-- ============================================================ 5 -->
390
+ <section id="redesign">
391
+ <h2><span class="num">05</span>Proposed redesign</h2>
392
+ <p class="lead">One pure module owns the model. The server is the single source of truth. The client renders the server's phase and may only <em>escalate optimistically</em> — it can never demote or second-guess.</p>
393
+
394
+ <h3>Before / after</h3>
395
+ <div class="svgwrap">
396
+ <svg viewBox="0 0 1000 320" role="img" aria-label="Before and after architecture">
397
+ <defs><marker id="a2" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#9aa3b2"/></marker>
398
+ <marker id="a2g" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#3ecf8e"/></marker></defs>
399
+
400
+ <text x="250" y="28" text-anchor="middle" fill="#ff6b6b" font-size="13" font-weight="bold" font-family="monospace">BEFORE — 5 engines override each other</text>
401
+ <g font-size="11" font-family="monospace">
402
+ <rect x="60" y="44" width="120" height="40" rx="8" fill="#13202b" stroke="#2f5066"/><text x="120" y="68" text-anchor="middle" fill="#7fd3ff">server ×3</text>
403
+ <rect x="320" y="44" width="120" height="40" rx="8" fill="#241f12" stroke="#6a5526"/><text x="380" y="68" text-anchor="middle" fill="#ffd27f">client ×2</text>
404
+ <rect x="60" y="120" width="120" height="40" rx="8" fill="#241f12" stroke="#6a5526"/><text x="120" y="144" text-anchor="middle" fill="#ffd27f">overrides</text>
405
+ <rect x="320" y="120" width="120" height="40" rx="8" fill="#1d212b" stroke="#2a2f3a"/><text x="380" y="144" text-anchor="middle" fill="#e6e9ef">sidebar</text>
406
+ </g>
407
+ <line x1="180" y1="64" x2="320" y2="64" stroke="#9aa3b2" marker-end="url(#a2)"/>
408
+ <path d="M380,84 C380,100 380,104 380,120" stroke="#ff6b6b" stroke-width="2" marker-end="url(#a2)"/>
409
+ <text x="392" y="106" fill="#ff6b6b" font-size="10">discard</text>
410
+ <line x1="320" y1="140" x2="180" y2="140" stroke="#ff6b6b" stroke-dasharray="3 3" marker-end="url(#a2)"/>
411
+ <line x1="120" y1="160" x2="350" y2="135" stroke="#9aa3b2" opacity=".4" marker-end="url(#a2)"/>
412
+
413
+ <line x1="500" y1="30" x2="500" y2="300" stroke="#2a2f3a"/>
414
+
415
+ <text x="760" y="28" text-anchor="middle" fill="#3ecf8e" font-size="13" font-weight="bold" font-family="monospace">AFTER — conditions → one phase → render</text>
416
+ <g font-size="11" font-family="monospace">
417
+ <rect x="540" y="50" width="150" height="80" rx="8" fill="#1d212b" stroke="#2a2f3a"/>
418
+ <text x="615" y="72" text-anchor="middle" fill="#9aa3b2">conditions[]</text>
419
+ <text x="615" y="92" text-anchor="middle" fill="#cbd2de" font-size="10">TurnOpen · AwaitingInput</text>
420
+ <text x="615" y="106" text-anchor="middle" fill="#cbd2de" font-size="10">OutputRecent · Restoring…</text>
421
+ <text x="615" y="120" text-anchor="middle" fill="#cbd2de" font-size="10">each {value,at,source,auth}</text>
422
+
423
+ <rect x="730" y="60" width="150" height="60" rx="8" fill="#13261c" stroke="#2f6648"/>
424
+ <text x="805" y="84" text-anchor="middle" fill="#3ecf8e" font-weight="bold">derivePhase()</text>
425
+ <text x="805" y="102" text-anchor="middle" fill="#9aa3b2" font-size="10">lib/session-phase.js</text>
426
+
427
+ <rect x="730" y="170" width="150" height="46" rx="8" fill="#1d212b" stroke="#2a2f3a"/>
428
+ <text x="805" y="198" text-anchor="middle" fill="#e6e9ef">sidebar (renders)</text>
429
+
430
+ <rect x="540" y="170" width="150" height="46" rx="8" fill="#241f12" stroke="#6a5526"/>
431
+ <text x="615" y="190" text-anchor="middle" fill="#ffd27f" font-size="10">optimistic overlay</text>
432
+ <text x="615" y="205" text-anchor="middle" fill="#9aa3b2" font-size="9">idle→running ONLY</text>
433
+ </g>
434
+ <line x1="690" y1="90" x2="730" y2="90" stroke="#9aa3b2" marker-end="url(#a2)"/>
435
+ <line x1="805" y1="120" x2="805" y2="170" stroke="#3ecf8e" stroke-width="2" marker-end="url(#a2g)"/>
436
+ <path d="M690,193 C710,193 712,193 730,193" stroke="#ffd27f" stroke-dasharray="3 3" marker-end="url(#a2)"/>
437
+ <text x="690" y="240" fill="#9aa3b2" font-size="10" font-family="monospace">overlay can escalate, never demote → reconciled by next server push</text>
438
+ </svg>
439
+ </div>
440
+
441
+ <h3>One pure module: <code>lib/session-phase.js</code></h3>
442
+ <p>Same dual node/browser export style as today's <code>session-status-precedence.js</code>, so server and client share <em>identical</em> logic. It exposes exactly two concepts:</p>
443
+
444
+ <div class="grid2">
445
+ <div class="card">
446
+ <p class="kicker">Conditions</p>
447
+ <p>Independent observed facts, each <code>{value, at, source, authoritative}</code>:</p>
448
+ <ul class="clean">
449
+ <li><code>ProcessAlive</code> — PTY/process exists</li>
450
+ <li><code>TurnOpen</code> — a turn is in progress (hook/detector/marker)</li>
451
+ <li><code>AwaitingInput</code> (+ <code>blocking</code> sub-flag) — agent wants the operator</li>
452
+ <li><code>OutputRecent</code> — bytes within the burst window</li>
453
+ <li><code>Restoring</code> — restart/resume in flight</li>
454
+ <li><code>ReviewReady</code> — turn finished, awaiting human review</li>
455
+ <li><code>InputRecent</code> — optimistic, client-only</li>
456
+ </ul>
457
+ </div>
458
+ <div class="card">
459
+ <p class="kicker">derivePhase(conditions, now)</p>
460
+ <p>The <strong>only</strong> producer of a phase string. One priority order, one set of constants, defined here and nowhere else:</p>
461
+ <ol class="clean">
462
+ <li><code>Restoring</code> → <span class="tag resume">resuming</span></li>
463
+ <li><code>AwaitingInput</code> → <span class="tag needs">needs_you</span></li>
464
+ <li><code>TurnOpen</code> (authoritative, within quiet bound) → <span class="tag run">running</span></li>
465
+ <li><code>OutputRecent</code> / <code>InputRecent</code> (only if no authoritative source contradicts) → <span class="tag run">running</span></li>
466
+ <li><code>ReviewReady</code> → <span class="tag review">review</span></li>
467
+ <li>else → <span class="tag idle">idle</span></li>
468
+ </ol>
469
+ </div>
470
+ </div>
471
+
472
+ <div class="callout good">
473
+ <h4>The asymmetry that kills divergence</h4>
474
+ <p>The client renders the server's <code>phase</code> directly. The <em>only</em> local liberty it has is an <strong>optimistic overlay that escalates idle → running</strong> for instant keystroke/output feedback — auto-expiring, and reconciled by the next server push. The client <strong>never demotes</strong> and never re-derives. All four “is-the-server-stale?” checks and the entire legacy Path B are deleted. Divergence becomes structurally impossible: there is one verdict, computed once, and one direction the client may nudge it.</p>
475
+ </div>
476
+
477
+ <h3>Event-first producers</h3>
478
+ <p>The 40+ heuristics don't vanish — they <strong>demote from phase-deciders to condition-producers</strong>. They stop voting on the answer and start reporting facts:</p>
479
+ <ul class="clean">
480
+ <li>The four Codex “Working” detectors all feed the single <code>TurnOpen</code> / <code>OutputRecent</code> condition — they no longer each fork the status.</li>
481
+ <li>The authority-inversion gate (#42) becomes the <code>authoritative</code> flag on a condition, encoded once inside <code>derivePhase</code> rather than scattered.</li>
482
+ <li>Conditions are populated primarily from existing event infra: <code>SessionStateBus</code> (<span class="ref">lib/session-state-bus.js</span>), the <code>telemetry-receiver</code> authoritative source, and turn-lifecycle markers (<code>_markServerSessionTurnOpen/Closed</code>, already hook/detector-driven).</li>
483
+ <li>PTY-silence inference survives only as the lowest-priority <code>OutputRecent</code> fallback — it can no longer override an authoritative condition.</li>
484
+ </ul>
485
+ </section>
486
+
487
+ <!-- ============================================================ 6 -->
488
+ <section id="mapping">
489
+ <h2><span class="num">06</span>Heuristic → condition mapping</h2>
490
+ <p class="lead">Proof that the redesign loses no coverage: every heuristic in the §2 inventory folds into a small set of condition producers.</p>
491
+ <table>
492
+ <thead><tr><th>Today (heuristic #s)</th><th>Becomes condition</th><th>Carried by</th></tr></thead>
493
+ <tbody>
494
+ <tr><td>#8, #15, #36, #37, #38 — working / bus-running / pty-activity / turn-open</td><td><code>TurnOpen</code> <span class="tag run">→ running</span></td><td>authoritative; hook/detector/StateBus</td></tr>
495
+ <tr><td>#17, #20, #21, #24, #28, #32 — active-turn / holds / recent-output / recent-input</td><td><code>OutputRecent</code> + <code>InputRecent</code></td><td>fallback (non-authoritative); InputRecent client-only</td></tr>
496
+ <tr><td>#19, #43–46 — Codex “Working” footer (all 4 detectors)</td><td><code>TurnOpen</code> evidence</td><td>one producer, feeds the single condition</td></tr>
497
+ <tr><td>#9, #16, #27, #30, #33, #34 — waiting / approval / permission</td><td><code>AwaitingInput</code> (+ <code>blocking</code>)</td><td>authoritative; hook Notification / StateBus</td></tr>
498
+ <tr><td>#3, #25, #35, #39 — resuming / restore / startup-hold</td><td><code>Restoring</code></td><td>restore lifecycle</td></tr>
499
+ <tr><td>#1, #41 — exited / authority-gated</td><td><code>ProcessAlive=false</code> / gate flag</td><td>process + authority gate</td></tr>
500
+ <tr><td>#2, #31 — walle generating/idle</td><td><code>TurnOpen</code> (generating) / idle</td><td>Wall-E work-state</td></tr>
501
+ <tr><td>#4–7, #10–14, #18, #22, #26 — the override/demotion guards</td><td><strong>deleted</strong></td><td>subsumed by the escalate-only overlay + single derive</td></tr>
502
+ <tr><td>#5, #29, #40 — stale-waiting / default-idle / passthrough</td><td>fall-through to <code>idle</code></td><td><code>derivePhase</code> default branch</td></tr>
503
+ </tbody>
504
+ </table>
505
+ <p class="footnote">The whole #4–#7 / #10–#14 / #18 / #22 / #26 cluster — roughly a third of the inventory — is pure override machinery. It exists only because the client second-guesses the server. Remove the second-guessing and that machinery deletes outright with zero behavioral loss.</p>
506
+ </section>
507
+
508
+ <!-- ============================================================ 7 -->
509
+ <section id="migration">
510
+ <h2><span class="num">07</span>Migration phases</h2>
511
+ <p class="lead">Staged so each step is independently shippable and reversible. <strong>None of this is built yet</strong> — this is the plan the implementation will follow after approval.</p>
512
+
513
+ <div class="phase"><div class="badge">P0</div><div class="pbody"><h4>Add the module, no wiring</h4><p>Create <code>lib/session-phase.js</code> (conditions + <code>derivePhase</code>) with unit tests. Encode <strong>each of the 40+ inventory scenarios</strong> as a test case (subagent-silent → running, codex-garbled-Working → running, wall-e work-state → running, non-blocking-waiting-stale → idle…). Ships dormant.</p></div></div>
514
+ <div class="phase"><div class="badge">P1</div><div class="pbody"><h4>Server emits conditions + phase</h4><p>Server populates <code>conditions[]</code> and computes <code>phase</code> via the new module, alongside the existing fields (back-compat). Nothing consumes it yet — pure shadow output for differential checking.</p></div></div>
515
+ <div class="phase"><div class="badge">P2</div><div class="pbody"><h4>Client renders phase; delete the overrides</h4><p>Client renders <code>phase</code> directly. Remove the override block (<span class="ref">precedence.js:134–157</span>) and the legacy Path B (<span class="ref">index.html:37963–38004+</span>). Add the thin optimistic idle→running overlay. <strong>This is the step that fixes the reported bug.</strong></p></div></div>
516
+ <div class="phase"><div class="badge">P3</div><div class="pbody"><h4>Collapse the duplicate server surfaces</h4><p>Point the standup classifier and the 3s heartbeat at the same <code>derivePhase</code>. Dedupe <code>normalizeLiveStatus</code> / <code>isBlockingWaitingReason</code> (currently duplicated across files).</p></div></div>
517
+ <div class="phase"><div class="badge">P4</div><div class="pbody"><h4>Detectors → pure condition producers</h4><p>Convert remaining detectors into condition producers; delete the now-dead heuristics from the §6 “deleted” row. End state: one model, one authority, one derive.</p></div></div>
518
+
519
+ <div class="callout warn">
520
+ <h4>Verification (every step)</h4>
521
+ <p style="margin:0">Unit tests per inventory scenario · <code>/ctm-dev</code> E2E + Playwright <strong>on the dev port only — never primary 3456/3457</strong> · the exact repro (Claude plan-mode waiting on a subagent) must stay <span class="tag run">running</span> in the sidebar · differential check of new <code>phase</code> vs old <code>status</code> across a live-session snapshot before any primary restart (which the user must explicitly authorize).</p>
522
+ </div>
523
+ </section>
524
+
525
+ <!-- ============================================================ 8 -->
526
+ <section id="risks">
527
+ <h2><span class="num">08</span>Risks &amp; open questions</h2>
528
+ <div class="grid2">
529
+ <div class="card">
530
+ <h3 style="margin-top:0">Codex has no clean “turn done” event</h3>
531
+ <p>Unlike Claude's <code>Stop</code> hook, Codex turn-end is <em>inferred</em> from an idle-composer repaint. <code>TurnOpen</code> for Codex therefore still leans on a detector + quiet-bound. Open question: is the detector reliable enough to be marked <code>authoritative</code>, or does it stay fallback-tier?</p>
532
+ </div>
533
+ <div class="card">
534
+ <h3 style="margin-top:0">Active-turn close is timeout-based</h3>
535
+ <p>Today a turn closes via the 45-min quiet bound, not a true event. If a hook is lost, <code>TurnOpen</code> could over-hold. Mitigation: reconciliation backstop sweeps stale <code>TurnOpen</code> conditions; keep the quiet bound as a ceiling.</p>
536
+ </div>
537
+ <div class="card">
538
+ <h3 style="margin-top:0">Wall-E permission write is async</h3>
539
+ <p>The coding permission card sets <code>AwaitingInput</code> via an async brain-KV write. There's a window where the condition lags the UI. Open question: emit the condition optimistically on the request, reconcile on persist?</p>
540
+ </div>
541
+ <div class="card">
542
+ <h3 style="margin-top:0">Optimistic-overlay reconciliation timing</h3>
543
+ <p>The escalate-only overlay must expire fast enough not to mask a real idle, but slow enough to bridge the gap to the next server push (≤3s heartbeat). Needs a chosen TTL and a test that a stuck overlay self-heals on the next push.</p>
544
+ </div>
545
+ </div>
546
+
547
+ <hr class="sep">
548
+ <p class="footnote">Decisions locked for this redesign (from the planning session): <strong>deliverable = this doc first</strong> (no production status-code changes until approved) · <strong>architecture = server SSOT + conditions</strong> · <strong>truth source = event-first</strong>, PTY-silence inference as last-resort fallback only.</p>
549
+ <p class="footnote">Source anchors verified 2026-06-18 against the <code>remote-access-smoothing</code> worktree: <code>public/js/session-status-precedence.js</code> · <code>server.js</code> (<code>_standupLiveStatusWithReasonForSession</code> 10976, holds/gates 10612–10718, heartbeat ~30398) · <code>public/index.html</code> (<code>getSessionStatus</code> 37912, Path B 37963).</p>
550
+ </section>
551
+
552
+ </div>
553
+ </body>
554
+ </html>