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
//! 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`] socket probes (short-timeout,
//! fail-safe; search moved off HTTP in #9214), 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 Path;
use Duration;
use crate;
use crateresolve_search_socket_or_unreachable;
/// Per-probe socket 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: calls `search.health` on the daemon's socket with a short timeout;
/// a dead socket or a refusal yields [`ProbeOutcome::unreachable`]. `socket`
/// defaults to the resolved trusty-search socket when `None` (#9214).
/// Test: `probe_search_over_the_socket_is_active`,
/// `probe_search_on_a_refusing_daemon_is_inactive`,
/// `probe_unreachable_search_is_inactive`.
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 `palace_create`. 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: calls `memory.health` on the socket; on success, calls
/// `memory.palace_get` for the `user` palace and, only on a genuine not-found,
/// `palace_create`s it. Returns [`ProbeOutcome::active`] with note
/// `palace: user` when memory is live and the palace is present/creatable, else
/// [`ProbeOutcome::unreachable`] with a short reason. `socket` defaults to the
/// derived trusty-memory path (#6286 — there is no discovered address and no
/// 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
/// Does the trusty-memory daemon answer at all?
async
/// Ensure the fixed `user` palace exists, creating it ONLY when the daemon says
/// it is absent (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 genuine not-found — any other refusal means the daemon's state is unknown,
/// so blindly creating 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. #6286 reads that distinction off
/// `MemoryRpcError::is_not_found` where it used to read a 404 status.
/// What: `memory.palace_get`; success means it exists → `true`; a not-found
/// refusal means it is genuinely absent → `palace_create` and return whether
/// that succeeded; any other error → `false` without creating.
/// Test: `ensure_user_palace_creates_only_on_not_found`.
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_socket`
/// / `search_socket` default to the resolved daemon sockets when `None`.
/// Test: composition is unit-tested via [`compose_banner`]; this print-only glue
/// is exercised by launching the TUI.
pub async