fix: reliable multi-device discovery and chat --device flag#30
fix: reliable multi-device discovery and chat --device flag#30
Conversation
Setup now preserves existing devices when adding new ones (append instead of replace). Adds `ff1 device list`, `ff1 device add`, and `ff1 device remove` commands for managing multiple FF1 devices. Status command updated to show all configured devices. https://claude.ai/code/session_01M1HKhzHKg3gKRMKq19RTiB
Extract discoverAndSelectDevice() and upsertDevice() to eliminate duplicated discovery+selection logic between setup and device add. Add normalizeDeviceIdToHost() for consistent ID-to-host conversion. Update docs/README.md and docs/CONFIGURATION.md with new device commands. Add multi-device resolution tests. https://claude.ai/code/session_01M1HKhzHKg3gKRMKq19RTiB
- Use avahi-browse on Linux for mDNS discovery (fixes single-device limit caused by bonjour-service missing responses on Linux) - Increase bonjour fallback timeout 2s → 5s for macOS/Windows - Remove apiKey/topicID prompts from device add (LAN-only for now) - Remove empty apiKey/topicID defaults from upsertDevice - Add --device flag to chat command for parity with play/send/ssh - Update docs: remove stale apiKey/topicID from config example, add --device to chat options list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3).
-
chat --deviceis still not implemented on the main AI chat path, which is the headline feature of this PR.index.tscorrectly accepts and forwardsoptions.deviceintobuildPlaylist(...), but insrc/main.ts:187-258that value is only consumed by the specialsend ...shortcut atsrc/main.ts:205-249. For the normal intent-parser / orchestrator flow,defaultDeviceNameis never merged intoparams.playlistSettings.deviceNamebefore the request is handed off, so commands likeff1 chat -d kitchen "build something and display it"can still target the wrong device unless the prompt text itself names the device. This was independently raised by all 3 reviewers, and the current code still supports that concern. -
The new docs promise a non-interactive
ff1 device add --host ... --name ...flow, but the implementation still always opensreadlineand always prompts for the name. Inindex.ts:1367-1416, even when both--hostand--nameare already provided, the command still callsask(namePrompt). That conflicts withdocs/CONFIGURATION.mdanddocs/README.md, and it means scripted or non-TTY usage can block on stdin instead of completing directly. This was also raised repeatedly across the reports and remains valid in the current diff. -
device removeintroduces device-name matching semantics that conflict with the rest of the multi-device feature.resolveConfiguredDevice()uses exact, case-sensitive name matching insrc/utilities/ff1-compatibility.ts:61-79, and the new tests explicitly preserveKitchenandkitchenas distinct devices intests/multi-device.test.ts:90-99. Butindex.ts:1456-1458lowercases both sides and removes the first match, soff1 device remove kitchencan delete the wrong configured device in a setup the rest of the CLI can distinguish. This was only raised by one reviewer, but re-review shows it is a real correctness issue introduced by this PR.
I also checked the “missing tests” points. Those are valid as supporting context, but they are downstream of the issues above rather than separate review blockers: there is still no coverage for chat --device on the AI-driven path, and no coverage for the documented non-interactive device add --host --name behavior.
|
Automated human review request for PR #30. Risk zone: yellow This PR needs targeted human review on the scoped areas below, not a full second pass of the whole PR.
|
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
- Critical:
chat --deviceis only honored in the literalsend...shortcut. The normal chat path still reaches src/main.ts without any device override, soff1 chat --device office "..."can still fall back to the first configured device. This was independently flagged by reviewers 2 and 3, and it affects the primary new behavior. - Medium:
device add --host --nameis still interactive. The command always opens readline and prompts for a device name in index.ts, so the documented non-interactive flow blocks waiting for stdin even when both flags are supplied. Also reported by reviewers 2 and 3. - Medium: Updating an existing host can silently rename the device to
ff1if the name prompt is left blank. In the overwrite path in index.ts, the fallback does not preserve the stored label, so a previously named device can be clobbered during an edit. - Medium: The new Linux discovery branch ignores the timeout contract. src/utilities/ff1-discovery.ts hard-codes an
avahi-browsetimeout and never forwardsoptions.timeoutMsinto that path, so callers cannot bound discovery duration on Linux. - Medium: Name-based routing can still hit stale duplicates.
upsertDevice()in index.ts dedupes only by host, so re-adding the same logical device under a new host can leave multiple entries with the same name; later selection in src/utilities/ff1-compatibility.ts will return the first match, which may be the stale device.
…reservation, timeout forwarding, name dedup
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
Critical issues
- Duplicate device names are still accepted, but device lookup is exact-name plus first-match. That makes
ff1 send -d kitchen,ssh -d kitchen, and chat-driven sends ambiguous and can route to the wrong device with no error. I re-checked the newdevice addflow and it only warns at write time (index.ts), while resolution still returns the first matching name (ff1-compatibility.ts). This is a real correctness bug, not just a UX issue.
Medium issues
chat --deviceis only honored by the literalsend ...shortcut. The CLI flag is passed intobuildPlaylist()(index.ts), but the main AI-drivensend_playlistpath ignores it and sendssendParams.deviceNamefrom the model instead (src/main.ts). Reviewers 1 and 3 both flagged this, and it will still target the wrong/default device on the common path.- Updating an existing device by host can silently lose the stored device name. In both setup and
ff1 device add, the overwrite default is derived fromdiscoveredNameor--nameinstead of the current config entry, so pressing Enter afterUpdate this device?relabels it toff1or the discovered mDNS name. Reviewers 2 and 3 both called this out, and it breaks later name-based selection. - The setup/status check still treats only
ff1Devices.devices[0]as meaningful (index.ts). In a multi-device config, a bad first entry can still report the whole config as missing even if later devices are valid, so the new multi-device flow is still carrying a single-device assumption.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
Critical issues
- The
chat --deviceoverride is still dropped on the direct send path.[src/main.ts](/Users/ffci/codex/repo/feral-file_ff1-cli-pr30-QamzMC/repo/src/main.ts#L328)only copies the CLI fallback intoplaylistSettings.deviceName, but thesend_playlistbranch readssendParams.deviceNameat[src/main.ts](/Users/ffci/codex/repo/feral-file_ff1-cli-pr30-QamzMC/repo/src/main.ts#L333). That means a chat request resolved assend_playlistcan ignore--deviceand route to the default device instead. This is the same root cause all three reviewers independently called out, so it should be treated as the main blocker. Add a regression test for thesend_playlistintent path with the CLI device override.
Medium issues
- Rediscovering an already configured host can silently rename the stored device entry. In
[index.ts](/Users/ffci/codex/repo/feral-file_ff1-cli-pr30-QamzMC/repo/index.ts#L1446), the prompt default prefersdiscoveredNameover the existing saved name, so pressing Enter after rediscovery writes the discovery label back into config. That mutates user-managed labels and can break exact-match routing forsendandssh. Preserve the existing name when the host is already configured, and add a regression test for the blank-response case.
Overall: the routing regression is a merge blocker, and the label-clobbering behavior is also worth fixing before merge.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
Medium issues
- src/utilities/ff1-discovery.ts:104 and src/utilities/ff1-discovery.ts:115 make the Linux path lossy: Avahi headers are split on whitespace and the service name is lowercased before it is stored. That can truncate multi-word instance names and rewrite mixed-case labels, but src/utilities/ff1-compatibility.ts:78 does an exact name lookup, so a Linux-discovered device may be impossible to target later under the label the operator saw. This needs regression coverage for mixed-case and multi-word discovery output.
- src/utilities/device-upsert.ts:26 removes a same-name device and appends the replacement to the end of the array. Because the CLI defaults to
devices[0]when--deviceis omitted, re-adding or rediscovering a moved device can silently change the implicit target forplay,send, andssh. That is user-visible routing drift; either preserve array position or lock down the new defaulting rule with a test.
I rechecked the reported same-host metadata-loss concern and did not carry it forward: the host-match branch in upsertDevice() spreads the existing entry, so apiKey and topicID survive.
…; preserve array position on host-change upsert
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
- Critical:
src/utilities/device-upsert.ts:30still replaces same-name, different-host entries with{ ...newDevice }, which drops any existingapiKeyandtopicID. That is real config data loss, and it matters becausesendPlaylistToDevice()andsendSshAccessCommand()still consume those fields. This was the one issue all three reviewers independently converged on, so it should stay active; it also needs a regression test for re-adding an existing device after a host change. - Critical:
src/main.ts:334copies the CLI--deviceflag intoplaylistSettings.deviceNamebefore the intent result is known, andsrc/utilities/index.js:526sends whenever that field is defined. In practice,ff1 chat --device ...now turns a build-only chat into an implicit send to hardware, which is a user-visible behavior regression with a network side effect. Two reviewers flagged the same path; I rechecked it and the concern is valid. Add a regression test that a build-only chat request with--devicedoes not dispatch unless the intent parser selectedsend_playlist. - Medium: the setup flow at
index.ts:528still defaults rediscovered devices toselection.discoveredName || 'ff1', so pressing Enter can overwrite an already curated label when the same host is seen again. Thedevice addpath now preserves the existing name default, so this is limited tosetup, but it is still enough to make exact-name targeting flaky. This should get a small regression test around rediscovery of an existing host. - Medium:
src/utilities/ff1-discovery.ts:188treats any non-emptyavahi-browsestdout as a successful Linux discovery, even when the subprocess exits non-zero, so Bonjour fallback is bypassed in partial-failure cases. That weakens the reliability fix the PR is aiming for because Linux discovery can still come back partial instead of recovering through the fallback path. A test should cover the non-zero-with-stdout branch, or the code should explicitly document that behavior if partial results are intentional.
Overall verdict: request changes.
… chat --device, preserve setup labels, fall through on avahi non-zero exit
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
src/utilities/ff1-discovery.ts:181-199still drops everyavahi-browseresult on any non-zero exit and falls back to Bonjour, even when stdout may already contain valid resolved devices. That weakens the main Linux reliability fix this PR is trying to land, and it was the recurring concern across reports, so I’d keep it as the highest-priority issue. Add a regression test for the “usable stdout + non-zero exit” case so the fallback only happens when the avahi output is actually unusable.index.ts:620-623only validatesff1Devices.devices[0].hostwhile advertising the full device count inff1 status. In a multi-device config, a malformed second or later entry can still show the whole block as OK, which makes the new plural-device summary misleading. Validate the full array, or fail status when any configured device is malformed.
I rechecked the reported chat --device precedence and raw "null" override concerns and did not raise them: confirmPlaylistForSending() already normalizes "null" and "" to undefined, and the direct send shortcut does not go through the auto-select path described in the reports.
…idate all device hosts in status
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
Critical issues
- index.ts:527 and index.ts:1422 still regress re-adding a device whose host changed. Setup and
device addonly preserve the stored friendly name on an exact host match, so when a previously configured FF1 comes back on a new IP the blank prompt falls back to the raw mDNS label andupsertDevice()appends a duplicate row instead of updating the existing one. That leaves the stale device in slot 0, soplay/send/sshcan keep targeting the wrong host. This is the same issue independently raised by reviewers 1 and 3.
Missing tests
- tests/chat-device-routing.test.ts and src/main.ts:334 still leave the
--devicerouting path under-tested. The current tests validate extracted helpers, but not the actualbuildPlaylistsend shortcut andsend_playlistfallback branches that decide whetherdefaultDeviceNameis honored. A regression test that drives those branches directly would better protect the user-facingchat --deviceflow. - src/utilities/ff1-discovery.ts:181 adds a new
avahi-browsenon-zero-exit fallback path, but the current discovery tests only coverparseAvahiBrowseOutput. A targeted test for theexecFileerror branch would make the Linux reliability change much safer.
…ahiResult and send-shortcut helpers for testing
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, request-changes: 3)
src/utilities/device-lookup.tsstill does not reconcile a stored IP host with a rediscovered.localname, andindex.tsstill marksalready configuredby exact host equality. That leaves the setup anddevice addflows able to treat the same FF1 as new after an address change, default to the raw discovery label, and create duplicate entries instead of preserving the saved friendly name. This is the same host-change regression raised by reviewers 1 and 2, and there is still no regression test covering the IP↔.localcase or the discovery-list state.
I did not keep the chat --device routing concern from reviewer 3: the explicit send_playlist path in src/main.ts already falls back to the CLI flag, and I did not find a separate orchestrator regression introduced by this diff.
…changes across setup, device add, and discovery list
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
src/utilities/device-lookup.tsand the write path inindex.tsstill do not make reconciliation end-to-end.findExistingDeviceEntry()checks exact host beforeid, so a stale configured row can win over the stable device identity, andupsertDevice()still dedupes only by host/name, so when a rediscovered device gets a new friendly label it is appended instead of updating the matched entry. That breaks the PR’s core host-change reconciliation goal and can silently reorder the default device list.src/utilities/device-upsert.tsis still vulnerable to erasing stored identity metadata: callers passid: discoveredIdunconditionally, so any same-host/same-name update that does not have a discovery id overwrites a previously persistedidwithundefined. This was independently raised by multiple reviewers, and it removes the metadata the new matching logic depends on.src/utilities/ff1-discovery.tsstill assumes_ff1._tcpwill appear in the resolvedavahi-browse -rheader line when extracting the service name. Real Avahi resolved output does not expose the type token that way, so multi-word device names can be truncated to the first word and later exact-name matching will fail on Linux.index.tsmakes the selection prompt wording more permissive than the matcher actually is. The prompt saysenter ID/host, but the comparison logic only checks the raw lowercased answer against bare host forms, so a pasted URL-form host likehttp://ff1-hh9jsnoc.local:1111will not select the device unless the user strips the scheme first.
Overall verdict: request changes.
…gex, URL-form selection - device-lookup: check mDNS device ID before exact host URL so a stale host entry for another device cannot shadow the correct identity match - device-upsert: add id-based deduplication as the first case; strip undefined values before spreading so callers passing id: undefined do not erase a previously stored id - ff1-discovery: use prefix regex for type token detection so _ff1._tcp.local variant does not truncate multi-word service names - index: also compare selection against the normalized URL-form host so pasting http://ff1-hh9jsnoc.local:1111 matches the correct device - tests: add regressions for all four fixes (63 passing)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 2, request-changes: 1, comment: 0)
Critical issues
src/utilities/device-lookup.tsandindex.tsstill leave a legacy gap for pre-idconfigs: if a stored device has a curated name likekitchenbut noid, and rediscovery returns a.localhost whose avahi label is just the raw service label or hostname-derived id,findExistingDeviceEntry()falls through andupsertDevice()appends a duplicate entry instead of updating in place. That means IP-to-.localmigration is still broken for an important class of existing configs. The current coverage intests/device-lookup.test.tsexercises stored-idand TXT-name fallback paths, but not this no-id + renamed-host regression.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 1, request-changes: 2)
- Medium: src/utilities/ff1-discovery.ts only keeps one resolved
addressper service, but src/utilities/device-lookup.ts depends on the full address set to bridge IP ↔.localmigrations. On dual-stack or otherwise multi-address FF1s, avahi can emit more than one resolved IP; if the stored IP is not the one that survives parsing,findExistingDeviceEntry()misses the match anddevice add/setup can append a duplicate instead of updating in place. This is the same defect both reviewers 1 and 3 raised, and the current tests only cover a single address line, so the regression remains unguarded.
I’m not elevating the updated: false log wording from reviewer 3; that looks like a UX nit rather than a functional bug.
parseAvahiBrowseOutput was overwriting rawAddress on each address line, keeping only the last resolved IP. On dual-stack or multi-interface FF1s avahi emits one resolved record per address family; the parser now accumulates all address lines into rawAddresses[] and merges them when the same hostname:port key appears more than once, so findExistingDeviceEntry can match the stored IP regardless of which address family avahi reports first. Tests: dual-stack merge (both IPs present), stored IP second in list (68 passing).
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
Critical issues
- None.
Medium issues
src/utilities/device-lookup.ts:findExistingDeviceEntry()only does IP↔.localreconciliation whendiscoveredAddressesis present. The newdevice add --host <ip>path calls it withdiscoveredAddressesunset, so an existing.localrow will not match and the command can append a duplicate device instead of updating in place.src/utilities/ff1-discovery.ts: when avahi emits a later partial record for the samehost:portkey, the merge preserves onlyaddressesand overwritesname,id, andtxtwith the later record. That can clobber a previously complete device entry and drop TXT metadata needed for stable lookup/routing.src/main.ts: the directsend_playlistbranch still trustssendParams.deviceNameverbatim.confirmPlaylistForSending()already normalizes sentinel values on the shortcut path, but this path does not, so a parser-emitted literal"null"can override the CLI--devicefallback and misroute or fail the send.
Missing tests
- No focused regression covers the manual
device add --hostcase where an existing.localentry should be recognized withoutdiscoveredAddresses. - No test asserts that merged avahi records preserve the earlier
name/txtfields instead of being overwritten by a later partial record. - No test exercises the parser-emitted
"null"case on thesend_playlistpath.
- device-lookup: add step 4b — when newHost is an IP URL and discoveredAddresses is absent (--host path), check if any stored entry has that IP in its stored addresses list; allows device add --host <ip> to match an existing .local row - device-upsert/index: add addresses to DeviceEntry and upsertDevice so mDNS-resolved IPs are persisted to config and available for step 4b lookups - ff1-discovery: avahi merge now preserves name/id/txt from whichever record has TXT metadata; a later partial record (e.g. IPv6 interface, no TXT) no longer clobbers the friendly name from the earlier complete record - main.ts: sanitize parser-emitted "null" string on the direct send_playlist path before resolving device name, matching confirmPlaylistForSending behaviour - tests: --host reverse lookup, avahi TXT-surviving merge, "null" sentinel (71 passing)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 0, request-changes: 3)
- IPv6 reverse lookup still never runs because
findExistingDeviceEntry()only treats dotted-quad hosts as IPs at src/utilities/device-lookup.ts. Since discovery now persists IPv6 addresses,ff1 device add --host [fe80::1]can miss an existing.localentry and create a duplicate on dual-stack or IPv6-only networks. This was raised by all three reviewers, and tests/device-lookup.test.ts should add an IPv6 regression for the stored-address fallback. - The discovery merge still prefers
existing?.txtunconditionally at src/utilities/ff1-discovery.ts, so a partial record that hastxt = []leaves{}behind and blocks a later complete TXT payload from replacing it. That conflicts with the merge intent in the surrounding comment and can leave the device name/id stuck on incomplete data. tests/avahi-parse.test.ts needs a partial-then-complete regression here. upsertDevice()still overwritesaddresseswholesale on every update at src/utilities/device-upsert.ts. If a later discovery only reports a subset of previously observed addresses, the stored list shrinks and the new reverse-lookup path can stop matching the device again, reintroducing duplicates. tests/device-upsert.test.ts should cover address preservation or merging on update.
I did not raise the chat --device end-to-end wiring test gap separately because the helper coverage and the direct option pass-through in index.ts make it lower risk than the parser/lookup regressions above.
… upsert
- device-lookup: strip IPv6 brackets from URL.hostname before comparing
against stored addresses (Node URL.hostname returns '[fe80::1]' but stored
addresses use the bare form 'fe80::1'); applies to both 4a and 4b checks
- ff1-discovery: treat empty txt objects {} (from avahi txt=[]) as absent so
a later complete TXT payload can replace it; only non-empty txt qualifies
as 'existing txt content' in the merge
- device-upsert: replace raw spread with applyPatch() helper that calls
mergeAddresses() so a later discovery reporting a subset of IPs does not
shrink the stored address list and break reverse-IP lookups
- tests: IPv6 stored-address regression, partial-then-complete TXT ordering,
address preservation on re-upsert (74 passing)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 2, request-changes: 1)
Critical issues
- src/utilities/device-upsert.ts and src/utilities/device-lookup.ts together leave stale IPs in
addressesacross host changes, and the reverse lookup still trusts that list for--host <ip>. Re-reviewing this confirms the reviewer 1 finding: a device that moved can continue to match an old IP and be resolved as the wrong configured FF1. That is a real correctness/routing bug, not just a cosmetic issue.
Missing tests
- The coverage gap around the discovery-to-upsert flow is still real and was raised by more than one reviewer: there is no command-level regression test for
ff1 device add/setupthat exercises the Linuxavahi-browsebranch plus the selection-to-upsert wiring end to end. The current tests cover parsing and helpers in isolation, but not the top-level CLI path. - There is still no end-to-end regression test for
chat --devicethrough src/main.ts and src/main.ts, so thesend_playlistfallback when the intent parser omits a device remains unverified outside the helper tests. This was also called out in review and is worth closing before merge.
Overall, this is not merge-ready yet because the stale-address lookup bug is user-visible and the new CLI routing paths still lack end-to-end coverage.
- device-upsert: applyPatch now replaces addresses when the host changes (old IPs belonged to old network location; merging them would let --host <old-ip> route to the wrong device after a move); still merges when host is unchanged to accumulate IPv6 across partial discoveries - main.ts: extract resolveSendPlaylistDeviceName() — the sanitize→resolve pipeline for the send_playlist action path — as an exported testable function; buildPlaylist now calls it instead of inlining the logic - tests/device-upsert.test.ts: stale-IP regression (old IP absent after host change) - tests/device-workflow.test.ts: end-to-end workflow tests chaining parseAvahiBrowseOutput → findExistingDeviceEntry → upsertDevice for re-discovery, IP→.local migration, and dual-stack avahi output - tests/chat-device-routing.test.ts: resolveSendPlaylistDeviceName suite covering undefined / null / "null" / "" sentinels and valid intent name (84 passing, 14 suites)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
Critical issues
device addstill allows duplicate friendly names, and the new upsert path resolves them by first-match overwrite. The command only warns, thenindex.tscallsupsertDevicewhich replaces the first row with that name. In a multi-device config that can silently clobber the wrong device entry and make the previous one unreachable. This needs either hard duplicate-name rejection or explicit update semantics; there is no regression test for the collision case.
Medium issues
device removeonly matchesd.name, so unnamed legacy/manual entries can be used elsewhere in the CLI but cannot be removed. Seeindex.ts. Add coverage for the unnamed-device shape, or teach the command how to target the implicit first device when no name is present.
- index.ts device add: change the duplicate-name warning into a hard guard; in non-interactive mode (--name flag) error and exit so scripts cannot silently clobber a different device; in interactive mode re-prompt once then abort, preventing upsertDevice case-3 from overwriting the wrong entry - index.ts device remove: also match by host URL (exact and normalised) so legacy/manual entries stored without a name field can be targeted and removed; name matching is still tried first - tests: name-collision guard (availability check, update-owner exemption, clobber regression), unnamed-device remove by host (91 passing)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 1, request-changes: 2, comment: 0)
Critical
- The same name-collision overwrite bug is still reachable in both
setupanddevice add:findExistingDeviceEntry()can fall back todiscoveredName, so a newly discovered device that advertises a friendly name already used by another row is resolved to the wrong existing entry, andupsertDevice()then replaces that other device instead of rejecting the add. The duplicate-name guard only runs after lookup and only protects the already-matched row, so it does not prevent this overwrite. This was raised independently by two reviewers, so it should be treated as confirmed rather than speculative.
Missing tests
- Add end-to-end coverage for the collision path: one test where two distinct devices share the same friendly/TXT name but have different ids/hosts and the lookup must not collapse them into one row, plus one setup/device-add test proving the flow rejects or reprompts instead of overwriting the other device.
…t devices findExistingDeviceEntry step 5 was matching stored entries by friendly name regardless of whether the stored entry had an id. A different physical device (different id, different host) that advertises the same TXT name was incorrectly resolved to the existing row, making existingIndex != -1, which exempted that row from the name-collision guard, and letting upsertDevice case-3 silently overwrite the wrong device. Fix: step 5 now only matches entries without a stored id. If a stored entry already has an id and it did not match in step 1, the discovered device is a distinct physical device and must not be conflated by name alone. Tests: TXT-name skipped for id-bearing entry (unit), lookup returns undefined for same-name different-id device, full add-flow shows guard fires correctly (94 passing, 17 suites)
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, request-changes: 3)
-
Critical:
setupstill writes throughupsertDevicewithout the same-name collision guard thatdevice addnow uses (index.ts vs index.ts). A new device that advertises a friendly name already owned by another row can silently replace the wrong entry and make the original device disappear. Reviewers 1 and 3 both flagged this, and thesetuppath still lacks a regression test of its own. -
Medium:
findExistingDeviceEntrynow excludes id-bearing rows from both TXT-name fallback and the resolved-address fallback (src/utilities/device-lookup.ts and src/utilities/device-lookup.ts). That means a partial avahi result or a manualff1 device add --host ...run with no discovery metadata can fail to land on an already-configured device and instead trip the name-collision rejection. Reviewers 2 and 3 reached the same root cause from different angles. -
Medium:
normalizeDeviceHoststill does not canonicalize bare IPv6 input (index.ts), soff1 device add --host fe80::1can still produce an invalidhttp://fe80::1form instead of a bracketed URL. The discovery path is now IPv6-aware; the manual path needs the same normalization.
…d, bare IPv6 normalization
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, request-changes: 3)
Critical issues
- None after re-review.
Medium issues
- src/utilities/device-upsert.ts:45 and src/utilities/device-lookup.ts:93: the new
addressescache is merged on same-host updates and then treated as a valid reverse-lookup signal, so a device that keeps the same.localhost but gets a new DHCP lease can retain stale IPs indefinitely. That can makeff1 device add --host <old-ip>resolve the wrong saved row later. The current tests cover host-change replacement, but not same-host lease churn. - index.ts:171 and index.ts:309:
normalizeDeviceIdToHost()is case-sensitive for both scheme detection and theff1-prefix. Uppercase pasted input likeHTTP://192.168.1.10:1111orFF1-HH9JSNOCcan be rewritten into malformed hosts instead of matching cleanly, which breaks the manual-entry fallback and the discovered-device picker. The new tests do not cover uppercase/pasted input.
Missing tests
- tests/device-upsert.test.ts, tests/device-lookup.test.ts, and tests/chat-device-routing.test.ts: add regressions for same-host IP churn and uppercase device/URL input so these paths are exercised directly.
Overall, the PR is not merge-ready yet. I re-checked reviewer 3's chat --device concern and did not keep it: the send_playlist action is resolved through resolveSendPlaylistDeviceName() before dispatch, and the CLI flag is intentionally not merged into build-only playlist settings.
…ost normalization
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 1, request-changes: 2).
- High: src/utilities/device-upsert.ts:29 preserves
existing.addresseswhenever the incoming patch omitsaddresses, and that leaves the stale-IP bug alive on two paths the reviewers independently flagged: a partial Linux avahi timeout result withhost/idbut no addresses, and the manualff1 device add --host <ip>update path. In both cases, later reverse-IP matching in src/utilities/device-lookup.ts:93 can continue to bind an old IP to the wrong FF1 after DHCP churn or host migration, sosend/sshcan target the wrong device. This recurrence across reviewers makes it the merge-blocking issue here; it needs regression coverage for both partial avahi output and the existing-device--hostupdate path.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, request-changes: 3)
-
src/utilities/device-lookup.ts / index.ts:
findExistingDeviceEntry()still suppresses TXT-name fallback for any stored row with anid, so a rediscovery that only has the host/name but nodiscoveredIdoraddressescannot reconcile an already-configured device. In thedevice add/setupflow that turns a legitimate re-add or host migration into either a duplicate append or a false name-collision error instead of an in-place update. Two reviewers independently hit this same gap; please add a regression for partial discovery with an id-bearing stored row. -
index.ts / src/utilities/device-upsert.ts: if lookup finds the existing physical device by hostname/IP but the user chooses a new friendly name,
upsertDevice()has no path to update that matched row unless the name also matches an existing entry. For id-less migrated devices this falls through to the append path and silently creates a duplicate config row. Add a workflow test for rename-on-rediscovery so the matched row is updated in place. -
src/intent-parser/index.ts / src/main.ts: the
confirm_send_playlistpath still callsconfirmPlaylistForSending()without the CLI--devicefallback, soff1 chat --device kitchen ...can still prompt for device selection or use the parser's omission instead of the CLI default. That leaves the new--deviceparity incomplete on the send path; add a regression coveringconfirm_send_playlistwith no model-provideddeviceName.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
Medium issues
src/main.ts:449still callsbuildPlaylistWithAI(...)without forwarding the CLI--devicevalue, and the orchestrator only auto-sends whenparams.playlistSettings.deviceNameis already present. In aff1 chat --device <name>build-and-send flow, the operator-selected device can still be dropped unless the model independently re-emits it.src/utilities/ff1-discovery.ts:288still treats avahi errors with no parsed stdout, including timeout cases, as a hard fallback to Bonjour. That leaves the Linux discovery path exposed on slow or busy scans, which is the same reliability gap this PR is trying to close.
Missing tests
- The new chat-routing coverage is still helper-level only; there is no end-to-end test for the real
buildPlaylist->processIntentParserRequest->buildPlaylistWithAIsend flow, so the device propagation issue above could regress unnoticed. - There is no regression test for
discoverFF1Devices()when avahi times out or returns no stdout, so the Linux fallback behavior is still unpinned.
…propagate --device to AI orchestrator
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 1, comment: 1, request-changes: 1)
Missing tests
- src/utilities/ff1-discovery.ts still lacks an end-to-end test for the Linux
process.platform === 'linux'branch indiscoverFF1Devices()and the Bonjour fallback decision. The new tests coverparseAvahiBrowseOutput()andresolveAvahiResult(), but not the actual branch that chooses avahi vs. Bonjour, so a regression in the platform gate or fallback wiring would still slip through.
I rechecked the reported setup retry leak and did not keep it: the finally block closes rl.
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, comment: 1, request-changes: 2)
Critical issues
- src/utilities/ff1-discovery.ts still returns
nullwhenavahi-browseexits non-zero and stdout parses to zero devices, which sends Linux back to Bonjour. That contradicts the contract aboveresolveAvahiResult()and can reintroduce the original missing-devices behavior on partial avahi output. No regression test covers the non-zero-exit / usable-stdout / zero-devices branch.
Medium issues
- src/utilities/device-lookup.ts disables TXT-name fallback for any stored row that already has an
id, even when the new discovery has neitherdiscoveredIdnor addresses. In the partial-avahi path, that prevents re-associating an existing device after a host change and pushesdevice addtoward duplicate entries or name-collision prompts. There is no test for the partial-avahi / no-id / no-addresses case. - index.ts plus src/utilities/device-lookup.ts still cannot update an existing IP-stored device when the user runs
ff1 device add --host <.local-url>and discovery data is unavailable. Because the new host is not an IP, the reverse-address lookup never runs, so the name-collision guard treats the existing row as a different device instead of updating it. No test covers the--hostmigration path.
I did not keep the IPv6 host:port concatenation concern because the current avahi/Bonjour discovery paths feed hostnames into those call sites, so I could not verify a live failure from this diff.
|
The avahi parser itself is mostly in decent shape. It handles multi-record merges, dual-stack addresses, partial TXT, and timeout behavior better than before. I do not think the parser is the main review risk anymore. The weak point is still identity reconciliation between “the device I just found” and “the row already in config.” The PR is trying to make device identity stable across host changes, but the final write path still does not preserve the specific matched row when the user changes the friendly name. That means the code is close, but not yet internally consistent.
|
…o prior match Two gaps identified in review: 1. Rename + host-change creates duplicate when device has no stored id. findExistingDeviceEntry can identify the row via address/hostname match, but upsertDevice was never told about it. Add a matchedIndex parameter; callers (device add + setup) now pass existingIndex so the correct row is always updated even when name and host both change in one step. 2. Manual IP → .local migration blocked by overly broad name-collision guard. When existingIndex=-1 (no discovery metadata to confirm identity), the guard was treating a same-name existing entry as a conflict and forcing a rename. Same-name + different-host is exactly the upsertDevice case-3 migration path, so the guard now only fires when existingIndex !== -1 (row is known; collision is with a genuinely different device).
feralfile-bot
left a comment
There was a problem hiding this comment.
Reviewers: 3 (approve: 0, request-changes: 3)
- Critical:
index.tsnow skips the duplicate-name guard wheneverfindExistingDeviceEntry()returns no match, and that is broader than the intended migration-only exception. In bothsetupanddevice add, a brand-new device that happens to reuse an existing friendly name can now fall through toupsertDevice()case 3 and overwrite the already-configured row in place. That is a data-loss regression, and it is the same underlying identity-reconciliation risk called out in the PR discussion and independently by all three reviewer reports. - Missing regression coverage: there is still no test that exercises the real CLI collision path where
existingIndex === -1and the chosen name matches another configured device. The current workflow tests document the old guard behavior, but they do not protect this newly introduced bypass, so the overwrite regression remains unguarded.
Overall verdict remains request changes until the collision guard is narrowed or restored and the missing regression test is added.
Summary
avahi-browsesubprocess, which reliably finds all devices.bonjour-servicewas silently missing devices on Linux due to multicast handling. Falls back to bonjour on macOS/Windows or if avahi isn't installed.device add: removed apiKey/topicID prompts — CLI is LAN-only for now; those fields aren't needed and users have no way to obtain themupsertDevice: no longer writes emptyapiKey/topicIDstrings to new device entrieschat --device: adds-d, --device <name>flag for parity withplay,send, andsshapiKey/topicIDfrom config example; adds--deviceto chat options; adds missingdeviceandsshsubcommands to public CLI docsRisk zone
Yellow — cross-surface changes to device discovery and multi-device routing. Anh and Brandon to review.
Test plan
ff1 device adddiscovers both devices on Linuxff1 device listshows both devices cleanly (no empty apiKey/topicID fields)ff1 chat --device kitchen "..."routes to the correct deviceff1 send playlist.json -d officestill worksnpm test)Related
🤖 Generated with Claude Code