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
//! Logs tail, admin stop, and fire-and-forget remember handlers.
//!
//! Why: Operational endpoints for debugging, shutdown, and agent-accessible
//! memory writes that do not require MCP. These are grouped together because
//! they all support daemon administration rather than palace data operations.
//! What: `GET /api/v1/logs/tail`, `POST /api/v1/admin/stop`, and
//! `POST /api/v1/remember` handlers.
//! Test: `logs_tail_*`, `admin_stop_returns_ok`, and `remember_async_*` tests
//! in `web::tests`.
use ;
use Deserialize;
use ;
use crateAppState;
use ApiError;
/// Default number of log lines returned by `GET /api/v1/logs/tail` when `n`
/// is absent. 100 lines is enough context for a glance without a huge payload.
const DEFAULT_LOGS_TAIL_N: usize = 100;
/// Hard ceiling on `GET /api/v1/logs/tail?n=` — equal to the ring-buffer
/// capacity, so a request can never ask for more lines than the buffer holds.
const MAX_LOGS_TAIL_N: usize = DEFAULT_LOG_CAPACITY;
/// Query parameters for `GET /api/v1/logs/tail`.
///
/// Why (issue #35): callers ask for a bounded number of recent log lines;
/// `n` defaults to a useful page size and is clamped server-side so a
/// misconfigured client cannot request more lines than the buffer holds.
/// What: `n` is optional; absent → [`DEFAULT_LOGS_TAIL_N`]. Clamped to
/// `[1, MAX_LOGS_TAIL_N]` in the handler.
/// Test: `logs_tail_clamps_n` exercises the clamp.
pub
/// `GET /api/v1/logs/tail?n=200` — return the most recent N tracing log lines.
///
/// Why (issue #35): operators debugging a running daemon want recent logs
/// over HTTP without SSHing to the box or restarting with a different
/// `RUST_LOG`. The in-memory ring buffer (fed by the `LogBufferLayer` wired
/// into the subscriber at startup) makes this near-free.
/// What: clamps `n` to `[1, MAX_LOGS_TAIL_N]`, drains the tail of
/// `state.log_buffer`, and returns `{ "lines": [...], "total": <buffered> }`
/// where `total` is the number of lines currently buffered (so callers can
/// tell whether the ring has wrapped).
/// Test: `logs_tail_returns_recent_lines` and `logs_tail_clamps_n`.
pub async
/// `POST /api/v1/admin/stop` — request a graceful shutdown of the daemon.
///
/// Why (issue #35): the admin UI and operators want a one-call way to stop
/// the daemon without resolving its PID and sending a signal. The daemon is
/// localhost-only and trusts every caller, so no auth is required.
/// Why (issue #1100): the original implementation spawned a detached task
/// that called `std::process::exit(0)` after 200 ms, relying on the race
/// between the sleep and the test's return to avoid killing the test process.
/// On loaded CI that race is lost. The exit call is now compiled out under
/// `#[cfg(not(test))]`; tests exercise only the JSON response shape without
/// risk of terminating the test process.
/// What: In production, spawns a detached task that sleeps 200 ms (giving
/// this HTTP response time to flush to the client) and then calls
/// `std::process::exit(0)`. In `#[cfg(test)]` builds the spawn is replaced
/// with a no-op so the test process stays alive. Returns
/// `{ "ok": true, "message": "shutting down" }` immediately in both cases.
/// Test: `admin_stop_returns_ok` asserts the response shape without any
/// timing dependency; `admin_stop_does_not_exit_in_test` asserts the test
/// path does not call `process::exit`.
pub async
// ---------------------------------------------------------------------------
// Fire-and-forget memory save (`POST /api/v1/remember`)
// ---------------------------------------------------------------------------
/// Request body for `POST /api/v1/remember`.
///
/// Why: agents spawned via Claude Code's Agent tool do not inherit any MCP
/// connections, so the `memory_remember` MCP tool is unreachable to them.
/// Exposing a plain HTTP entry point lets those agents shell out via the
/// `trusty-memory note` CLI (or any `curl` call) without learning MCP.
/// What: `content` is the drawer body and is required; `palace` falls back
/// to the daemon's `--palace` default when omitted; `tags` is optional and
/// passed through verbatim to the underlying handler.
/// Test: `remember_async_*` tests in this module.
pub
/// Minimum word count for content accepted by `POST /api/v1/remember`.
///
/// Why (issue #466): the fire-and-forget endpoint returns `202 Accepted`
/// immediately and dispatches the write on a detached task. Any content that
/// the background worker would reject (e.g. too few tokens) caused silent data
/// loss — the caller believed the memory was stored when it wasn't. Validating
/// the minimum synchronously turns silent drops into explicit `422` rejections
/// so callers know immediately that their content was not queued.
/// What: mirrors `tools::CONTENT_GATE_MIN_WORDS` (4 words) — the same gate
/// `handle_memory_remember` applies via `content_gate` in the background task.
/// Test: `remember_async_rejects_short_content`.
const REMEMBER_MIN_WORDS: usize = 4;
/// `POST /api/v1/remember` — fire-and-forget memory save.
///
/// Why: sub-agents spawned via Claude Code's Agent tool have no MCP
/// connection (MCP servers are not inherited across sub-agent spawns), so
/// they cannot invoke `mcp__trusty-memory__memory_remember` directly. They
/// can, however, run shell commands — this endpoint plus the
/// `trusty-memory note` CLI gives them a writable handle that needs no
/// MCP plumbing. The contract is one-way: the request is parsed, validated,
/// and queued on a `tokio::spawn`, then `202 Accepted` is returned
/// immediately. Failures during the spawned dispatch (palace not found,
/// content gate skip, redb error) are logged at `warn` but never propagate
/// back to the caller because the agent has already exited by then.
/// Issue #466: synchronous validation of obvious rejections (empty content,
/// fewer than [`REMEMBER_MIN_WORDS`] whitespace-delimited words) now returns
/// `422 Unprocessable Entity` before queuing so callers receive a clear error
/// instead of a false `202`. Content that passes the synchronous checks may
/// still be dropped by the background worker's fuller filter set (blocklist,
/// dedup, MCP-level token threshold), but those are less predictable from
/// the HTTP surface.
/// What: deserialises the body, rejects empty content (400) and sub-threshold
/// word count (422), then maps `{content, palace, tags}` → `{text, palace,
/// tags}` (the field names `handle_memory_remember` expects) and dispatches
/// `memory_remember` from a detached task. Returns `202 Accepted` with
/// `{"status":"queued"}`.
/// Test: `remember_async_returns_202_and_persists` (happy path),
/// `remember_async_rejects_empty_content` (400 input validation), and
/// `remember_async_rejects_short_content` (422 for sub-word-count content).
pub async