1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
//! Claude-Code-style startup banner + backplane probes (STUI-0, DOC-16 §3.1).
//!
//! Why: operators launching `tm session tui` want the same confidence-building
//! startup Claude Code gives — the tool names itself + its version and confirms
//! the trusty backplane (memory + search) is live before the interactive view
//! takes over the terminal. DOC-16 §3.1 (STUI-0) pins that contract: a banner
//! line `trusty-mpm sessions v<CARGO_PKG_VERSION>`, a memory line (probed against
//! the fixed user-level `user` palace, decision D4), a search line (plain health
//! probe), and a startup line with the active-session count + a `/help` hint. A
//! probe failure renders `○ unreachable` and never blocks the TUI (§3.1 error
//! conditions).
//! What: the pure [`compose_banner`] (probe results + count → the exact banner
//! text, unit-testable offline), the [`ProbeOutcome`] result type, the async
//! [`probe_memory`] / [`probe_search`] probes (short-timeout, fail-safe), and
//! [`print_startup_banner`] which runs the probes and prints the banner to
//! stderr (stdout stays clean) before the alternate screen is entered.
//! Test: `super::tests` covers the pure composition (version, glyphs, palace
//! note, count + `/help`) for active/unreachable/zero-count permutations; the
//! async probes degrade to `unreachable` against a dead daemon and are exercised
//! by `probe_unreachable_*`.
use Duration;
use crateDEFAULT_SEARCH_URL;
/// Per-probe HTTP timeout for the startup backplane checks.
///
/// Why: a down (or slow) memory/search daemon must not stall the operator's
/// startup; a short timeout turns an unresponsive service into a clean
/// `○ unreachable` line rather than a multi-second hang before the TUI opens.
/// What: two seconds — comfortably above a healthy local round-trip yet short
/// enough to keep startup snappy when a service is down.
/// Test: timeout value is side-effect-only; the unreachable path is covered by
/// `probe_unreachable_memory_is_inactive` / `probe_unreachable_search_is_inactive`.
const PROBE_TIMEOUT: Duration = from_secs;
/// The fixed user-level palace the banner confirms memory against (D4).
///
/// Why: DOC-16 decision D4 fixes memory confirmation to a single user-scoped
/// palace named `user` (shared across projects, distinct from the per-project
/// and `session-manager` palaces). Naming it once here single-sources the id
/// between the probe and its rendered note.
/// What: the literal palace id `user`.
/// Test: `compose_banner_active_shows_palace_note` asserts the rendered note
/// carries this id.
pub const USER_PALACE: &str = "user";
/// Glyph shown for an active (reachable + confirmed) backplane service.
///
/// Why: §3.1 pins `●` for active so the operator reads "live" at a glance; a
/// constant keeps the glyph single-sourced between the renderer and its tests.
/// What: the literal `●`.
/// Test: `compose_banner_active_shows_glyphs`.
pub const GLYPH_ACTIVE: char = '●';
/// Glyph shown for an unreachable backplane service.
///
/// Why: the counterpart to [`GLYPH_ACTIVE`] — §3.1 pins `○` for unreachable so
/// a degraded backplane reads distinctly without aborting startup.
/// What: the literal `○`.
/// Test: `compose_banner_unreachable_shows_glyphs`.
pub const GLYPH_UNREACHABLE: char = '○';
/// The `/help` hint appended to the startup line.
///
/// Why: §3.1 requires the startup line to point operators at `/help`; naming
/// the literal here keeps the hint single-sourced.
/// What: the literal `type /help for commands`.
/// Test: `compose_banner_appends_help_hint`.
pub const HELP_HINT: &str = "type /help for commands";
/// The outcome of probing one backplane service for the startup banner.
///
/// Why: the pure [`compose_banner`] must render memory/search lines without
/// performing any IO, so the async probes collapse their (network-dependent)
/// result into this small value the composer consumes. Carrying an optional
/// `note` lets the memory probe surface the `user`-palace detail (or the reason
/// it is degraded) alongside the active/unreachable verdict (D4).
/// What: `active` (reachable + any service-specific confirmation passed) plus an
/// optional parenthetical `note` (e.g. `palace: user`).
/// Test: `compose_banner_*` construct these directly to drive the composer
/// offline.
/// Compose the full startup banner text from probe results and a session count.
///
/// Why: STUI-0's banner shape (§3.1) is a behavior contract that must be
/// verifiable WITHOUT a terminal or live daemons; keeping composition pure
/// (probe outcomes + count to string) makes the exact layout — a version line,
/// memory/search glyph lines with optional parentheticals, and an active-count
/// plus `/help` startup line — unit-testable offline. The version is the crate
/// `CARGO_PKG_VERSION` resolved at the call site so this stays a pure function.
/// What: returns a multi-line string of the form:
/// ```text
/// trusty-mpm sessions v<version>
/// memory <glyph> <word> (<note>)
/// search <glyph> <word>
/// <count> active sessions · type /help for commands
/// ```
/// The memory/search parentheticals are emitted only when the outcome carries a
/// `note`. The count line uses the singular `session` when `active_count == 1`
/// and renders a daemon-down marker when `active_count` is `None`.
/// Test: `compose_banner_active_shows_glyphs`,
/// `compose_banner_unreachable_shows_glyphs`,
/// `compose_banner_active_shows_palace_note`, `compose_banner_appends_help_hint`,
/// `compose_banner_pluralizes_count`, `compose_banner_handles_unknown_count`.
/// Build one backplane service line: ` <label> <glyph> <word> (<note>)`.
///
/// Why: the memory and search lines share the same shape (only the label and the
/// optional note differ); a helper keeps the two-space indent, the padded label,
/// and the optional parenthetical consistent.
/// What: returns ` <label padded to 6> <glyph> <word>` plus ` (<note>)` when
/// the outcome carries a note, terminated by a newline.
/// Test: covered by the `compose_banner_*` tests via [`compose_banner`].
/// Build the startup line: `<count> active sessions · type /help for commands`.
///
/// Why: §3.1 requires the active-session count plus a `/help` hint; an unknown
/// count (daemon unreachable at startup) degrades to a marker rather than a
/// misleading `0`. A pure helper keeps the pluralisation and the unknown-count
/// branch testable offline.
/// What: renders `<n> active session(s)` (singular at `1`) when the count is
/// known, or `daemon unreachable` when `None`, then ` · <HELP_HINT>`, and
/// terminates with a single trailing newline so the composed banner ends cleanly
/// (the print site adds exactly one blank separator, never two).
/// Test: `compose_banner_pluralizes_count`, `compose_banner_handles_unknown_count`,
/// `compose_banner_has_single_trailing_newline`.
/// Probe trusty-search health for the startup banner (fail-safe).
///
/// Why: §3.1 requires search to be confirmed via its plain health probe; a
/// failure must degrade to `○ unreachable` (the TUI still opens), never abort.
/// What: GETs `<base>/health` with a short timeout; any transport or non-2xx
/// response yields [`ProbeOutcome::unreachable`]. `base` defaults to
/// [`DEFAULT_SEARCH_URL`] when `None`. Builds a single bounded client and reuses
/// it for the health check.
/// Test: `probe_unreachable_search_is_inactive` drives the dead-daemon branch;
/// the live path is exercised by launching the TUI against a running daemon.
pub async
/// Probe trusty-memory health + the fixed `user` palace for the banner (D4).
///
/// Why: §3.1 + decision D4 make memory "active" only when (a) the health probe
/// succeeds AND (b) the `user` palace exists — created idempotently on first run
/// via `POST /api/v1/palaces`. The probe is fail-safe: a down daemon or a failed
/// palace assertion degrades to `○ unreachable` with a reason note, and the TUI
/// still opens.
/// What: GETs `<base>/health`; on success, GETs `<base>/api/v1/palaces/user` and,
/// only on a real 404, POSTs `<base>/api/v1/palaces` with `{"name":"user"}` to
/// create it. Builds a single bounded client and threads it into both helpers.
/// Returns [`ProbeOutcome::active`] with note `palace: user` when memory is live
/// and the palace is present/creatable, else [`ProbeOutcome::unreachable`] with a
/// short reason. `base` defaults to the discovered trusty-memory address
/// (issue #2030: `TRUSTY_MEMORY_URL` override, else the daemon's actual
/// discovered bound address — never a hardcoded port) when `None`.
/// Test: `probe_unreachable_memory_is_inactive` drives the dead-daemon branch;
/// the live + palace-assertion path is exercised against a running daemon.
pub async
/// Build the short-timeout HTTP client shared by the startup probes.
///
/// Why: every probe needs the same bounded client so a hung service cannot stall
/// startup; centralising construction keeps the timeout single-sourced.
/// What: a `reqwest::Client` with [`PROBE_TIMEOUT`], falling back to the default
/// client if the builder somehow fails (it cannot in practice).
/// Test: covered indirectly by the probe tests.
pub
/// GET `<base>/health` with the shared client and report whether it answered 2xx.
///
/// Why: both probes share the same health check; a daemon that is down, slow, or
/// returns an error status all collapse to "not healthy" so the banner degrades
/// cleanly. Taking the client by reference lets each probe build exactly one
/// client and reuse it across the health check and any follow-up request.
/// What: issues a bounded GET to `<base>/health` via `client` and returns `true`
/// only on a 2xx response.
/// Test: covered by `probe_unreachable_*` (dead daemon → `false`).
async
/// Ensure the fixed `user` palace exists at `base`, creating it ONLY on a 404 (D4).
///
/// Why: D4 makes memory "active" only when the `user` palace is present; the
/// banner asserts it idempotently so first-run operators get a confirmed memory
/// line without a manual provisioning step. Crucially, creation must be gated on
/// a *real* 404 — a transient `500`/`503` (or any other non-2xx) means the
/// daemon's state is unknown, so blindly POSTing a create would be wrong (it
/// could race a half-up daemon or mask a real outage). Those cases degrade to
/// `false` (memory renders `○ unreachable`) instead of attempting a write.
/// What: GETs `<base>/api/v1/palaces/user` with the shared `client` and branches
/// on the status: a 2xx means the palace already exists → `true`; a 404 means it
/// is genuinely absent → POST `<base>/api/v1/palaces` with `{"name":"user"}` and
/// return whether the create answered 2xx; any other status (5xx, etc.) →
/// `false` without creating; a transport error → `false`.
/// Test: `ensure_user_palace_creates_only_on_404` exercises the status branching
/// against a stub server; the live assertion path runs against a running daemon.
pub async
/// Run the backplane probes and print the startup banner to stderr.
///
/// Why: §3.1 requires the banner to render before the interactive view opens.
/// Printing to stderr (not stdout) follows the workspace convention that stdout
/// stays clean for any downstream framing, and printing BEFORE the alternate
/// screen is entered (mirroring Claude Code) means the banner stays visible in
/// the operator's scrollback after the TUI exits. Probes run concurrently and
/// are fail-safe, so a down service slows startup by at most one [`PROBE_TIMEOUT`]
/// and never aborts.
/// What: probes memory + search concurrently, composes the banner with the
/// crate `CARGO_PKG_VERSION` and `active_count`, and writes it to stderr. The
/// composed banner ends with a single trailing newline (from `startup_line`), so
/// `eprintln!` adds exactly one blank separator after it — not two. `memory_url`
/// / `search_url` default to the canonical local addresses when `None`.
/// Test: composition is unit-tested via [`compose_banner`]; this print-only glue
/// is exercised by launching the TUI.
pub async