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
//! Per-session file-change view over the existing hook-event ring buffer (#94).
//!
//! Why: the daemon already records every coding-relevant `FileChanged` event —
//! the filesystem watcher synthesises them in
//! [`crate::daemon::watcher::FileWatcher::record_change`] and posted hooks land
//! through [`crate::daemon::services::hook_service`]. What the GUI and other
//! clients lacked was a stable per-session read surface over that record; the
//! alternative, a second `HashMap<SessionId, Vec<FileChange>>` ledger, would
//! drift from the ring buffer it duplicated. This module adds the view and no
//! new state.
//! What: [`router`] registers `GET /api/v1/sessions/managed/{id}/files`.
//! [`files_core`] resolves the path id, filters the ring buffer to that
//! session's `FileChanged` records, projects each onto a [`FileChangeEntry`],
//! applies the optional `?since=<unix_timestamp>` lower bound, and collapses
//! entries that would render identically. An id no store knows answers 404 with
//! a [`FilesNotFound`] body.
//! Test: `files_tests` — `files_route_returns_only_that_sessions_records`,
//! `files_route_since_excludes_earlier_records`,
//! `files_route_collapses_duplicate_reports`,
//! `files_route_unknown_session_is_a_typed_404`.
use HashSet;
use Arc;
use Router;
use ;
use IntoResponse;
use get;
use ;
use crate;
use crateSessionId;
use crateRouteOutcome;
use crateDaemonState;
use parse_id;
/// Query parameters for `GET /api/v1/sessions/managed/{id}/files`.
///
/// Why: a polling client re-asks for the same session repeatedly and wants only
/// what it has not seen.
/// What: `since` is a Unix timestamp in SECONDS, matching the `timestamp` field
/// each [`FileChangeEntry`] reports. The bound is INCLUSIVE — a caller passing
/// the newest timestamp it holds sees that second again rather than losing an
/// entry recorded later in the same second, and the dedup key below is exactly
/// the tuple that lets it discard the repeat.
/// Test: `files_route_since_excludes_earlier_records`.
/// One file change, as this route reports it.
///
/// Why: the ring buffer holds an opaque hook payload whose shape varies by
/// producer; clients need one stable projection of it.
/// What: `path` and `timestamp` are always present. `operation` carries
/// `payload.operation` when the producer set it and is `null` otherwise — the
/// watcher records a bare path, so a synthesised change reports no operation.
/// `additions`/`deletions` are read from the payload and are likewise `null`
/// when absent; this route never computes diff statistics of its own.
/// `Eq + Hash` is what [`files_core`] deduplicates on: two records that would
/// render as the same entry collapse to one.
/// Test: `files_route_collapses_duplicate_reports`.
/// Response body for `GET /api/v1/sessions/managed/{id}/files`.
///
/// Why: echoing the session and the applied `since` lets a polling client
/// confirm what it actually asked for without tracking request state.
/// What: `files` is ordered oldest-first. `count` is `files.len()`, reported so
/// a caller can check for truncation without walking the array.
/// Test: `files_route_returns_only_that_sessions_records`.
/// The 404 body for a session id no store knows.
///
/// Why: the sibling managed routes answer a miss with a bare string, which a
/// client has to pattern-match. This route is a polling surface, so its refusal
/// is JSON like its success: `error` mirrors the daemon-wide
/// `{"error": …}` shape that [`crate::daemon::error::DaemonError`] emits.
/// Test: `files_route_unknown_session_is_a_typed_404`.
/// Register the route on its own sub-router.
///
/// Why: mirrors `provision_status::router()` and `sync_assets::router()` —
/// the route table for a cohesive surface lives beside the handler rather than
/// in `api.rs`. Merged into the daemon router, so it inherits the router-wide
/// same-origin guard and the daemon's loopback-only bind exactly as the sibling
/// managed routes do; it adds no listener and no guard exemption of its own.
/// Test: every test in `files_tests` drives the real `api::router`.
/// `GET /api/v1/sessions/managed/{id}/files` — this session's file changes.
///
/// Test: `files_tests`.
pub async
/// Project one hook record onto a [`FileChangeEntry`].
///
/// Why: the payload is opaque JSON, and a record whose `path` is missing or
/// empty carries nothing a client could act on.
/// What: returns `None` for such a record so it is skipped rather than reported
/// as a change to the empty path.
/// Test: exercised by every `files_tests` case that posts a `FileChanged` event.
/// Every session-id key space this managed id's `FileChanged` records can sit
/// under, or `None` when no store knows the id.
///
/// Why: a `FileChanged` record is keyed by the [`SessionId`] its producer used,
/// and two producers use two different ids for one managed session. The watcher
/// keys by the daemon-registered session id, which shares the managed id's UUID
/// — the daemon already reads the two as one value (`ManagedSessionId(id.0)` in
/// `daemon::mcp_backend`). A Claude Code session instead announces itself on
/// `SessionStart`, is auto-registered under its OWN UUID, and is bound to the
/// managed record through `claude_session_id` (#1744). Reading only the first
/// key space would return an empty list for exactly the sessions a GUI asks
/// about, so this unions both — the same two-key-space union `delegations_for`
/// already performs for delegations (#4141).
/// What: 404s (returns `None`) only when NEITHER the managed store NOR the
/// daemon session registry knows the id, so "not found" means unknown to the
/// daemon rather than unknown to one of its two stores.
/// Test: `files_route_unknown_session_is_a_typed_404`.
async
/// The body of `GET .../{id}/files`, with no transport attached.
///
/// Why: the managed routes return a [`RouteOutcome`] so one implementation can
/// serve both axum and the Unix socket (#6288); this route follows that shape
/// even though only the HTTP projection is registered today.
/// What: resolves the id, reads the ring buffer for each key space
/// [`key_spaces_for`] reports, keeps `FileChanged` records at or after `since`,
/// collapses entries that would render identically, and sorts oldest-first. The
/// sort is stable, so entries recorded in the same second keep arrival order.
/// Test: `files_tests`.
pub async