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
//! Message send/receive/mark-read operations.
//!
//! Why: Isolates the async I/O operations from the type definitions so each
//! file stays under the 500-SLOC cap and the async functions are grouped by
//! concern.
//! What: `send_message_to_palace`, `list_unread_messages`, `list_messages`,
//! `mark_message_read`, and the `cwd_palace_slug` / `cwd_palace_slug_at`
//! helper that resolves the current project's palace slug.
//! Test: `round_trip_send_and_inbox`, `mark_read_is_atomic_under_concurrency`,
//! `mark_read_is_idempotent`.
use ;
use Path;
use Arc;
use ;
use RememberOptions;
use PalaceHandle;
use Uuid;
use ;
/// Persist a message into the recipient palace.
///
/// Why: every send entry point (MCP, CLI, HTTP) needs the same write path:
/// build tags + drawer, call `remember_with_options(force=true)` (we
/// bypass the signal/noise filter because short notifications like "ping"
/// are legitimately short messages), return the new drawer id. Centralising
/// it keeps the three surfaces in lock-step.
/// What: opens a handle to the recipient palace under `data_root`, writes
/// the drawer with the message envelope tags plus the supplied creator
/// attribution tags, and returns the new drawer id. The recipient palace
/// must already exist — sending to a non-existent palace fails fast with
/// a clear error rather than silently creating an empty inbox. `creator`
/// is the writer's identity (HTTP / MCP / CLI / hook) — passed by every
/// caller so noise drawers can be traced back to their origin.
/// Test: `round_trip_send_and_inbox`.
pub async
/// List every unread message drawer in `palace`.
///
/// Why: the SessionStart hook needs to emit every unread message before
/// marking them read. Filtering happens client-side (against
/// `list_drawers`) because the message marker tag is namespaced — the
/// existing tag filter accepts a single string and we filter on the
/// composite `msg:v1` + `msg:read=false` predicate.
/// What: pulls every drawer carrying [`MSG_MARKER_TAG`], decodes the
/// envelope via [`Message::from_drawer`], and returns the ones with
/// `read == false`. Sorted oldest-first by `sent_at` so multi-message
/// inboxes deliver in a natural reading order.
/// Test: `round_trip_send_and_inbox`.
/// List every message drawer in `palace`, optionally filtering to unread.
///
/// Why: the HTTP `GET /api/v1/messages` endpoint exposes both modes — full
/// audit history and the unread-only view used by debuggers.
/// What: same as `list_unread_messages` but with an opt-in `unread_only`
/// filter; sorted by `sent_at` ascending in both cases.
/// Test: `round_trip_send_and_inbox` and `inbox_returns_only_unread_after_mark`.
/// Mark a message drawer as read by atomically rewriting its `msg:read=...`
/// tag.
///
/// Why: the SessionStart hook MUST flip the read flag exactly once per
/// message, even when two terminals race to start a session against the
/// same palace. The naive "forget + remember" approach is not atomic
/// (both racers can forget, then both can re-insert, producing two
/// drawers). The single source of truth for "have we flipped this flag
/// yet" is the in-memory drawer table — a `parking_lot::RwLock<Vec<Drawer>>`
/// guarded by the palace handle. We take the write lock, do the
/// compare-and-swap (return `false` if already read; otherwise rewrite
/// the tag and clone the post-mutation drawer), then release the lock
/// before crossing the `await` boundary for the persistent write.
/// What: returns `Ok(false)` if the drawer is missing or already
/// `msg:read=true`. Otherwise rewrites the tag in place under the write
/// lock, clones the updated drawer, releases the lock, persists via
/// `handle.kg.upsert_drawer`, and returns `Ok(true)`. The persistent
/// write is async (it routes through the per-palace `KgWriter` actor for
/// coalescing) so we cannot hold the parking_lot lock across it — but we
/// don't need to: the in-memory CAS is the single source of truth for
/// "have we flipped this flag", and the persistent write is just durable
/// backing.
/// Test: `mark_read_is_atomic_under_concurrency`,
/// `mark_read_is_idempotent`.
pub async
/// Resolve the calling project's palace slug from cwd, preferring the
/// git toplevel when available.
///
/// Why: the SessionStart hook runs with whatever cwd Claude Code launches
/// it under. Using the git toplevel makes `slug` stable regardless of
/// which subdirectory the user opened — `cd /repo/crates/foo && trusty-memory
/// inbox-check` and `cd /repo && trusty-memory inbox-check` resolve to the
/// same slug.
/// What: runs `git rev-parse --show-toplevel` from `cwd` (best-effort, no
/// network); on success slugifies the basename of the returned path. On
/// failure (not a repo, no git on PATH, command timeout) falls back to
/// slugifying `cwd` itself.
/// Test: `tests::cwd_palace_slug_uses_git_toplevel`,
/// `tests::cwd_palace_slug_falls_back_to_basename`.
/// Variant of [`cwd_palace_slug`] that takes the working directory explicitly.
///
/// Why: lets unit tests drive the function without mutating the process' real
/// cwd (which races with concurrent tests). Also used by the `prompt-context`
/// hook, which must NOT trigger the lazy pin-file write (a read-only context
/// may not have write permission and the hook's stdout must stay clean for the
/// injection protocol). The pin-file read path therefore always uses the
/// non-writing variant (`project_slug_at_readonly`).
///
/// Resolution order (issue #1217 extends the pre-existing pin-file primacy with
/// project-identity derivation):
///
/// 1. `TRUSTY_MEMORY_PALACE` env override, slugified — wins unconditionally.
/// 2. If `.trusty-tools/trusty-memory.yaml` exists anywhere above `start`,
/// return its `palace` field — the canonical, rename-stable slug. This is
/// what keeps existing palaces from being orphaned by the new derivation: a
/// committed pin always wins, so a project already pinned to `trusty-tools`
/// stays `trusty-tools`.
/// 3. Otherwise derive from project identity via
/// [`crate::palace_id_derive::derive_palace_id`]: the git `owner/repo` slug
/// from `remote.origin.url` (`bobmatnyc/trusty-tools` →
/// `bobmatnyc-trusty-tools`), else the `parent/dir` slug of the git toplevel
/// (or `start` when not in a repo), e.g. `Projects/trusty-tools` →
/// `projects-trusty-tools`.
///
/// All git calls are best-effort (no network, short-lived); a corrupt pin file
/// at step 2 is logged to stderr and falls through to step 3 — it never emits
/// to stdout and never panics.
/// What: returns `Ok(slug)` or an error when no slug can be derived (empty
/// cwd basename in a non-git context with no override).
/// Test: `tests::cwd_palace_slug_uses_git_toplevel`,
/// `tests::cwd_palace_slug_falls_back_to_parent_dir`,
/// `tests::cwd_palace_slug_at_prefers_pin_file`,
/// `tests::cwd_palace_slug_at_reads_pin_from_subdir`,
/// `tests::cwd_palace_slug_at_pin_read_does_not_create_pin_file`,
/// `tests::cwd_palace_slug_at_env_override_wins`,
/// `tests::cwd_palace_slug_at_uses_git_owner_repo`.
/// Resolve the git working-tree root for `start` via `git rev-parse`.
///
/// Why: the `parent/dir` fallback must describe the *project* root, not the
/// arbitrary subdirectory a hook was launched from. Resolving the toplevel
/// first makes `cd repo/crates/foo` and `cd repo` derive the same slug.
/// What: runs `git rev-parse --show-toplevel` in `start`; returns the trimmed
/// path on success, `None` when git is absent, `start` is not in a repo, or the
/// command fails. Best-effort and side-effect-free (no network).
/// Test: covered via `cwd_palace_slug_at_uses_git_owner_repo` and the existing
/// `cwd_palace_slug_uses_git_toplevel`.
/// Read `remote.origin.url` for the repo containing `start`.
///
/// Why: the primary identity source for the default palace is the GitHub-style
/// `owner/repo` path, which lives in the origin remote URL. Shelling out to
/// `git config` (rather than reading `.git/config` directly) transparently
/// handles worktrees, where `.git` is a file pointing at the parent repo.
/// What: runs `git -C <start> config --get remote.origin.url`; returns the
/// trimmed URL on success, `None` when there is no origin remote, git is
/// absent, or `start` is not in a repo. Best-effort, no network.
/// Test: covered via `cwd_palace_slug_at_uses_git_owner_repo`.