car-server-core 0.52.1

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
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
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
//! Zero-install registration of the flagship assistant into CarHost.
//!
//! Seeds one [`AgentSpec`] into `~/.car/agents.json` whose command is the `car`
//! binary invoked as `car do --serve`, so the assistant shows up in CarHost's
//! Agents list and auto-starts on daemon boot — nothing for the user to install.
//! Idempotent and non-destructive: if an entry with this id already exists we
//! leave it untouched (respecting any operator change, e.g. a disabled
//! `auto_start`).
//!
//! Two entry points, because the manifest is lock-guarded by whoever owns it:
//! - [`ensure_registered_in`] registers into an already-held [`Supervisor`] —
//!   the daemon calls this at boot with the sibling `car` binary path.
//! - [`ensure_registered`] opens the user-default supervisor itself — the `car`
//!   CLI calls this when no daemon owns the manifest.
//!
//! ## Agent id: `parslee-core`, with `car-assistant` as a compatibility alias
//!
//! [`ASSISTANT_AGENT_ID`] used to be `car-assistant`, and macOS
//! (`ChatTabView.swift`) still hardcodes that. Both mobile hosts (iOS, Android)
//! were built against `parslee-core` as the canonical id — matching the product
//! name — and only alias `car-assistant` client-side for *display*, never on
//! the wire: `agents.chat {agent_id:"parslee-core"}` against an unpatched
//! daemon returned `-32603 "agent 'parslee-core' is not attached"` (car#1107).
//!
//! The fix moves the canonical registration id to `parslee-core` — the spawned
//! `car do --serve` process picks it up via `CAR_AGENT_ID`
//! (`car-registry/src/supervisor.rs`) and attaches under it — while
//! [`is_assistant_alias`] lets the daemon's `agents.chat` dispatch (see
//! `car-server-core::handler::handle_agents_chat`) resolve a request for
//! either spelling to whichever one is actually attached. That keeps macOS,
//! any already-registered install, and raw WS integrators still targeting
//! `car-assistant` working with no client-side change required.
//! [`ensure_registered_in`] additionally migrates an existing `car-assistant`
//! `agents.json` entry to the new id in place, so an upgrade doesn't leave an
//! orphaned duplicate registration behind.

use car_registry::supervisor::{AgentSpec, RestartPolicy, Supervisor};
use std::collections::BTreeMap;
use std::path::Path;

/// The reserved agent id for the built-in assistant. Canonical since car#1107
/// (previously `car-assistant` — see [`LEGACY_ASSISTANT_AGENT_ID`]).
pub const ASSISTANT_AGENT_ID: &str = "parslee-core";

/// The id the flagship assistant registered under before car#1107 unified it
/// with the mobile-canonical `parslee-core`. macOS (`ChatTabView.swift`) still
/// hardcodes this. Kept as a server-side alias — never used for new
/// registration — so old clients and old `agents.json` entries keep working.
/// See [`is_assistant_alias`].
pub const LEGACY_ASSISTANT_AGENT_ID: &str = "car-assistant";

/// True if `id` names the flagship assistant under either its canonical or
/// legacy spelling. Used by callers that need to resolve a caller-supplied
/// `agent_id` to whichever spelling the assistant actually attached under
/// (see `car-server-core::handler::handle_agents_chat`).
pub fn is_assistant_alias(id: &str) -> bool {
    id == ASSISTANT_AGENT_ID || id == LEGACY_ASSISTANT_AGENT_ID
}

const ASSISTANT_CAPABILITIES: &[&str] = &[
    "chat",
    "verified-answers",
    "safety-checks",
    "memory-routines",
    "background-work",
    "trust-control",
    "approvals",
    "approval-channels",
    "a2ui",
    "continuity",
    "notifications",
    "connected-services",
    "conversation-channels",
    "workspace-files",
    "governed-host-execution",
    "durable-transcript",
    "durable-action-fencing",
    "personal-context",
    "documents",
    "creative-output",
];

/// Build the assistant's spec for a given `car` binary path.
fn spec(command: String) -> AgentSpec {
    AgentSpec {
        id: ASSISTANT_AGENT_ID.to_string(),
        name: "Parslee Core".to_string(),
        command,
        args: vec!["do".to_string(), "--serve".to_string()],
        cwd: None,
        env: BTreeMap::new(),
        restart: RestartPolicy::OnFailure,
        max_restarts: 5,
        backoff_secs: 2,
        auto_start: true,
        // Empty → the supervisor mints and persists a per-agent token.
        token: String::new(),
        // Advertise the consumer-visible Parslee Core strengths so hosts do not
        // mistake the flagship assistant for a narrow local chat agent.
        capabilities: assistant_capabilities(),
    }
}

fn assistant_capabilities() -> Vec<String> {
    ASSISTANT_CAPABILITIES
        .iter()
        .map(|capability| capability.to_string())
        .collect()
}

fn merge_assistant_capabilities(existing: &[String]) -> Vec<String> {
    let mut merged = existing.to_vec();
    for capability in ASSISTANT_CAPABILITIES {
        if !merged.iter().any(|value| value == capability) {
            merged.push((*capability).to_string());
        }
    }
    merged
}

/// Register the assistant into an already-held supervisor, using `car_binary`
/// as the command. Skips if already present. `Ok(true)` = newly created.
///
/// If an entry from before car#1107 exists under [`LEGACY_ASSISTANT_AGENT_ID`]
/// and nothing is registered under [`ASSISTANT_AGENT_ID`] yet, migrate it to
/// the new id in place first — preserving the operator's command/args/token —
/// rather than leaving the old entry orphaned alongside a freshly spawned one.
pub async fn ensure_registered_in(sup: &Supervisor, car_binary: &Path) -> Result<bool, String> {
    let already_canonical = sup
        .list()
        .await
        .iter()
        .any(|a| a.spec.id == ASSISTANT_AGENT_ID);
    if !already_canonical {
        if let Some(legacy) = sup
            .list()
            .await
            .iter()
            .find(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID)
        {
            let mut migrated = legacy.spec.clone();
            migrated.id = ASSISTANT_AGENT_ID.to_string();
            sup.upsert(migrated).await.map_err(|e| e.to_string())?;
            sup.remove(LEGACY_ASSISTANT_AGENT_ID)
                .await
                .map_err(|e| e.to_string())?;
            migrate_legacy_agent_memgine(sup);
        }
    } else if sup
        .list()
        .await
        .iter()
        .any(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID)
    {
        // Both entries exist: reachable when a pre-car#1107 `car` binary's
        // `register_via_daemon` (which only ever checked for
        // `LEGACY_ASSISTANT_AGENT_ID`) upserts a `car-assistant` entry
        // alongside a daemon that already self-registered as canonical. The
        // canonical entry is what new code spawns and
        // `resolve_assistant_agent_alias` keeps old clients working against
        // it, so we leave the legacy entry alone rather than risk removing
        // one that might currently be the *attached* process — but it is
        // otherwise silently orphaned, so make it diagnosable.
        tracing::warn!(
            canonical = ASSISTANT_AGENT_ID,
            legacy = LEGACY_ASSISTANT_AGENT_ID,
            "both the canonical and legacy flagship agent ids are registered; \
             the legacy entry is orphaned and will not receive new dispatches"
        );
    }
    if let Some(existing) = sup
        .list()
        .await
        .iter()
        .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
    {
        let merged_capabilities = merge_assistant_capabilities(&existing.spec.capabilities);
        if merged_capabilities != existing.spec.capabilities {
            let mut updated = existing.spec.clone();
            updated.name = "Parslee Core".to_string();
            updated.capabilities = merged_capabilities;
            sup.upsert(updated).await.map_err(|e| e.to_string())?;
        }
        return Ok(false);
    }
    sup.upsert(spec(car_binary.to_string_lossy().into_owned()))
        .await
        .map(|_| true)
        .map_err(|e| e.to_string())
}

/// Carry the flagship's per-agent memgine snapshot over from
/// [`LEGACY_ASSISTANT_AGENT_ID`] to [`ASSISTANT_AGENT_ID`], alongside the
/// `agents.json` manifest migration above.
///
/// This is not cosmetic: the daemon loads that snapshot lazily from
/// `<CAR_HOME>/memory/agents/<id>.json`
/// (`car_server_core::handler::agent_memgine_snapshot_path` /
/// `get_or_load_agent_memgine`, both keyed by whichever id the flagship
/// actually attaches under). Once the manifest entry above is renamed, a
/// freshly spawned `car do --serve` attaches as `parslee-core` — which has no
/// snapshot file yet — while the install's real accumulated memory sits
/// untouched under `car-assistant.json`. Without this step an existing
/// user's personal memory would silently look reset after upgrading past
/// car#1107, even though nothing was deleted.
///
/// The root is derived from the supervisor's own `manifest_path` rather than
/// calling `car_home::root()` directly: in production the two are the same
/// path (`Supervisor::user_default_manifest_path` *is*
/// `car_home::root().join("agents.json")`), but deriving it this way means a
/// test supervisor built on an isolated tempdir (`Supervisor::with_paths`,
/// as every test in this module uses) never reaches into the real home
/// directory — the same reasoning `peers::append_peer_audit_at` documents
/// for not mutating the process-global `CAR_HOME` in tests.
///
/// Best-effort: a filesystem error here must not undo (or fail) the manifest
/// migration that already committed above — losing the memgine snapshot is
/// bad, refusing to register the assistant at all over it would be worse.
fn migrate_legacy_agent_memgine(sup: &Supervisor) {
    let Some(root) = sup.manifest_path().parent() else {
        tracing::warn!(
            "skipping flagship memgine snapshot migration: manifest path has no parent directory"
        );
        return;
    };
    let legacy = agent_memgine_snapshot_path_under(root, LEGACY_ASSISTANT_AGENT_ID);
    let canonical = agent_memgine_snapshot_path_under(root, ASSISTANT_AGENT_ID);
    migrate_legacy_agent_memgine_at(&legacy, &canonical);
}

/// `<root>/memory/agents/<id>.json` — mirrors the shape of
/// `car_server_core::handler::agent_memgine_snapshot_path`, which resolves
/// `<root>` from `car_home::root()`; here the caller supplies `<root>`
/// directly (see [`migrate_legacy_agent_memgine`]).
fn agent_memgine_snapshot_path_under(root: &Path, agent_id: &str) -> std::path::PathBuf {
    root.join("memory")
        .join("agents")
        .join(format!("{agent_id}.json"))
}

/// [`migrate_legacy_agent_memgine`] against explicit paths.
///
/// Split out so the migration logic itself is directly unit-testable.
fn migrate_legacy_agent_memgine_at(legacy: &Path, canonical: &Path) {
    // Nothing to carry over, or a snapshot already exists under the new id
    // (e.g. the flagship already ran once as `parslee-core`) — never
    // overwrite real data with a stale legacy copy.
    if canonical.exists() || !legacy.exists() {
        return;
    }
    if let Some(parent) = canonical.parent() {
        if let Err(e) = std::fs::create_dir_all(parent) {
            tracing::warn!(
                dir = %parent.display(),
                "could not migrate flagship memgine snapshot to the new agent id: {e}"
            );
            return;
        }
    }
    if let Err(e) = std::fs::rename(legacy, canonical) {
        tracing::warn!(
            from = %legacy.display(),
            to = %canonical.display(),
            "could not migrate flagship memgine snapshot to the new agent id: {e}"
        );
    }
}

/// Register the assistant via the user-default supervisor, using the running
/// executable as the command (the `car` CLI calls this). Returns `Err` if the
/// manifest is locked by a running daemon — the caller should treat that as
/// "already handled by the daemon" and stay quiet.
pub async fn ensure_registered() -> Result<bool, String> {
    let exe =
        std::env::current_exe().map_err(|e| format!("cannot resolve current executable: {e}"))?;
    let sup = Supervisor::user_default().map_err(|e| e.to_string())?;
    ensure_registered_in(&sup, &exe).await
}

/// True if `err` is the "another supervisor owns the manifest" lock message —
/// i.e. a daemon is running and will have registered the assistant itself.
pub fn is_manifest_locked(err: &str) -> bool {
    err.contains("another supervisor already owns this manifest")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    /// A real, absolute, non-world-writable executable to stand in for the `car`
    /// binary. The upsert validator requires an absolute path to an existing file
    /// and rejects world-writable scratch dirs (`/tmp`, `%TEMP%`, …) — so we
    /// can't point at the tempdir, and `/bin/sh` doesn't exist on Windows.
    /// `%SystemRoot%\System32` is the Windows analogue of `/bin`: real, absolute,
    /// and not on the scratch denylist.
    fn stand_in_binary() -> PathBuf {
        if cfg!(windows) {
            let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
            PathBuf::from(format!(r"{root}\System32\cmd.exe"))
        } else {
            PathBuf::from("/bin/sh")
        }
    }

    /// A *different* real executable, for asserting that an existing operator-set
    /// command is not overwritten.
    fn other_binary() -> PathBuf {
        if cfg!(windows) {
            let root = std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
            PathBuf::from(format!(r"{root}\System32\where.exe"))
        } else {
            PathBuf::from("/bin/echo")
        }
    }

    #[tokio::test]
    async fn register_is_idempotent_and_uses_the_given_binary() {
        let dir = tempfile::tempdir().unwrap();
        let manifest = dir.path().join("agents.json");
        let logs = dir.path().join("logs");
        let sup = Supervisor::with_paths(manifest, logs).unwrap();

        let car = stand_in_binary();

        // First registration creates it; second is a no-op (respects existing).
        assert!(ensure_registered_in(&sup, &car).await.unwrap());
        assert!(!ensure_registered_in(&sup, &car).await.unwrap());

        let listed = sup.list().await;
        let entry = listed
            .iter()
            .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
            .expect("assistant registered");
        assert_eq!(entry.spec.command, car.to_string_lossy());
        assert_eq!(
            entry.spec.args,
            vec!["do".to_string(), "--serve".to_string()]
        );
        assert!(entry.spec.auto_start);
        assert!(
            entry.spec.capabilities.contains(&"chat".to_string()),
            "assistant must advertise the chat capability"
        );
        assert!(
            entry.spec.capabilities.len() > 10,
            "assistant must advertise the full Parslee Core capability surface"
        );
        for expected in [
            "verified-answers",
            "memory-routines",
            "approvals",
            "approval-channels",
            "a2ui",
            "continuity",
            "notifications",
            "connected-services",
            "documents",
            "creative-output",
        ] {
            assert!(
                entry.spec.capabilities.contains(&expected.to_string()),
                "assistant capability metadata missing {expected}"
            );
        }
        // The supervisor minted a token.
        assert!(!entry.spec.token.is_empty());
    }

    #[tokio::test]
    async fn register_refreshes_stale_builtin_capabilities_without_overwriting_operator_settings() {
        let dir = tempfile::tempdir().unwrap();
        let manifest = dir.path().join("agents.json");
        let logs = dir.path().join("logs");
        let sup = Supervisor::with_paths(manifest, logs).unwrap();

        // The operator's own command/args, which registration must not clobber.
        // Never executed (auto_start = false), so the args are inert data.
        let operator_cmd = stand_in_binary();
        sup.upsert(AgentSpec {
            id: ASSISTANT_AGENT_ID.to_string(),
            name: "CAR Assistant".to_string(),
            command: operator_cmd.to_string_lossy().into_owned(),
            args: vec!["-c".to_string(), "sleep 1".to_string()],
            cwd: None,
            env: BTreeMap::new(),
            restart: RestartPolicy::Never,
            max_restarts: 1,
            backoff_secs: 9,
            auto_start: false,
            token: "operator-token".to_string(),
            capabilities: vec!["chat".to_string()],
        })
        .await
        .unwrap();

        assert!(!ensure_registered_in(&sup, &other_binary()).await.unwrap());

        let listed = sup.list().await;
        let entry = listed
            .iter()
            .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
            .expect("assistant registered");
        assert_eq!(entry.spec.name, "Parslee Core");
        // Still the operator's binary, not the one we just passed in.
        assert_eq!(entry.spec.command, operator_cmd.to_string_lossy());
        assert_eq!(
            entry.spec.args,
            vec!["-c".to_string(), "sleep 1".to_string()]
        );
        assert_eq!(entry.spec.restart, RestartPolicy::Never);
        assert_eq!(entry.spec.max_restarts, 1);
        assert_eq!(entry.spec.backoff_secs, 9);
        assert!(!entry.spec.auto_start);
        assert_eq!(entry.spec.token, "operator-token");
        assert!(entry
            .spec
            .capabilities
            .contains(&"verified-answers".to_string()));
        assert!(entry.spec.capabilities.contains(&"a2ui".to_string()));
        assert!(entry
            .spec
            .capabilities
            .contains(&"connected-services".to_string()));
        assert!(entry
            .spec
            .capabilities
            .contains(&"creative-output".to_string()));
    }

    /// car#1107: an install that registered before the id unification has a
    /// `car-assistant` entry in `agents.json`. Registering fresh code against
    /// it must migrate that entry to `parslee-core` in place — preserving the
    /// operator's command/token/auto_start — rather than leaving `car-assistant`
    /// orphaned alongside a brand-new `parslee-core` entry.
    #[tokio::test]
    async fn ensure_registered_in_migrates_a_legacy_car_assistant_entry() {
        let dir = tempfile::tempdir().unwrap();
        let manifest = dir.path().join("agents.json");
        let logs = dir.path().join("logs");
        let sup = Supervisor::with_paths(manifest, logs).unwrap();

        let legacy_cmd = stand_in_binary();
        sup.upsert(AgentSpec {
            id: LEGACY_ASSISTANT_AGENT_ID.to_string(),
            name: "Parslee Core".to_string(),
            command: legacy_cmd.to_string_lossy().into_owned(),
            args: vec!["do".to_string(), "--serve".to_string()],
            cwd: None,
            env: BTreeMap::new(),
            restart: RestartPolicy::OnFailure,
            max_restarts: 5,
            backoff_secs: 2,
            auto_start: true,
            token: "legacy-token".to_string(),
            capabilities: vec!["chat".to_string()],
        })
        .await
        .unwrap();

        // Seed a pre-migration memgine snapshot under the legacy id, the way
        // an install that has actually been chatting with the assistant
        // would have one. Lives under the same tempdir root the manifest
        // does, so this stays fully isolated from the real `~/.car`.
        let legacy_memgine =
            agent_memgine_snapshot_path_under(dir.path(), LEGACY_ASSISTANT_AGENT_ID);
        std::fs::create_dir_all(legacy_memgine.parent().unwrap()).unwrap();
        std::fs::write(
            &legacy_memgine,
            r#"[{"subject":"s","body":"remembered fact"}]"#,
        )
        .unwrap();

        // Newly created is false: the migration counts as "already present",
        // not a fresh registration.
        assert!(!ensure_registered_in(&sup, &other_binary()).await.unwrap());

        let listed = sup.list().await;
        assert!(
            listed
                .iter()
                .all(|a| a.spec.id != LEGACY_ASSISTANT_AGENT_ID),
            "the legacy car-assistant entry must not survive migration: {:?}",
            listed.iter().map(|a| &a.spec.id).collect::<Vec<_>>()
        );
        let migrated = listed
            .iter()
            .find(|a| a.spec.id == ASSISTANT_AGENT_ID)
            .expect("legacy entry migrated to the canonical id");
        assert_eq!(migrated.spec.command, legacy_cmd.to_string_lossy());
        assert_eq!(migrated.spec.token, "legacy-token");
        assert!(migrated.spec.auto_start);

        // The memgine snapshot moved with it: gone from the legacy path,
        // present with its original content under the canonical one. This is
        // the regression `mliotta`'s review on car#1110 flagged — the
        // manifest migration alone left an existing user's personal memory
        // looking reset after the id rename.
        assert!(
            !legacy_memgine.exists(),
            "legacy memgine snapshot should have moved, not been copied"
        );
        let canonical_memgine = agent_memgine_snapshot_path_under(dir.path(), ASSISTANT_AGENT_ID);
        let content = std::fs::read_to_string(&canonical_memgine).unwrap();
        assert!(content.contains("remembered fact"));

        // Both spellings name the assistant, and the canonical id is the one
        // actually registered.
        assert!(is_assistant_alias(ASSISTANT_AGENT_ID));
        assert!(is_assistant_alias(LEGACY_ASSISTANT_AGENT_ID));
        assert!(!is_assistant_alias("some-other-agent"));
    }

    /// `migrate_legacy_agent_memgine_at` in isolation, covering the cases
    /// `ensure_registered_in_migrates_a_legacy_car_assistant_entry` above
    /// doesn't reach: no legacy file to begin with, and a canonical file
    /// that already exists (must never be clobbered by a stale legacy copy).
    #[test]
    fn migrate_legacy_agent_memgine_at_is_a_noop_with_no_legacy_file() {
        let dir = tempfile::tempdir().unwrap();
        let legacy = dir.path().join("legacy.json");
        let canonical = dir.path().join("canonical.json");

        migrate_legacy_agent_memgine_at(&legacy, &canonical);

        assert!(!legacy.exists());
        assert!(!canonical.exists());
    }

    #[test]
    fn migrate_legacy_agent_memgine_at_never_overwrites_an_existing_canonical_file() {
        let dir = tempfile::tempdir().unwrap();
        let legacy = dir.path().join("legacy.json");
        let canonical = dir.path().join("canonical.json");
        std::fs::write(&legacy, "stale legacy content").unwrap();
        std::fs::write(&canonical, "real canonical content").unwrap();

        migrate_legacy_agent_memgine_at(&legacy, &canonical);

        // Both untouched: the canonical file is what the flagship is
        // actually reading from, so a stale legacy copy must never stomp it.
        assert!(legacy.exists());
        assert_eq!(
            std::fs::read_to_string(&canonical).unwrap(),
            "real canonical content"
        );
    }

    /// Reachable when a pre-car#1107 `car` binary re-registers the legacy id
    /// against a daemon that has already self-registered as canonical (its
    /// `register_via_daemon` only ever checked for `car-assistant`). Both
    /// entries end up in the manifest; `ensure_registered_in` must leave both
    /// alone rather than delete a legacy entry that might currently be the
    /// attached process.
    #[tokio::test]
    async fn ensure_registered_in_leaves_both_entries_alone_when_both_exist() {
        let dir = tempfile::tempdir().unwrap();
        let manifest = dir.path().join("agents.json");
        let logs = dir.path().join("logs");
        let sup = Supervisor::with_paths(manifest, logs).unwrap();

        let car = stand_in_binary();
        sup.upsert(spec(car.to_string_lossy().into_owned()))
            .await
            .unwrap();
        sup.upsert(AgentSpec {
            id: LEGACY_ASSISTANT_AGENT_ID.to_string(),
            name: "Parslee Core".to_string(),
            command: other_binary().to_string_lossy().into_owned(),
            args: vec!["do".to_string(), "--serve".to_string()],
            cwd: None,
            env: BTreeMap::new(),
            restart: RestartPolicy::OnFailure,
            max_restarts: 5,
            backoff_secs: 2,
            auto_start: true,
            token: "legacy-token".to_string(),
            capabilities: vec!["chat".to_string()],
        })
        .await
        .unwrap();

        // Reports "already present", not newly created, and must not error.
        assert!(!ensure_registered_in(&sup, &car).await.unwrap());

        let listed = sup.list().await;
        assert!(
            listed.iter().any(|a| a.spec.id == ASSISTANT_AGENT_ID),
            "canonical entry must survive"
        );
        assert!(
            listed
                .iter()
                .any(|a| a.spec.id == LEGACY_ASSISTANT_AGENT_ID),
            "legacy entry is left in place (orphaned, but not deleted out from \
             under a process that might be attached under it)"
        );
    }
}