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
//! HTTP handlers for `tm sessions sync-assets` (issue #2444).
//!
//! Why: `#2002`'s agent/skill/output-style deployment is one-shot at session
//! launch — a long-lived managed session never re-syncs when the bundled or
//! catalog source changes underneath it. These two routes give the operator (or
//! the PM, via the CLI) a way to force a re-deploy against a LIVE session's
//! workspace without a full relaunch: one per-session, one fleet-wide for
//! `--all`. They live in their own file so `mod.rs` stays under the 500-SLOC
//! production cap, mirroring the `prune.rs`/`reactivate.rs` split precedent.
//! What: [`sync_session_assets_route`] (POST …/managed/{id}/sync-assets) and
//! [`sync_all_session_assets_route`] (POST …/managed/sync-assets) delegate to
//! [`crate::core::session_launch::sync_session_assets`] — the SAME roster+style
//! deployers `prepare_session` calls at launch — via [`sync_one`], run on the
//! blocking pool since the deploy does synchronous filesystem I/O. BOTH routes
//! gate on [`syncable`] (`Active`/`Stopped`/`Errored` only) before touching the
//! filesystem — the per-session route added this gate after a code-critic HIGH
//! finding on the initial PR: a `Decommissioned` tombstone clears
//! `workspace_path` but not `cwd`, so an ungated sync would silently recreate
//! an already-deleted workspace directory.
//! Test: `sync_route_redeploys_stale_agent`, `sync_route_missing_session_404`,
//! `sync_route_decommissioned_session_blocked_no_fs_write`,
//! `sync_all_route_skips_provisioning_and_decommissioned` in
//! `sync_assets_tests.rs`.
use Arc;
use ;
use Serialize;
use parse_id;
use crateFrameworkPaths;
use crateRouteOutcome;
use crateDaemonState;
use crate;
/// Build the sync-assets sub-router (both the per-session and fleet-wide
/// routes), merged into the main daemon router by `api.rs`.
///
/// Why: keeps both `/api/v1/sessions/managed/{id}/sync-assets` and the
/// literal `/api/v1/sessions/managed/sync-assets` fleet route registered
/// together with their handlers, mirroring the `provision_status::router()`
/// merge precedent — `api.rs` gains one `.merge(...)` call instead of two
/// inline `.route(...)` registrations.
/// What: a bare `Router<Arc<DaemonState>>` with both POST routes; `api.rs`
/// merges it into the shared router (state is injected once, at that merge
/// point).
/// Test: covered end-to-end via the CLI's `session_sync_assets_not_found_errors`
/// (`bin/tm/commands/sync_assets_tests.rs`, hits this router over real HTTP)
/// and the route-level tests in `sync_assets_tests.rs`.
/// Response body for both sync-assets routes' per-session result.
///
/// Why: the operator needs to see exactly what changed (or that nothing did,
/// because everything was already current) rather than a bare success flag.
/// What: mirrors [`crate::core::session_launch::SyncAssetsReport`]'s fields
/// plus the session `id` (needed on the fleet-wide response to attribute each
/// result to its session).
/// Test: `sync_route_redeploys_stale_agent`.
/// Response body for POST /api/v1/sessions/managed/sync-assets (`--all`).
///
/// Why: a fleet-wide run must report per-session outcomes rather than a single
/// aggregate — some sessions may be skipped (no live workspace to sync) and
/// some may individually fail without aborting the rest of the fleet.
/// What: `synced` (one [`SyncAssetsResponse`] per session actually re-deployed),
/// `skipped` (session ids not in a syncable state — `provisioning`/
/// `decommissioned`), and `errors` (`"<id>: <message>"` for any session whose
/// redeploy failed; every other session still ran).
/// Test: `sync_all_route_skips_provisioning_and_decommissioned`.
/// Whether a session's state has a live workspace worth re-syncing.
///
/// Why: shared by both routes below so the per-session and fleet-wide paths
/// can never disagree on which sessions `sync-assets` applies to — the same
/// state set `summary.rs`'s private `staleness_meaningful_for` gates the
/// `stale_assets` marker on (a `Provisioning` session has not deployed yet,
/// and a `Decommissioned` one has no workspace left) — so "we can tell you
/// it's stale" and "we can fix it" can never disagree. Note the FLEET LIST
/// path additionally skips `Stopped` for latency (#4322,
/// `summary::probe_staleness_in_list`); that is a display-side decision only
/// and deliberately does NOT narrow what `sync-assets` will act on.
/// What: `true` for `Active`, `Stopped`, and `Errored`.
/// Test: `sync_all_route_skips_provisioning_and_decommissioned`.
/// Run [`crate::core::session_launch::sync_session_assets`] for one session on
/// the blocking pool.
///
/// Why: the redeploy does synchronous filesystem I/O (directory walks, file
/// reads/writes for the agent/skill/output-style deployers) — running it
/// directly on the async executor thread would block it. Resolving the
/// workspace-scoped [`FrameworkPaths`] here (rather than in the caller) keeps
/// both route handlers below identical one-liners.
/// What: resolves `record`'s workdir via
/// [`crate::core::session_assets::session_workdir`], builds
/// `FrameworkPaths::for_managed_workspace(workdir)`, and delegates.
/// Test: `sync_route_redeploys_stale_agent`.
async
/// POST /api/v1/sessions/managed/{id}/sync-assets — re-deploy one session's
/// assets against the current catalog (issue #2444).
///
/// Why: the concrete fix for a single stale session flagged by `stale_assets`
/// on `tm sessions ls` — re-run the SAME deployers launch uses, without a full
/// relaunch or touching CLAUDE.md/hooks/MCP config. MUST apply the SAME
/// [`syncable`] state gate the fleet-wide route already enforces (code-critic
/// HIGH finding on the initial PR): a `Provisioning` session has not deployed
/// yet, and — critically — a `Decommissioned` tombstone clears
/// `record.workspace_path` but NEVER `record.cwd`, so
/// [`crate::core::session_assets::session_workdir`]'s cwd fallback would
/// resolve to the ALREADY-REMOVED workspace directory; running the deployers
/// against it (`create_dir_all` et al.) would silently RECREATE a directory
/// `decommission` deleted on disk, with none of that workspace's git/session
/// state. Gating here closes that hole before `sync_one` ever touches the
/// filesystem.
/// What: looks up the session (404 if unknown), rejects a non-[`syncable`]
/// state with 409 (mirroring the `InvalidState` → 409 convention every other
/// managed route uses for "session exists but is in the wrong lifecycle state
/// for this operation" — see `delete_managed_session`/`resume_error.rs`),
/// then delegates to [`sync_one`]. For a syncable session this applies
/// regardless of its `stale_assets` flag — running it on an already-current
/// workspace is a safe, idempotent no-op (every deployer here is the SAME
/// manifest-checksum-driven machinery `prepare_session` uses).
/// Test: `sync_route_redeploys_stale_agent`, `sync_route_missing_session_404`,
/// `sync_route_decommissioned_session_blocked_no_fs_write` in
/// `sync_assets_tests.rs`.
pub async
/// The transport-neutral body of `POST .../{id}/sync-assets` (#6288), served
/// over the socket as `mpm.managed.sync_assets`.
///
/// Test: `managed_sync_assets_parity` in `daemon::rpc::managed_tests`.
pub async
/// POST /api/v1/sessions/managed/sync-assets — re-deploy EVERY syncable
/// session's assets (`tm sessions sync-assets --all`, issue #2444).
///
/// Why: an operator refreshing a catalog/bundled skill or agent (a `tm
/// install`, or a fresh `tm` binary) wants to push it to every live session in
/// one call rather than iterating ids by hand.
/// What: lists every managed session, skips one whose state has no live
/// workspace ([`syncable`] — `provisioning`/`decommissioned`), and runs
/// [`sync_one`] for the rest SEQUENTIALLY (this is an infrequent,
/// operator-triggered admin action across an independent per-session
/// filesystem tree each — concurrency would only add complexity, not
/// meaningfully reduce latency for a typical fleet size). A single session's
/// redeploy failure is recorded in `errors` and does NOT abort the rest of the
/// fleet run.
/// Test: `sync_all_route_skips_provisioning_and_decommissioned`.
pub async
/// The transport-neutral body of `POST .../managed/sync-assets` (#6288), served
/// over the socket as `mpm.managed.sync_assets_all`.
///
/// Test: `managed_sync_assets_all_parity` in `daemon::rpc::managed_tests`.
pub async