{"_type":"fact","id":"Uq5TODxFqZrz","mind":"default","section":"Architecture","content":"styrened has three separate systems touching hub/network settings: (1) core-config.yaml reticulum/config for TCP interfaces, (2) HubConnection for LXMF propagation destination tracking, (3) TUI first-run config generator with hardcoded template","status":"active","created_at":"2026-03-01T14:09:41.816Z","source":"extraction","content_hash":"4c85fb6a5025e163","confidence":1,"last_reinforced":"2026-03-02T21:30:04.876Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ezPlrye3Ebpd","mind":"default","section":"Architecture","content":"The LXMF hub destination hash (6fc8bf22aa293588c9bf8d7488102e95) is hardcoded and separate from TCP transport interfaces - HubConnection.connect() uses RNS.Transport.has_path()/request_path() for LXMF store-and-forward, not for establishing TCP connections","status":"active","created_at":"2026-03-01T14:09:41.816Z","source":"extraction","content_hash":"f6a20e060dce1342","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uuaviEtN1oAV","mind":"default","section":"Architecture","content":"RNS TCPClientInterface peers are defined in core-config.yaml under reticulum.interfaces.peers, then generate_rns_config() writes them to ~/.reticulum/config which RNS reads at startup","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"8622eb5a788049e2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mNYQWi0EP6nh","mind":"default","section":"Architecture","content":"Planned Network tab scope: peer list management (add/remove/edit TCPClientInterface entries), AutoInterface on/off toggle, transport mode (standalone/shared, enable_transport), all writing back to core-config.yaml and regenerating ~/.reticulum/config","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"deec92c07a8722cf","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"p7XaOBBP1JOG","mind":"default","section":"Architecture","content":"get_profile_defaults() seeds config.reticulum.interfaces.peers with rns.styrene.io:4242 as a TCPClientInterface stanza","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"2801854573110680","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2IlvxvM0W6vA","mind":"default","section":"Architecture","content":"styrened uses async-first architecture with asyncio event loop, periodic tasks for announces and cleanup","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"7e59f53d2c5d5a12","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"U798NFh0-Vdw","mind":"default","section":"Architecture","content":"CI/CD runs on Argo Workflows on brutus K3s cluster, triggered by Argo Events GitHub webhooks. GitHub Actions removed.","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"41c37fa325857562","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ekcaAhKa7fQz","mind":"default","section":"Architecture","content":"Settings screen Network tab redesign is in progress: removing hub selector/KNOWN_HUBS/STYRENE_HUB_ADDRESS UI, replacing with TCPClientInterface peer manager (add/remove/edit), AutoInterface toggle, transport mode (DeploymentMode enum), and server interface config. Save writes both core-config.yaml and regenerates ~/.reticulum/config.","status":"active","created_at":"2026-03-01T14:11:25.101Z","source":"extraction","content_hash":"294873da05466afb","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"U_tD0JBToRUU","mind":"default","section":"Architecture","content":"CoreConfig model includes DeploymentMode enum (STANDALONE, HUB, etc.), PeerConfig (host, port, name), ServerInterfaceConfig (enabled, listen_ip, port), and InterfaceConfig (peers list, auto bool, server) under config.reticulum.interfaces","status":"active","created_at":"2026-03-01T14:11:25.101Z","source":"extraction","content_hash":"33a376bb86ae315f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"w8AOMPDMa-9z","mind":"default","section":"Architecture","content":"Config validation via validate_core_config() returns errors with field names — validates peer host non-empty, port in valid range, and hub mode requiring server or peer interfaces","status":"active","created_at":"2026-03-01T14:11:25.101Z","source":"extraction","content_hash":"2210d8e4ad786fcc","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kBfLZENfv-EK","mind":"default","section":"Architecture","content":"Config serialization uses _serialize_config() which converts CoreConfig to dict; PeerConfig entries without a name omit the name key from serialized output","status":"active","created_at":"2026-03-01T14:11:25.101Z","source":"extraction","content_hash":"22caa99b9d458533","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"sVhDMh36UoNH","mind":"default","section":"Architecture","content":"Settings screen Network tab now has 5 panels: TRANSPORT (mode, enable_transport, announce_interval), PEERS (dynamic add/remove TCPClientInterface rows), LOCAL DISCOVERY (AutoInterface toggle), SERVER (TCPServerInterface enable/IP/port), BATMAN-ADV MESH (existing mesh settings)","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"a0713e5bd4ab68bf","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":9,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"44d-ZThmImEt","mind":"default","section":"Architecture","content":"Settings save now persists network config to both core-config.yaml (via save_core_config) AND regenerates ~/.reticulum/config (via generate_rns_config + write_text) so RNS picks up changes on restart","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"a8cf8789f427e5d0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.877Z","reinforcement_count":7,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Zc2RRB2aRrd2","mind":"default","section":"Architecture","content":"KNOWN_HUBS dict, hub_select widget, custom hub address input, and hub announce interval have been removed from the settings screen - replaced by the interface manager panels","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"892454ddc17fea85","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wGF2zpkC6PfZ","mind":"default","section":"Architecture","content":"The LXMF hub address, hub selector (KNOWN_HUBS), and hub_announce_interval fields have been removed from the TUI settings screen as part of the Network tab redesign. Hub/propagation is handled separately from the interface manager.","status":"active","created_at":"2026-03-01T14:14:13.350Z","source":"extraction","content_hash":"87b218732c4df218","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"lWOe7XdFWUjf"}
{"_type":"fact","id":"-QsILc8Sa5fQ","mind":"default","section":"Architecture","content":"Peer list in Network tab now has column headers (Name/Host/Port) rendered as a .peer-header-row of Static widgets with .peer-header CSS class above the dynamic peer rows","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"8bc222448cfeb4d1","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"375ZhDD5LQWn","mind":"default","section":"Architecture","content":"styrene --upgrade CLI runs pipx upgrade styrene (or pip install --upgrade) then pkill -f styrened daemon. styrene --reset backs up configs as .bak, regenerates core-config.yaml and ~/.reticulum/config from defaults, and kills daemon.","status":"active","created_at":"2026-03-01T14:55:06.497Z","source":"extraction","content_hash":"c79153e5a550005f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"o9AbDW_Xh3sU","mind":"default","section":"Architecture","content":"TUI app's _show_upgrade_screen callback calls _restart_tui() which uses os.execvp to re-exec the process. _restart_tui() finds the styrene entry point (argv0 or shutil.which), calls self.exit(), then registers atexit handler to exec after Textual teardown.","status":"active","created_at":"2026-03-01T15:02:17.862Z","source":"extraction","content_hash":"729638e1b612f49e","confidence":1,"last_reinforced":"2026-03-04T01:01:26.922Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"UpefLxTl9UZB"}
{"_type":"fact","id":"mzN9U6GNx58S","mind":"default","section":"Architecture","content":"UpgradeScreen upgrade flow: _build_upgrade_cmd() detects pipx vs pip and uses --upgrade-strategy=eager to force transitive dep upgrades. _do_upgrade() runs subprocess in @work thread with 120s timeout, streams output to Log widget, then kills daemon via pkill. Dismiss returns bool to app callback.","status":"active","created_at":"2026-03-01T15:10:06.595Z","source":"extraction","content_hash":"adb05f4d15b1b0a7","confidence":1,"last_reinforced":"2026-03-03T20:06:00.606Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"ZIMMr0UpcAXS"}
{"_type":"fact","id":"T_lxIK5t2Ixh","mind":"default","section":"Architecture","content":"Settings screen tab navigation uses [ and ] screen-level keyboard bindings that delegate to TabbedContent.action_previous/next_tab, bypassing arrow key consumption by Input widgets inside tab panes (v0.10.46)","status":"active","created_at":"2026-03-01T15:57:41.286Z","source":"extraction","content_hash":"09e43d3e806389f8","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"UG-cXvhjwYxw","mind":"default","section":"Architecture","content":"UpgradeScreen._build_upgrade_cmd() passes --upgrade-strategy=eager for both pipx and pip paths. For pipx, uses '--pip-args=--upgrade-strategy=eager' as single combined argument (fixed from two separate args that pipx couldn't parse).","status":"active","created_at":"2026-03-01T15:59:52.149Z","source":"extraction","content_hash":"0acb6b58ab92efef","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"U-t0iyBbfmDP"}
{"_type":"fact","id":"1VQ2IuyXzjU7","mind":"default","section":"Architecture","content":"RNS interfaces have .online attribute for connection status, accessible via RNS.Transport.interfaces — rns_service._log_interface_status() and _last_interface_states dict track UP/DOWN state changes at runtime","status":"active","created_at":"2026-03-01T16:23:55.405Z","source":"extraction","content_hash":"3e3e8708ab220621","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"iN3rsjQmfqYS","mind":"default","section":"Architecture","content":"TUI has 19+ files with inline CSS (CSS = or DEFAULT_CSS =): screens (upgrade, contacts, conversation, exploration, inbox, provision, confirm_flash, mesh_device_detail) and widgets (chat_widget, config_form, activity_feed, node_info_panel, hardware_panel, message_bubble, image_preview, uptime_panel, highlighted_panel, page_browser, forge_log). Main theme CSS lives in imperial_crt.tcss.","status":"active","created_at":"2026-03-01T16:32:58.196Z","source":"extraction","content_hash":"4a1880928a66ee50","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IOE00P3Zafkh","mind":"default","section":"Architecture","content":"Textual ColorSystem has limited built-in theme variables (primary, secondary, warning, error, success, accent, foreground, background, surface, panel, boost, text_alpha). Custom variables are added via the 'variables' dict parameter. styrene_brand.py adds: border, border-blurred, block-cursor-*, footer-*, input-*, scrollbar*, and text-muted.","status":"active","created_at":"2026-03-01T16:35:01.630Z","source":"extraction","content_hash":"5d8d5da292c68a13","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"z_9_RmKqfCz3","mind":"default","section":"Architecture","content":"Settings screen uses Switch (not Checkbox) for all toggles as of v0.10.52. Switch renders as a sliding toggle. CSS class .setting-checkbox styles it borderless/transparent/compact. Save handler queries Switch widgets, not Checkbox. Tests use Switch import.","status":"active","created_at":"2026-03-01T16:37:43.866Z","source":"manual","content_hash":"95eee3c8451bb6b6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"poD7G9a2kJxN","mind":"default","section":"Architecture","content":"Custom Textual theme variable $text-muted added to styrene_brand.py ColorSystem variables dict, mapped to STYRENE_DARK['muted_foreground'] (#9ca3af) — provides readable muted text color for descriptions and headers. Used in imperial_crt.tcss for .setting-description and .peer-header.","status":"active","created_at":"2026-03-01T16:37:44.940Z","source":"extraction","content_hash":"ac70a6e13f0ea03d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"AfdAxa9U3CL9"}
{"_type":"fact","id":"t3tVAYa5vr1Y","mind":"default","section":"Architecture","content":"TUI scrollbar architecture (v0.10.54): Screen-level global scrollbar rules (scrollbar-background:$surface, scrollbar-color:$border, size:1). Blanket transparent background rule for VerticalScroll/ScrollableContainer/RichLog/Log/TextArea. Individual widgets (ActivityFeed, ForgeLog, ChatWidget) have explicit scrollbar styling in DEFAULT_CSS. All scrollable containers must set background+scrollbar-track+scrollbar-thumb.","status":"active","created_at":"2026-03-01T16:37:44.941Z","source":"extraction","content_hash":"34b8837ad05e85e5","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MbUPeiYB98tF","mind":"default","section":"Architecture","content":"styrene-pypi sync-version.yml now builds and publishes to PyPI directly (not via release event cascade). GITHUB_TOKEN-created releases cannot trigger other workflows (GitHub anti-cascade). The workflow: detect new styrened on PyPI → bump pyproject.toml → commit+tag → build wheel → publish to PyPI → create GitHub release. Cron hourly + repository_dispatch from Argo.","status":"active","created_at":"2026-03-01T16:37:57.954Z","source":"manual","content_hash":"94c14a4fd3771296","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wK8k8XuPIt9A","mind":"default","section":"Architecture","content":"Local PyPI publish path added: justfile 'publish' recipe runs .venv/bin/python -m build + .venv/bin/python -m twine upload dist/* for immediate availability instead of waiting for Argo CI","status":"active","created_at":"2026-03-01T16:48:45.058Z","source":"extraction","content_hash":"3fa07d2088863471","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"RsDownXhS2MC","mind":"default","section":"Architecture","content":"Argo release-build.yaml uses twine upload --skip-existing, so local pre-publishing via 'just publish' doesn't break the Argo pipeline — it silently skips the already-uploaded wheel and continues to OCI build and GitHub Release steps","status":"active","created_at":"2026-03-01T16:56:44.749Z","source":"extraction","content_hash":"58467c403bd8ebeb","confidence":1,"last_reinforced":"2026-03-02T21:30:04.878Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"TJiVd6ke9CaQ","mind":"default","section":"Architecture","content":"RNS announce callback chain is being investigated to determine if the receiving interface name is available in announce handlers — needed for correlating discovered devices to their source hub/peer for hierarchical display","status":"active","created_at":"2026-03-01T17:02:47.647Z","source":"extraction","content_hash":"2e7194b125580ede","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qjEGj_HqxZWZ","mind":"default","section":"Architecture","content":"RNS announce handler callback signature is fixed at 3-5 params (destination_hash, announced_identity, app_data, announce_packet_hash, is_path_response) — receiving interface is NOT passed to handlers. However, RNS.Transport.path_table[destination_hash][5] stores the receiving_interface object post-announce, with .name attribute matching config stanza name.","status":"active","created_at":"2026-03-01T17:04:59.719Z","source":"extraction","content_hash":"ae6bd31f7e7f3c86","confidence":1,"last_reinforced":"2026-03-03T03:08:26.505Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-b9sl7K3raMy","mind":"default","section":"Architecture","content":"RNS.Transport dispatches announce handlers via inspect.signature() parameter count detection (3, 4, or 5 params) and runs each handler in a separate daemon thread via threading.Thread(target=job, daemon=True).start()","status":"active","created_at":"2026-03-01T17:04:59.719Z","source":"extraction","content_hash":"32089722f08613a6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MqRriGkByUyR","mind":"default","section":"Architecture","content":"NodeStore SQLite schema uses ALTER TABLE migrations for new columns (try/except OperationalError pattern) — lxmf_destination_hash, short_name, system_fingerprint were added this way. Same pattern will be used for discovered_via column.","status":"active","created_at":"2026-03-01T17:04:59.720Z","source":"extraction","content_hash":"2097f468bfaa116c","confidence":1,"last_reinforced":"2026-03-04T05:19:57.259Z","reinforcement_count":7,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"opuD4Ehx5HJB","mind":"default","section":"Architecture","content":"Dashboard screen replaced flat MeshDeviceTable(DataTable) with MeshDeviceTree(Tree) widget that groups discovered devices hierarchically by their discovered_via interface name. Interface branches are auto-expanded, leaf nodes carry device identity as data for selection/navigation.","status":"active","created_at":"2026-03-01T17:07:02.621Z","source":"extraction","content_hash":"b1aa22a3fafe057d","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IFPmw3xOIRHR","mind":"default","section":"Architecture","content":"MeshDevice.discovered_via field populated in StyreneAnnounceHandler.received_announce() by looking up RNS.Transport.path_table[destination_hash][5].name — index 5 is the receiving interface object with a .name attribute matching the config stanza name.","status":"active","created_at":"2026-03-01T17:07:02.621Z","source":"extraction","content_hash":"fd19d541f5f3725b","confidence":1,"last_reinforced":"2026-03-03T03:08:26.505Z","reinforcement_count":7,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"5mSntYh8qbG8","mind":"default","section":"Architecture","content":"NodeStore 'nodes' table has discovered_via TEXT column (ALTER TABLE migration pattern) with idx_discovered_via index. UPSERT uses COALESCE(excluded.discovered_via, nodes.discovered_via) to preserve existing interface info when new announce lacks it.","status":"active","created_at":"2026-03-01T17:07:02.621Z","source":"extraction","content_hash":"19dbfcc6369178dd","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"153n4B2chIOE","mind":"default","section":"Architecture","content":"Hierarchical mesh device tree implemented in dashboard — MeshDeviceTree(Tree) widget replaces flat DataTable, grouping discovered devices under their source interface (e.g., 'Styrene Community Hub' branch → child devices, 'Direct / Unknown' → devices without interface info). Uses MeshDevice.discovered_via field populated from RNS.Transport.path_table[dest_hash][5].name post-announce.","status":"active","created_at":"2026-03-01T17:07:02.621Z","source":"extraction","content_hash":"c86858a99cbb50a6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"E9NoYuwO1BRN"}
{"_type":"fact","id":"RwOLhx-ubv6g","mind":"default","section":"Architecture","content":"NodeStore has 'paths' table with interface_type and interface_name columns per destination_hash. The 'nodes' table now has a discovered_via TEXT column (added via ALTER TABLE migration) with idx_discovered_via index for efficient dashboard Tree grouping. UPSERT uses COALESCE to preserve existing interface info.","status":"active","created_at":"2026-03-01T17:07:02.622Z","source":"extraction","content_hash":"0766018ef0963b94","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"Ji8DQ4-sZyAm"}
{"_type":"fact","id":"TF3RRF5qEgP1","mind":"default","section":"Architecture","content":"DashboardScreen event handling changed from on_data_table_row_selected(DataTable.RowSelected) to on_tree_node_selected(Tree.NodeSelected[str]) for device detail navigation. Tree node .data carries device identity string; None data on branch/empty nodes is ignored.","status":"active","created_at":"2026-03-01T17:07:02.622Z","source":"extraction","content_hash":"7233c12e086da9f0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"4RXOJ4iHHfX2","mind":"default","section":"Architecture","content":"Dashboard MESH DEVICES panel uses Tree widget (MeshDeviceTree) instead of DataTable as of v0.10.59. Devices grouped by discovered_via interface name. MeshDevice.discovered_via populated from RNS.Transport.path_table[destination_hash][5].name in the announce handler (post-hoc lookup — RNS doesn't pass interface to announce callbacks). nodes table has discovered_via column + idx_discovered_via index. COALESCE preserves existing value when re-announce has no interface info.","status":"active","created_at":"2026-03-01T17:08:56.306Z","source":"manual","content_hash":"c119e5ffb93e0673","confidence":1,"last_reinforced":"2026-03-03T01:28:27.727Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tRZrT5aFLbPB","mind":"default","section":"Architecture","content":"NodeInfoPanel renders in two-column layout as of v0.10.61. Left column: SYSTEM (CPU/RAM/NET/STORAGE), DAEMON (IPC mode only), IDENTITY (name/alias/hash/PQC tier). Right column: RETICULUM (RNS status/uplink/interfaces), STYRENE (mode/hub/mesh count), VERSION (styrened __version__). Uses Rich markup with regex-based padding for side-by-side rendering within a single Static widget. render() delegates to _render_left_column() and _render_right_column() methods. Located at src/styrened/tui/widgets/node_info_panel.py.","status":"active","created_at":"2026-03-01T17:32:26.917Z","source":"extraction","content_hash":"fac992dcf82aa8f9","confidence":1,"last_reinforced":"2026-03-04T04:45:00.313Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":"Q3U1SpdZfWRP"}
{"_type":"fact","id":"S6PVVxtYoTu-","mind":"default","section":"Architecture","content":"HighlightedPanel (Vertical) composes: hp-top Static (1 line) + hp-content Vertical + hp-bottom Static (1 line). hp-content default is height:auto (safe for modals/settings). Dashboard panels (#mesh-devices-panel, #activity-feed-panel) override hp-content to height:1fr via CSS specificity. Box-drawing borders rendered via Rich markup with ColorCascade hex colors.","status":"active","created_at":"2026-03-01T17:47:39.092Z","source":"extraction","content_hash":"a9cbd3c7223d8b1b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"WEl1qKfZ0Hg7"}
{"_type":"fact","id":"yjb77o3W_fJN","mind":"default","section":"Architecture","content":"styrened TUI uses Textual 8.0.0 as of 2026-03-01","status":"active","created_at":"2026-03-01T17:51:00.465Z","source":"extraction","content_hash":"d8edb55767fa2642","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wYrwWwosmZgU","mind":"default","section":"Architecture","content":"Two identity systems coexist: (1) RNS operator identity — cryptographic keypair at reticulum.operator_identity_path, identity hash for routing, (2) Styrene IdentityConfig — display_name, icon, short_name for human-readable appearance. Both stored in core-config.yaml under different sections (reticulum vs identity). Investigation ongoing to ensure they don't conflict.","status":"active","created_at":"2026-03-01T17:58:58.721Z","source":"extraction","content_hash":"ae8cee59a14e3a43","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"A-Y4FEgvo6qE","mind":"default","section":"Architecture","content":"YubiKey identity provider uses FIDO2 hmac-secret extension to derive deterministic 64-byte RNS identity (X25519 + Ed25519) from hardware token. Two application-specific salts (styrene-encryption-v1, styrene-signing-v1) produce independent 32-byte outputs via getAssertion. Credential ID stored in config as base64. Located at src/styrened/services/yubikey.py. Requires pip install styrened[yubikey] (python-fido2>=1.0).","status":"active","created_at":"2026-03-01T18:02:39.951Z","source":"extraction","content_hash":"870a5f840c67fc48","confidence":1,"last_reinforced":"2026-03-02T21:30:04.879Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cnBGWeIQZ9AK","mind":"default","section":"Architecture","content":"Identity provider selection in _resolve_identity_path(): priority 0 is YubiKey (if identity_config.provider == 'yubikey'), then config override path, system /etc/styrene/identity, user ~/.config/styrene/identity, LXMF app detection, and finally new generation. YubiKey path calls derive_identity_bytes() and RNS.Identity.from_bytes() inline, returning hex hash directly without writing to disk.","status":"active","created_at":"2026-03-01T18:02:39.951Z","source":"extraction","content_hash":"4bc86161a0df2233","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"FJXLts2VyhWr","mind":"default","section":"Architecture","content":"get_operator_identity() and get_operator_identity_object() now delegate to _load_identity_config() which loads IdentityConfig from core-config.yaml, then check provider field. YubiKey provider calls derive_identity_bytes() inline; file provider falls through to _resolve_identity_path() + from_file(). get_operator_identity() wraps get_operator_identity_object() with hash extraction and exception handling.","status":"active","created_at":"2026-03-01T18:06:40.422Z","source":"extraction","content_hash":"a3c56f86318ed4d1","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Uz5hINmnXdRm","mind":"default","section":"Architecture","content":"Well-known RNS community hubs include: dublin.connect.reticulum.network:4965 (Dublin Hub), reticulum.betweentheborders.com:4242 (BetweenTheBorders), istanbul.reserve.network:9034, node01.rns.bnz.se:4242, rns.stoppedcold.com:4242, rtclm.de:4242, sydney.reticulum.au:4242. These are being added alongside rns.styrene.io:4242 as default/selectable peer interfaces.","status":"active","created_at":"2026-03-01T18:16:43.038Z","source":"extraction","content_hash":"d0c55fd1adc5737c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ddFl-sonixWn","mind":"default","section":"Architecture","content":"PeerConfig model now has an 'enabled' field (default True) — parsed from core-config.yaml via _parse_bool(p.get('enabled', True)), serialized as 'enabled' key in _serialize_config(), and used by generate_rns_config() to skip disabled peers (continue if not peer.enabled).","status":"active","created_at":"2026-03-01T18:18:24.438Z","source":"extraction","content_hash":"dca3b7b770c2c3ba","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"HLYOQXYPPpdT","mind":"default","section":"Architecture","content":"TUI peer rows replaced static status indicator (● dot with CSS color classes) with a Switch toggle (peer_enabled_{i}) that controls the PeerConfig.enabled field. Switch uses .setting-checkbox and .peer-enabled-toggle CSS classes. Peer enabled state persists to core-config.yaml and controls whether the peer is written to ~/.reticulum/config.","status":"active","created_at":"2026-03-01T18:18:24.438Z","source":"extraction","content_hash":"db385eb9cb4e288a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"YcqzErOTVR0q"}
{"_type":"fact","id":"NlE5bJSK6iWO","mind":"default","section":"Architecture","content":"WELL_KNOWN_HUBS constant defined in models/config.py — list of 5 PeerConfig entries (Styrene Community Hub enabled, 4 community hubs disabled by default). Used by both daemon (services/config.py) and TUI (tui/services/config.py) config generators to seed new installs.","status":"active","created_at":"2026-03-01T18:22:18.978Z","source":"extraction","content_hash":"eb367938465a72f0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"sfmHtLWC4XTZ","mind":"default","section":"Architecture","content":"Config loaders (daemon services/config.py and TUI tui/services/config.py) merge missing WELL_KNOWN_HUBS as disabled entries after parsing peers list. Keyed by (host, port) tuple to avoid duplicates. Existing peers are never modified — only missing hubs are appended with enabled=False. This ensures existing installs see well-known hubs in Settings without manual addition.","status":"active","created_at":"2026-03-01T18:26:14.275Z","source":"extraction","content_hash":"0fd0ff80edd8521b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pz__bKqrNGH3","mind":"default","section":"Architecture","content":"MeshDeviceTree CSS changed in latest fix: .tree--cursor background changed from $surface to $background (eliminates visible gray shadow bar on selected row), .tree--highlight changed from $background to transparent (hover no longer shows background change). Selected row identified by $primary color + bold text only.","status":"active","created_at":"2026-03-02T03:34:35.544Z","source":"extraction","content_hash":"c6e21d886399a40f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Z51ceomf0VYF","mind":"default","section":"Architecture","content":"StyreneConfig.identity is a property that proxies to StyreneConfig.core.identity. The TUI load path calls _overlay_core_identity() which reads core config via load_core_config() and replaces config.core.identity with the loaded IdentityConfig. Settings screen Input widgets are populated from self.config.identity.display_name at compose() time.","status":"active","created_at":"2026-03-02T03:35:51.713Z","source":"extraction","content_hash":"7906e30af8c249f6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.880Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"JHYTlaRoACfp","mind":"default","section":"Architecture","content":"No image/media handling exists in StyreneEnvelope wire protocol yet — image attachment support is greenfield. No content_type, attachment, or media fields in models/styrene_wire.py or protocols/.","status":"active","created_at":"2026-03-02T04:07:00.697Z","source":"extraction","content_hash":"cc9c2340472c8041","confidence":1,"last_reinforced":"2026-03-02T22:50:54.651Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"7nUnLzAqcMxV","mind":"default","section":"Architecture","content":"Wire protocol image attachment design for StyreneEnvelope: MediaAttachment struct with content_type, data, original_type (for lossless JPEG repack tracking), thumbnail (progressive prefix), width, height. Defined in docs/image-format-assessment.md in styrene-rs repo. Vec<MediaAttachment> on envelope.","status":"active","created_at":"2026-03-02T04:14:33.285Z","source":"extraction","content_hash":"9dd99c5953b581d0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8LOBy5r9cN49","mind":"default","section":"Architecture","content":"Image attachment design for StyreneEnvelope finalized: MediaAttachment(content_type, data, original_type, thumbnail, width, height). JXL as native format with JPEG fallback. original_type tracks lossless JPEG repacks for reversible extraction. Target 15-50KB per image. Progressive decode for mesh UX (2KB→1/8 res, 10KB→1/4 res, full). Design doc at styrene-rs/docs/image-format-assessment.md.","status":"active","created_at":"2026-03-02T04:14:33.286Z","source":"extraction","content_hash":"54feba4c78585cd9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"257d_Vf2qZje"}
{"_type":"fact","id":"TWEAKniyhHcL","mind":"default","section":"Architecture","content":"JXL Rust crate ecosystem (2026-03): jxl-oxide 0.12 (pure Rust decoder, active, modular sub-crates), zune-jpegxl 0.5 (pure Rust encoder, early), jxl-encoder 0.1 (pure Rust, early), jpegxl-rs 0.13 (libjxl C bindings). For styrene-mesh, jxl-oxide is primary decode, jpegxl-rs or zune-jpegxl for encode (TBD evaluation).","status":"active","created_at":"2026-03-02T04:14:33.286Z","source":"extraction","content_hash":"7c80dc9e9afa241b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wu7PaBuWPent","mind":"default","section":"Architecture","content":"Python-Rust binding landscape: PyO3 (v0.23+, production-grade, standard choice — used by Polars, ruff, Pydantic V2) + Maturin (v1.8+, build backend for PyO3→wheels) is the clear winner. Also: pyo3-asyncio (Rust futures↔asyncio), rust-numpy (zero-copy ndarray), uniffi (Mozilla multi-lang IDL). Potential future use: styrene-imagecodecs PyO3 crate wrapping jxl-oxide to eliminate libjxl system dep for Python.","status":"active","created_at":"2026-03-02T04:14:33.286Z","source":"extraction","content_hash":"0bc409b00227a6ec","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6IKsYcfhmvWA","mind":"default","section":"Architecture","content":"JXL lossless JPEG repack flow for Styrene: phone sends JPEG → Styrene wraps to JXL losslessly (20% smaller) with original_type='image/jpeg' → transmit over mesh → receiver can decode JXL or extract bit-exact original JPEG for non-Styrene clients. Zero quality loss, reversible.","status":"active","created_at":"2026-03-02T04:14:33.287Z","source":"extraction","content_hash":"a4f3877f1e30f0e0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"dwwASn4CoPJE","mind":"default","section":"Architecture","content":"Incremental Rust migration strategy via PyO3 FFI proposed in styrene-rs/docs/incremental-rust-migration.md. Two integration modes: (1) wire interop (independent Rust daemon over LXMF), (2) FFI hybrid (Rust modules exposed via PyO3 as styrene-native package, imported by Python daemon as drop-in replacements). Uses try/except ImportError pattern for graceful fallback to pure Python.","status":"active","created_at":"2026-03-02T04:17:53.410Z","source":"extraction","content_hash":"c59a8a2975a44634","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"--a96OUbXUMV","mind":"default","section":"Architecture","content":"styrene-native PyO3 crate planned at crates/bindings/styrene-native/ — cdylib extension module built with maturin, re-exports from styrene-mesh/styrene-rns/styrene-lxmf crates. Installed via pip install styrened[native]. Falls back to pure Python when not installed.","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"22c1ade422b9d09f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9ghXfO1W9PX0","mind":"default","section":"Architecture","content":"Dual-path compatibility verification strategy: pytest fixture with params=['python','rust'] runs every test twice against both implementations. Any behavioral divergence caught immediately. Impossible with wire-only approach. Feature flag STYRENE_NATIVE=0 forces pure Python in production.","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"c0c1b55bbdc860e2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"LQ8sJa3QzaoL","mind":"default","section":"Architecture","content":"styrene-rs has two integration modes with styrened: (1) Wire interop — independent Rust daemon communicates over LXMF mesh (styrened-rs binary), (2) FFI hybrid — Rust modules exposed via PyO3 (styrene-native package) imported by Python daemon as drop-in replacements. Wire protocol (StyreneEnvelope in styrene-mesh crate) remains the shared contract for mode 1. Mode 2 enables incremental migration.","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"afd2c30a18972207","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"iY5acZdBG7h2"}
{"_type":"fact","id":"1S16utapwHAN","mind":"default","section":"Architecture","content":"styrene-rs PARITY_GAPS.md shows ~37K Rust LOC across 6 crates with 212 tests. Missing service layer (~12K LOC): conversation service, auto-reply, node store persistence, remote terminal, protocol registry, file transfer, hub connection. Critical gaps: IFAC multi-hop bug, Serial/KISS interface, propagation store-and-forward, config system.","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"dc6efef90f9120bf","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wweAyUK1QmpK","mind":"default","section":"Architecture","content":"styrene-native PyO3 crate scaffolded at crates/bindings/styrene-native/ in styrene-rs repo. Cargo.toml defines cdylib crate type with pyo3 0.23 + extension-module feature, depending on styrene-mesh and styrene-rns path deps. pyproject.toml uses maturin>=1.0 build backend. Package name: styrene-native, lib name: styrene_native.","status":"active","created_at":"2026-03-02T04:34:19.768Z","source":"extraction","content_hash":"5038339cc8128387","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uD6dsd3TSzCp","mind":"default","section":"Architecture","content":"styrene-mesh crate's wire protocol uses StyreneMessage struct (not StyreneEnvelope) with encode()/decode() methods. Wire format: 10-byte namespace 'styrene.io' + 1-byte version (0x01) + 1-byte type enum + 16-byte request_id + variable msgpack payload. Header is 28 bytes. StyreneMessageType enum covers Control, Status, RPC Commands/Responses, Terminal Sessions, and PQC Sessions ranges.","status":"active","created_at":"2026-03-02T04:34:19.770Z","source":"extraction","content_hash":"7c7946814a3e36e4","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ukjlOo-2RcGW","mind":"default","section":"Architecture","content":"Python styrened service layer LOC breakdown: reticulum.py 1407, conversation_service.py 1316, lxmf_service.py 1114, config.py 1075, node_store.py 986, doctor.py 753, rns_service.py 653, file_transfer.py 637, auto_reply.py 634, page_browser.py 524, hardware.py 518, pqc_session.py 424, attachment_store.py 359, hub_connection.py 342. Total services ~12.5K LOC. Models total ~17K LOC (styrene_wire.py alone is 1124).","status":"active","created_at":"2026-03-02T04:34:19.770Z","source":"extraction","content_hash":"f25d6f96d8531a32","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"0lbCYbsiavbx","mind":"default","section":"Architecture","content":"StyreneApp.action_interrupt() implements double ctrl+c quit pattern: single ctrl+c pops screen stack toward dashboard, second ctrl+c within 1.0s window exits app. Uses time.monotonic() tracking via _last_interrupt class var. Binding uses priority=True to bypass focused widget key consumption.","status":"active","created_at":"2026-03-02T04:49:35.893Z","source":"extraction","content_hash":"924924122b7fe7de","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"90I9AxncjdHV","mind":"default","section":"Architecture","content":"LXMF propagation destination populated in TUI settings from config.core.lxmf.propagation_destination. Community Hub Switch toggle (#community_hub_enabled) auto-fills propagation_destination Input with COMMUNITY_HUB_PROPAGATION_HASH on enable, clears on disable (only if it matches the community hash). Empty propagation field despite enabled hub suggests config.core.lxmf.propagation_destination is None/empty at load time.","status":"active","created_at":"2026-03-02T04:49:35.893Z","source":"extraction","content_hash":"f8cb1a51a175322d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.881Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8CIeG131Vkmb","mind":"default","section":"Architecture","content":"TUI config loading changed from _overlay_core_identity() (identity-only) to _overlay_core_config() which replaces entire config.core with load_core_config() result. This means core-config.yaml is the single source of truth for ALL CoreConfig fields (identity, reticulum, lxmf, rpc, discovery, api). tui.yaml still contains a stale copy of reticulum/rpc/etc sections that gets parsed then overwritten by the overlay.","status":"active","created_at":"2026-03-02T04:51:08.865Z","source":"extraction","content_hash":"75571d5de52244ec","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":7,"decay_rate":0.05,"supersedes":"isv_0_cOBmYi"}
{"_type":"fact","id":"Dl4ikdUfBDZF","mind":"default","section":"Architecture","content":"_overlay_core_config() in tui/services/config.py replaces entire config.core with load_core_config() result. core-config.yaml is the single source of truth for all CoreConfig fields. Identity is stored in core-config.yaml, settings screen _save_identity() writes there first, then updates self.config.core.identity in memory so subsequent save_core_config() preserves new values.","status":"active","created_at":"2026-03-02T04:51:08.866Z","source":"extraction","content_hash":"74cad9b9d85479e8","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":7,"decay_rate":0.05,"supersedes":"KzRlTinsSTSG"}
{"_type":"fact","id":"TvTzpVMhFty-","mind":"default","section":"Architecture","content":"MeshDeviceTree does NOT filter LOST nodes — all device statuses (ACTIVE, STALE, LOST) are displayed with different status symbols (● active, ◐ stale, ○ lost). Previous DataTable implementation filtered LOST nodes out.","status":"active","created_at":"2026-03-02T05:05:38.494Z","source":"extraction","content_hash":"b450c18797972b1c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2f-1sgN1i33P","mind":"default","section":"Architecture","content":"_deduplicate_by_identity() in services/reticulum.py both deduplicates by identity_hash AND filters out devices with last_announce older than 30 minutes (cutoff = now - 1800). LOST devices are not filtered by status — they're filtered by age. Devices <30min old with LOST status still appear in the tree.","status":"active","created_at":"2026-03-02T05:15:29.117Z","source":"extraction","content_hash":"446fc4fd3dd5c07d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6UCcmqukjjEq","mind":"default","section":"Architecture","content":"_parse_config_dict in tui/services/config.py now parses 'identity' and 'lxmf' sections from config.yaml/tui.yaml in addition to tui/fleet/provisioning/mesh/reticulum/rpc sections. Identity parsed only if display_name is non-empty (empty string keeps default 'Anonymous Styrene').","status":"active","created_at":"2026-03-02T06:54:39.865Z","source":"extraction","content_hash":"83d48a6025c91fe2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"hvlbnUX6g1id","mind":"default","section":"Architecture","content":"StyreneConfig._parse_config_dict parses: tui, fleet, provisioning, mesh, reticulum (interfaces/peers/hub), rpc, identity (display_name/icon/short_name if non-empty), and lxmf sections. Identity from config.yaml is now read but may be superseded by _overlay_core_config() if core-config.yaml has a real identity.","status":"active","created_at":"2026-03-02T06:54:39.865Z","source":"extraction","content_hash":"534753eaad9046f1","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"4J8HSGZl8Xqn"}
{"_type":"fact","id":"WM6vn1-b5Ceu","mind":"default","section":"Architecture","content":"ContactsScreen DataTable was restructured: columns changed from ALIAS/PEER HASH/NOTES to ALIAS/STATUS/LAST MESSAGE/PEER HASH. Now cross-references IPCBridge get_devices() and get_conversations() to show live presence (● online, ◐ stale, ○ unknown) and last message preview with relative time. ContactInfo.to_dict() exists on the service DTO (services/contacts.py ContactInfo dataclass), not on the SQLAlchemy Contact model (models/contacts.py).","status":"active","created_at":"2026-03-02T22:28:33.915Z","source":"extraction","content_hash":"f17359addc3d42e7","confidence":1,"last_reinforced":"2026-03-02T22:34:26.752Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"6QxCnItg_Wbm"}
{"_type":"fact","id":"lfAxg9e_TBDT","mind":"default","section":"Architecture","content":"ContactInfo (services/contacts.py) is a dataclass DTO with to_dict() and from_model() methods. Contact (models/contacts.py) is the SQLAlchemy model with no to_dict(). These are two distinct classes — ContactService returns ContactInfo instances.","status":"active","created_at":"2026-03-02T22:28:33.915Z","source":"extraction","content_hash":"b7859a767a74bbb6","confidence":1,"last_reinforced":"2026-03-02T22:28:33.915Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-TrkH0XUNfKw","mind":"default","section":"Architecture","content":"DeviceInfo (ipc/messages.py) has lxmf_destination_hash and destination_hash fields. ContactsScreen presence lookup matches contact peer_hash against both device fields to find the corresponding device.","status":"active","created_at":"2026-03-02T22:28:33.916Z","source":"extraction","content_hash":"a25b9ce286e16758","confidence":1,"last_reinforced":"2026-03-02T22:34:26.752Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WMC34awwn9d1","mind":"default","section":"Architecture","content":"styrened.tui.menubar subpackage added at src/styrened/tui/menubar/ — macOS menu bar agent showing unread count, recent conversations, and daemon status via rumps NSApplication loop. IPC polling runs in a background asyncio thread. Architecture: rumps event loop on main thread, asyncio IPC loop in daemon thread, shared state via MenuBarState dataclass with threading.Lock.","status":"active","created_at":"2026-03-02T22:34:26.750Z","source":"extraction","content_hash":"4cc7c09485b69ceb","confidence":1,"last_reinforced":"2026-03-02T22:50:54.652Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kswBkTW6R71D","mind":"default","section":"Architecture","content":"MenuBar agent uses osascript for native macOS notifications (display notification command via subprocess.Popen) rather than pyobjc or native APIs — lightweight, no additional deps.","status":"active","created_at":"2026-03-02T22:34:26.751Z","source":"extraction","content_hash":"944e42e52c2603db","confidence":1,"last_reinforced":"2026-03-02T22:50:54.652Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Tn16tvbjAdhC","mind":"default","section":"Architecture","content":"ChatWidget._pending_attachment dict tracks staged attachments with keys: awaiting_path (bool), data_b64, filename, mime. Attachment sent on next message submission.","status":"active","created_at":"2026-03-02T22:50:54.651Z","source":"extraction","content_hash":"55999f38976d19ca","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"OXxQX1WvodOP","mind":"default","section":"Architecture","content":"rumps added to pyproject.toml [tui] extras with sys_platform='darwin' marker. Import in agent.py still has friendly ImportError message as fallback.","status":"active","created_at":"2026-03-02T22:50:54.652Z","source":"extraction","content_hash":"70dea4e8ff10966f","confidence":1,"last_reinforced":"2026-03-02T22:50:54.652Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"YfqSEV81HTpk"}
{"_type":"fact","id":"IWXTnW_gsx1E","mind":"default","section":"Architecture","content":"Clipboard integration module added at src/styrened/tui/menubar/clipboard.py — reads image data and file references from macOS clipboard using PyObjC AppKit. Priority: PNG image data → TIFF image data → file URL references (Finder copy) → text that looks like a file path. Returns ClipboardAttachment dataclass with data_b64, filename, mime, size, source fields.","status":"active","created_at":"2026-03-02T22:52:57.808Z","source":"extraction","content_hash":"19cdd851dff6bc44","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"BXqtkunxNxg3","mind":"default","section":"Architecture","content":"ChatWidget on_paste() handler detects clipboard image data (screenshots via NSPasteboardTypePNG/TIFF) before falling back to file path text. Calls _stage_from_clipboard() for image data, _stage_attachment_from_path() for path text. Supported path file types expanded beyond images to include pdf, txt, md, zip, tar, gz, mp3, wav, ogg, mp4, mov.","status":"active","created_at":"2026-03-02T22:52:57.808Z","source":"extraction","content_hash":"6c4a595682c4432b","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"030IVD-_lF9x"}
{"_type":"fact","id":"1qbFOiFjBDfa","mind":"default","section":"Architecture","content":"action_attach_file (a key) now tries clipboard first via read_clipboard_attachment() before prompting for a file path. action_paste_attachment (ctrl+v binding) explicitly stages clipboard content when widget is focused in message-browsing mode.","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"d7ccb3563a80f16d","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tOh9_QTyE1qC","mind":"default","section":"Architecture","content":"_stage_from_clipboard() async worker reads clipboard in executor, sets _pending_attachment dict, displays status with source label (📋 screenshot, 📋 copied image, 📎 file, 📎 path), and focuses chat input for optional message text.","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"cad03c9eb048ceb6","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ev6VJfEpCvj5","mind":"default","section":"Architecture","content":"pyobjc-core and pyobjc-framework-Cocoa 12.1 are already available in the styrened venv (pulled in by rumps dependency) — AppKit imports work without adding new deps.","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"dfde480283b2d971","confidence":1,"last_reinforced":"2026-03-02T22:52:57.809Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2lkprN3dd94X","mind":"default","section":"Architecture","content":"ChatWidget action_attach_clipboard() calls _attach_from_clipboard_or_prompt() via run_worker — if clipboard read returns None, falls back to path prompt by setting awaiting_path:True and focusing the Input. Worker errors are not explicitly caught in on_worker_state_changed.","status":"active","created_at":"2026-03-02T23:57:47.915Z","source":"extraction","content_hash":"65b207b0d714a48c","confidence":1,"last_reinforced":"2026-03-02T23:57:47.915Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"KjasggM2i5I2","mind":"default","section":"Architecture","content":"NodeInfoPanel _render_left_column() has sections: SYSTEM, COMMS (inbox unread, chats count, contacts count, auto-reply status), IDENTITY; _render_right_column() has sections: RETICULUM, STYRENE, TRAFFIC (msg sent/recv, pending, active links, transport/propagation roles), VERSION","status":"active","created_at":"2026-03-03T01:00:45.612Z","source":"extraction","content_hash":"41a8346263296803","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"yx5QB_weBFD4"}
{"_type":"fact","id":"5Kw_Nct1YhPk","mind":"default","section":"Architecture","content":"NodeInfoPanel reactive vars for comms/traffic: unread_count, conversation_count, contact_count, messages_sent, messages_received, pending_deliveries, auto_reply_enabled, propagation_enabled, transport_enabled, active_links. _load_comms_data() populates from local SQLite DB in standalone mode; IPC bridge populates in daemon mode.","status":"active","created_at":"2026-03-03T01:00:45.612Z","source":"extraction","content_hash":"c3c650cd8d88cf49","confidence":1,"last_reinforced":"2026-03-03T01:46:20.001Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"fuWIWX_fOyfa"}
{"_type":"fact","id":"DcICBRcfpukA","mind":"default","section":"Architecture","content":"Dashboard panel CSS proportions (v0.12.0): #node-info-panel height:auto max-height:28, #mesh-devices-panel height:2fr min-height:6, #activity-feed-panel height:1fr min-height:4 max-height:16.","status":"active","created_at":"2026-03-03T01:00:45.612Z","source":"extraction","content_hash":"4e31af935e7a8201","confidence":1,"last_reinforced":"2026-03-03T01:00:45.612Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"P74lplZsVYtS"}
{"_type":"fact","id":"zIp2BY_ojnp1","mind":"default","section":"Architecture","content":"Exploration table has HOPS and VIA columns as of v0.12.1. HOPS from RNS.Transport.path_table[dest][2], VIA from discovered_via. Both sortable. MeshDevice model extended with optional hops field.","status":"active","created_at":"2026-03-03T01:28:27.726Z","source":"extraction","content_hash":"9def04c83463ff5f","confidence":1,"last_reinforced":"2026-03-03T03:08:26.505Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"3DQH6w-mxOVl","mind":"default","section":"Architecture","content":"ChatWidget._last_displayed_date tracks the most recent date separator rendered, enabling date separator continuity across incremental message appends. _maybe_mount_date_separator() helper mounts a separator Static when the message date changes.","status":"active","created_at":"2026-03-03T01:29:47.184Z","source":"extraction","content_hash":"b77dc7cd3c47a5e1","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"VmgwlFuVJxJe","mind":"default","section":"Architecture","content":"ChatWidget message format (v0.13.0): outgoing uses 'content icon HH:MM' right-padded (8-char left margin), incoming uses sender name on own line + 'content HH:MM' left-padded (8-char right margin). No 'ME:' prefix on outgoing. Position, color, and alignment differentiate sender.","status":"active","created_at":"2026-03-03T01:43:46.904Z","source":"extraction","content_hash":"3163483e54bce75e","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"Jxxcrt0FQqbt"}
{"_type":"fact","id":"jxOyyIIKHHKt","mind":"default","section":"Architecture","content":"RNS.Transport.path_table keys are bytes (raw destination hash bytes), not hex strings. Index 5 is receiving_interface object. This matches the existing implementation in StyreneAnnounceHandler which passes bytes destination_hash to path_table.get().","status":"active","created_at":"2026-03-03T01:49:03.312Z","source":"extraction","content_hash":"8709e8b131d65ade","confidence":1,"last_reinforced":"2026-03-03T01:49:03.312Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"v3dl2Tn1G6Pm","mind":"default","section":"Architecture","content":"RNS.Transport.path_table entry index 1 (received_from) contains the next-hop transport node hash that relayed the announce — distinct from index 5 (interface) which is the local interface. Both are available for richer VIA display.","status":"active","created_at":"2026-03-03T03:08:26.505Z","source":"extraction","content_hash":"4bc7edde6c628b54","confidence":1,"last_reinforced":"2026-03-03T03:08:26.505Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"oA9oB1kyZXss","mind":"default","section":"Architecture","content":"ExplorationScreen has a fifth 'Styrene' tab (tab-styrene) added before the existing 4 tabs, using StyreneFleetTable (DataTable subclass) instead of ReticumAnnounceTable, with columns NAME/VERSION/CAPABILITIES/HOPS/VIA/STATUS/LAST ANNOUNCE and Styrene-specific filtering/deduplication via _deduplicate_by_identity().","status":"active","created_at":"2026-03-03T03:13:27.878Z","source":"extraction","content_hash":"d1a738ae4fa429c5","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"5QnZus5UMCk2","mind":"default","section":"Architecture","content":"ExplorationScreen has five tabs (Styrene, LXMF, Pages, Infrastructure, Other). _EXPLORATION_TYPES excludes DeviceType.STYRENE_NODE from generic discovery. Styrene tab uses StyreneFleetTable with fleet-specific columns. ReticumAnnounceTable is shared by the other 4 tabs. _STYRENE_TYPES = frozenset({DeviceType.STYRENE_NODE}).","status":"active","created_at":"2026-03-03T03:13:27.879Z","source":"extraction","content_hash":"9fab4c8a055c8a00","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"t-JnhMraNYSQ"}
{"_type":"fact","id":"PgYzUhAxJRVl","mind":"default","section":"Architecture","content":"StyreneFleetTable (DataTable[str]) at exploration.py shows Styrene fleet nodes with columns NAME, VERSION, CAPABILITIES, HOPS, VIA, STATUS, LAST ANNOUNCE. Capability icons: 📨 autoreply, ⚡ exec, 🔌 rpc, 🔀 transport. Deduplicates via _deduplicate_by_identity(). Sortable by all columns. Has set_filter(), sort_by(), refresh_data() methods. Standalone mode reads from node_store.get_styrene_nodes() + discover_devices().","status":"active","created_at":"2026-03-03T03:13:27.879Z","source":"extraction","content_hash":"ce7a540c0cddb5a4","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XjMI6b7PnSTw","mind":"default","section":"Architecture","content":"IPC CMD_RETRY_MESSAGE handler fully implemented in handlers.py: prepare_retry() → reset to PENDING → rebuild LXMF payload → send via lxmf_service.send_message() with delivery callbacks → register_delivery_tracking() → mark_sent(). Returns ErrorResponse.not_found if message not in FAILED state.","status":"active","created_at":"2026-03-03T19:49:16.773Z","source":"extraction","content_hash":"8629b47960e13c4b","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"GgFZ51fNkYuy","mind":"default","section":"Architecture","content":"_resolve_identity() Strategy 5 implemented: requests RNS path + parallel identity_hash path request, then polls for up to 10s (0.5s sleep intervals) for identity to arrive via re-announce or path response. IPC handlers wrap send_message calls in run_in_executor to prevent event loop blocking during the 10s wait.","status":"active","created_at":"2026-03-03T20:06:00.607Z","source":"extraction","content_hash":"7ea559d88679aecb","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vDsqYTQP8YYM","mind":"default","section":"Architecture","content":"lxmf_service.send_message() returns None when: (1) LXMF not initialized, (2) _resolve_identity() returns None after exhausting 5 strategies: direct RNS recall, NodeStore operator dest lookup, NodeStore LXMF dest lookup, persistent storage load, Strategy 5 (request_path + 10s poll loop). Strategy 5 blocks calling thread up to 10s — IPC handlers call send_message via run_in_executor to isolate blocking. send timeout bumped to 30s in TUI.","status":"active","created_at":"2026-03-03T20:06:00.607Z","source":"extraction","content_hash":"62aa27e0182d7682","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"TM3-ccLDCH-7"}
{"_type":"fact","id":"FEmka9aGpoBO","mind":"default","section":"Architecture","content":"MeshDeviceDetailScreen._resolve_nomadnet_destination() checks if the device is a NOMADNET_NODE directly, or queries NodeStore for any node with matching identity_hash and DeviceType.NOMADNET_NODE — enabling Pages tab for Styrene nodes that co-announce NomadNet aspects (same RNS identity, different destination hash).","status":"active","created_at":"2026-03-03T20:42:53.971Z","source":"extraction","content_hash":"45c89abdacd41212","confidence":1,"last_reinforced":"2026-03-03T22:28:27.377Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"QttNORazZVre","mind":"default","section":"Architecture","content":"NomadNet pages are a feature set of a Styrene node: same RNS identity produces separate destination hashes for different announce aspects — ('nomadnetwork','node') for NomadNet pages and ('styrene_node','operator') for Styrene. Devices sharing an identity_hash are the same physical node.","status":"active","created_at":"2026-03-03T20:42:53.971Z","source":"extraction","content_hash":"03216ee60d1ce3fd","confidence":1,"last_reinforced":"2026-03-03T20:52:21.736Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XOBTBs3TsJVa","mind":"default","section":"Architecture","content":"NomadNet pages use Micron (.mu) format, stored at ~/.nomadnetwork/storage/pages/. Demo pages live in public-hub/pages/demo/ and include index.mu, identity.mu, fleet.mu, hardware.mu etc.","status":"active","created_at":"2026-03-03T20:52:21.736Z","source":"extraction","content_hash":"ea9bfcac031a0203","confidence":1,"last_reinforced":"2026-03-03T23:47:58.460Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IWvoELGy79WL","mind":"default","section":"Architecture","content":"PageServerConfig.pages_dir defaults to ~/.styrene/pages/ (None means use paths.data_dir()/pages), with enabled:false by default. PageServerService.start() creates the directory, attempts RNS destination creation, and now calls _ensure_default_index() to generate index.mu if missing.","status":"active","created_at":"2026-03-03T20:53:43.484Z","source":"extraction","content_hash":"afd5662c0245e39e","confidence":1,"last_reinforced":"2026-03-03T20:57:57.903Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"70ijLb-8Kfwe","mind":"default","section":"Architecture","content":"_detect_hardware() in page_server.py reads hw.model via sysctl on macOS, /proc/device-tree/model on Linux SBCs (RPi), or /sys/class/dmi/id/product_name on x86. _detect_capabilities() introspects running lxmf_service, rns_service, and rpc_server singletons at page generation time.","status":"active","created_at":"2026-03-03T20:53:43.484Z","source":"extraction","content_hash":"9210bb4aa56c8e03","confidence":1,"last_reinforced":"2026-03-03T20:53:43.484Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"o9GX1dhicZ0V","mind":"default","section":"Architecture","content":"PageServerService.regenerate_index() is a public method that overwrites index.mu with current node info — callable from TUI settings or CLI without requiring a restart.","status":"active","created_at":"2026-03-03T20:53:43.484Z","source":"extraction","content_hash":"4d055ccad223e02d","confidence":1,"last_reinforced":"2026-03-03T20:57:57.904Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Xs-Q67R3J4El","mind":"default","section":"Architecture","content":"CMD_PAGE_REGENERATE IPC command: CmdPageRegenerateRequest (no-arg), handle_cmd_page_regenerate() calls daemon._page_server_service.regenerate_index() and returns ResultResponse with {regenerated: bool, pages_dir: str|None}. Client method: IPCBridge.page_regenerate_index() returns bool.","status":"active","created_at":"2026-03-03T20:55:29.386Z","source":"extraction","content_hash":"4c1010bb02cb0bae","confidence":1,"last_reinforced":"2026-03-03T20:55:29.386Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CRIHShvMJksl","mind":"default","section":"Architecture","content":"Settings screen System tab now has a PAGES panel with: Serve Pages Switch (page_server_enabled), Node Name Input (page_server_node_name), and a 'Generate Node Page' Button (btn-generate-page), plus a description explaining the feature.","status":"active","created_at":"2026-03-03T20:57:57.903Z","source":"extraction","content_hash":"6b9556f14131408b","confidence":1,"last_reinforced":"2026-03-03T20:57:57.903Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"oWZ6qEymx3nL","mind":"default","section":"Architecture","content":"PageServerService._generate_node_info_page() includes an 'About Styrene' section with docs URL (https://docs.styrene.io), Source link (https://github.com/styrene-lab/styrened), Community Hub link, 'pipx install styrene' install hint, and rns.styrene.io:4242 connection info as mild self-promotion in the default NomadNet index page.","status":"active","created_at":"2026-03-03T21:01:37.454Z","source":"extraction","content_hash":"e7eee94b772eeba2","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":"qXgCnOyM_HuJ"}
{"_type":"fact","id":"iPhY7OswQmUW","mind":"default","section":"Architecture","content":"scripts/generate_changelog.py generates src/styrened/_changelog.py (ENTRIES list of (version, summary) tuples) from git tags at build time, embedding changelog data in the package wheel so it's available in pipx/pip installs without a git checkout.","status":"active","created_at":"2026-03-03T21:05:40.679Z","source":"extraction","content_hash":"20db84089c74bb34","confidence":1,"last_reinforced":"2026-03-03T21:09:19.377Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Xl4QlscjzwYD","mind":"default","section":"Architecture","content":"tui/services/changelog.py: Changelog/ChangelogEntry dataclasses, fetch_changelog() with 4-strategy chain: (1) bundled _changelog.py baked at build time, (2) local git log, (3) GitHub compare API, (4) PyPI version list fallback. Strategy 1 always has commit messages and works for pipx installs. Changelog.format_lines() groups entries by version. UpgradeScreen shows WHAT'S NEW section, hides on upgrade start, runs _fetch_changelog() worker on_mount.","status":"active","created_at":"2026-03-03T21:05:40.680Z","source":"extraction","content_hash":"67a7146d41ff9d5e","confidence":1,"last_reinforced":"2026-03-03T22:29:43.637Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"CSk9y2EZHCDC"}
{"_type":"fact","id":"RkjHwdBSMCgX","mind":"default","section":"Architecture","content":"public-hub/container/Containerfile.consolidated installs styrened[web,metrics] from PyPI via ARG STYRENED_VERSION build argument, plus nomadnet==0.9.7. Demo pages from pages/demo/ are baked into the image at /app/demo-pages/ and copied to NomadNet storage/pages/ at startup by the entrypoint script.","status":"active","created_at":"2026-03-03T21:09:19.376Z","source":"extraction","content_hash":"44580739eae92cdb","confidence":1,"last_reinforced":"2026-03-03T21:09:19.376Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"AOTmYdomB4Vw","mind":"default","section":"Architecture","content":"public-hub entrypoint-consolidated.sh runs styrened as primary process (owns RNS transport), waits for RNS storage dir, then starts NomadNet in daemon mode connecting to styrened's shared RNS instance. Hub has two monitored processes (styrened + nomadnet).","status":"active","created_at":"2026-03-03T21:09:19.376Z","source":"extraction","content_hash":"fb2e3db5fd7d708c","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"q7d3eOA4DR3d","mind":"default","section":"Architecture","content":"public-hub has two Containerfiles: Containerfile (legacy — rnsd+lxmd+nomadnet, no styrened) and Containerfile.consolidated (current — styrened+nomadnet). Hub's NomadNet process owns page serving from ~/.nomadnetwork/storage/pages/, not styrened's PageServerService.","status":"active","created_at":"2026-03-03T21:09:19.376Z","source":"extraction","content_hash":"ff2c823295a4f943","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uQoZNNn_ahmx","mind":"default","section":"Architecture","content":"On public-hub, styrened and NomadNet are separate processes each with their own RNS identity. styrened uses ~/.styrene/identity; NomadNet uses ~/.nomadnetwork/storage/identity. These produce different identity_hashes, meaning the two announce destinations (styrene_node/operator and nomadnetwork/node) cannot be correlated by identity_hash alone.","status":"active","created_at":"2026-03-03T21:53:53.372Z","source":"extraction","content_hash":"9da5cb88126f07a1","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lsJlOvyVnBjX","mind":"default","section":"Architecture","content":"Styrene announce wire format extended to 8-field: 'styrene:{name}:{version}:{caps}:{lxmf_dest}:{short_name}:{fingerprint}:{nomadnet_dest}' — nomadnet_dest is the NomadNet destination hash, populated when page_server is active (edge mode: styrened owns destination; bridge mode: reads NomadNet's identity file and computes hash).","status":"active","created_at":"2026-03-03T21:55:44.831Z","source":"extraction","content_hash":"0f51154f480e1c75","confidence":1,"last_reinforced":"2026-03-03T22:28:27.377Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"istMi1iIKSTu","mind":"default","section":"Architecture","content":"parse_announce_data() return type extended from 7-tuple to 8-tuple by adding nomadnet_dest (str|None) as the 8th element. MeshDevice parsing now handles 8-field announce format alongside legacy 4/6/7-field formats.","status":"active","created_at":"2026-03-03T21:55:44.831Z","source":"extraction","content_hash":"9a3897316cd23379","confidence":1,"last_reinforced":"2026-03-03T22:28:27.377Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"C2AexFIJ4C6Y","mind":"default","section":"Architecture","content":"NodeStore 'nodes' table now has nomadnet_destination_hash TEXT column (ALTER TABLE migration). Saved in save_node() as 14th positional param, COALESCE preservation on UPSERT. Read back in _row_to_device() with try/except KeyError guard for older DBs.","status":"active","created_at":"2026-03-03T22:28:27.378Z","source":"extraction","content_hash":"c95671b71501711b","confidence":1,"last_reinforced":"2026-03-03T22:28:27.378Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"l8Y1ZhgPyTlD","mind":"default","section":"Architecture","content":"fetch_changelog() Strategy chain now has 5 strategies: (1) bundled _changelog.py (installed version), (2) download target version's wheel from PyPI and extract its _changelog.py (fixes 'what's new in NEXT version' gap for pipx installs), (3) local git log, (4) GitHub compare API, (5) PyPI version listing fallback.","status":"active","created_at":"2026-03-03T23:04:04.524Z","source":"extraction","content_hash":"723955ac3bb71791","confidence":1,"last_reinforced":"2026-03-03T23:04:04.524Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"4tBtYV6nJQ6h"}
{"_type":"fact","id":"HhDY2-ugt6Vu","mind":"default","section":"Architecture","content":"fetch_changelog() in tui/services/changelog.py has 5-strategy chain: (1) bundled _changelog.py, (2) PyPI wheel download — fetches target version's wheel, extracts _changelog.py via zipfile, parses ENTRIES via exec, (3) local git log, (4) GitHub compare API, (5) PyPI version list fallback. Strategy 2 via _fetch_from_pypi_wheel() resolves the 'next version changelog' problem for pipx installs.","status":"active","created_at":"2026-03-03T23:04:04.524Z","source":"extraction","content_hash":"f934e4d15f3fb74b","confidence":1,"last_reinforced":"2026-03-03T23:04:04.524Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"T0cxy7M2RK1X"}
{"_type":"fact","id":"3bPPEGI2-Qrn","mind":"default","section":"Architecture","content":"_fetch_from_pypi_wheel() downloads target version's .whl from PyPI JSON API, reads the zip in-memory via zipfile.ZipFile(io.BytesIO), extracts styrened/_changelog.py, executes it with restricted builtins, and parses ENTRIES list. Uses packaging.Version for range filtering.","status":"active","created_at":"2026-03-03T23:04:04.525Z","source":"extraction","content_hash":"01a3632fbded9404","confidence":1,"last_reinforced":"2026-03-03T23:04:04.525Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-kRWjZMYXLHt","mind":"default","section":"Architecture","content":"NomadNet destination hash for hub's multi-process setup: af1ec9121da534836e6a39b7d261fa65 (computed from /app/.nomadnetwork/storage/identity with identity_hash 20a6dd01480cd200b46260a7e2014f88).","status":"active","created_at":"2026-03-03T23:45:53.981Z","source":"extraction","content_hash":"b9a4ee50c5547c0f","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2CLuInF1D5Z5","mind":"default","section":"Architecture","content":"public-hub K8s deployment has 4 PVCs in namespace styrene-hub: lxmf-data (500Mi), nomadnet-data (500Mi), reticulum-data (100Mi), styrene-data (500Mi), all using local-path StorageClass on the brutus cluster.","status":"active","created_at":"2026-03-03T23:47:58.460Z","source":"extraction","content_hash":"e257ac8ffb71d1aa","confidence":1,"last_reinforced":"2026-03-03T23:47:58.460Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"F3QLPtzkAMP0","mind":"default","section":"Architecture","content":"page_browser.py fetch_page() flow: (1) RNS.Transport.request_path if no path, (2) poll PATH_DISCOVERY_TIMEOUT for path, (3) RNS.Identity.recall for identity, (4) _get_or_create_link with one retry on failure after forced path re-discovery.","status":"active","created_at":"2026-03-04T00:25:17.415Z","source":"extraction","content_hash":"252ef5fb0e73b49d","confidence":1,"last_reinforced":"2026-03-04T01:44:41.586Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"x2U6_yfdTG2s","mind":"default","section":"Architecture","content":"PageBrowserService._get_or_create_link() creates RNS.Link to RNS.Destination(identity, OUT, SINGLE, 'nomadnetwork', 'node'), then calls link.request(path, ...) to invoke NomadNet's registered request handlers. Link establishment timeout is separate from page request timeout (step 5 asyncio.wait_for).","status":"active","created_at":"2026-03-04T00:33:05.113Z","source":"extraction","content_hash":"add8662a78498c86","confidence":1,"last_reinforced":"2026-03-04T01:44:41.587Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"4bVlns-NDHF2","mind":"default","section":"Architecture","content":"NomadNet Node.py registers each page as a request handler via destination.register_request_handler('/page/<relpath>', response_generator=self.serve_page, allow=ALLOW_ALL). Executable .mu files (exec bit set) are run as scripts; non-executable are served verbatim.","status":"active","created_at":"2026-03-04T00:33:05.113Z","source":"extraction","content_hash":"3cb167b0141693dd","confidence":1,"last_reinforced":"2026-03-04T00:33:05.113Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rpMm9D-g4h9M","mind":"default","section":"Architecture","content":"public-hub NomadNet pages directory has 28+ registered pages including /page/index.mu, /page/about.mu, /page/getting-started.mu, /page/connect.mu, demo/ (executable .mu scripts), and docs/ (static .mu files).","status":"active","created_at":"2026-03-04T00:36:39.523Z","source":"extraction","content_hash":"5b65ec95ce12a673","confidence":1,"last_reinforced":"2026-03-04T00:36:39.523Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Et6etDb9ust5","mind":"default","section":"Architecture","content":"NomadNet peersettings shows served_page_requests: 3299+ and node_last_announce firing ~100s intervals — NomadNet on the hub is actively serving pages and announcing. Page browser failures are client-side link establishment issues, not a hub-side problem.","status":"active","created_at":"2026-03-04T00:39:06.215Z","source":"extraction","content_hash":"28edad240304b889","confidence":1,"last_reinforced":"2026-03-04T00:39:06.215Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"PpTpDuEjpC9l","mind":"default","section":"Architecture","content":"PageBrowserService can be tested standalone by initializing RNS.Reticulum() directly before creating the service — the service itself does not init RNS, it assumes it is already running (either as daemon's shared instance or caller-initialized).","status":"active","created_at":"2026-03-04T00:41:34.175Z","source":"extraction","content_hash":"d4c0cb8ea31be45b","confidence":1,"last_reinforced":"2026-03-04T01:44:41.587Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"gfu0_kRpGkmM","mind":"default","section":"Architecture","content":"rns_service._check_interface_states() debounce added: _RECONNECT_DEBOUNCE constant suppresses TCP object-identity-change reconnect events that fire within the cooldown window, preventing reconnect storms from flooding _handle_reconnection() when multiple TCP peers cycle rapidly. Uses self._last_reconnect_time class attribute.","status":"active","created_at":"2026-03-04T00:43:29.360Z","source":"extraction","content_hash":"da071e892ba13cfa","confidence":1,"last_reinforced":"2026-03-04T00:50:09.950Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"nA052JS-GyfU","mind":"default","section":"Architecture","content":"Micron renderer visual improvements (v0.13.17): Headings use Imperial CRT palette (h1=#5af0ce bold, h2=#3fbfa0 bold, h3=#6a9a8e dim italic). Links render blue underline (#5ac8fa) with ▸ prefix. Dividers colored dim teal (#3a6a5e). Literal blocks get │ pipe prefix. File: src/styrened/tui/widgets/micron_parser.py","status":"active","created_at":"2026-03-04T01:13:43.427Z","source":"manual","content_hash":"4cb83c2c636162b8","confidence":1,"last_reinforced":"2026-03-04T01:13:43.427Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"dE_W-1wRQYQM","mind":"default","section":"Architecture","content":"Upgrade flow (v0.13.21+): upgrade screen runs pipx upgrade → kills old daemon → starts new daemon via Popen([styrened, daemon]) → waits → dismisses with True → app._restart_tui() re-execs TUI. If daemon start fails in upgrade screen, TUI on_mount auto-start (v0.13.22) catches it.","status":"active","created_at":"2026-03-04T01:13:43.520Z","source":"manual","content_hash":"009bcdee8ce493b9","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CRvTyl2XGqyw","mind":"default","section":"Architecture","content":"PageCacheService at src/styrened/services/page_cache.py: write-through SQLite cache for NomadNet pages. Models PageCache (destination_hash+path unique, content, content_length, fetched_at) and SavedSite (destination_hash unique, display_name, refresh_interval, last_crawl_at, pages_cached, max_depth) added to models/messages.py Base. Background crawl loop refreshes saved sites at their configured interval. extract_page_links() parses micron content for same-node BFS crawling.","status":"active","created_at":"2026-03-04T01:46:27.806Z","source":"extraction","content_hash":"1dca9ba2a7337c3a","confidence":1,"last_reinforced":"2026-03-04T02:20:04.124Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Dnu6_pKjqEp2","mind":"default","section":"Architecture","content":"PageCacheService is initialized in daemon._start_page_browser() using init_db() to get the SQLAlchemy engine (not a _message_store._engine attribute), then set_page_browser() wires it to the PageBrowserService. Shutdown order: page_cache_service stopped before page_browser_service in daemon shutdown.","status":"active","created_at":"2026-03-04T01:48:00.318Z","source":"extraction","content_hash":"2b9db36f787bc194","confidence":1,"last_reinforced":"2026-03-04T02:20:04.126Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"aOBro8Yg9EDa","mind":"default","section":"Architecture","content":"IPC fetch-page handler has write-through cache pattern: on status=='ok' with content, calls cache_svc.cache_page(); on non-ok status OR exception, tries get_cached_page() and returns cached_content/cached_at/cached_content_length fields in the response dict alongside the error.","status":"active","created_at":"2026-03-04T01:48:00.318Z","source":"extraction","content_hash":"dfaee45485022b15","confidence":1,"last_reinforced":"2026-03-04T02:20:04.126Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"aovM_C_F-P1v","mind":"default","section":"Architecture","content":"PageBrowserWidget has 'S' (save_site) and 'C' (crawl_site) keybindings, and _show_cached_page() method that renders cached micron content with a ⚠ banner showing cache age and error reason when live fetch fails.","status":"active","created_at":"2026-03-04T01:49:46.269Z","source":"extraction","content_hash":"3cf0ee65514177a9","confidence":1,"last_reinforced":"2026-03-04T02:20:04.126Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cZ7th-3drzUk","mind":"default","section":"Architecture","content":"IPC cache page commands wired: CMD_PAGE_SAVE_SITE/REMOVE_SITE/LIST_SITES/CRAWL_SITE/GET_CACHED all registered in server.py dispatch table and handler methods added to handlers.py. Client methods: page_save_site(), page_remove_site(), page_list_sites(), page_crawl_site(), page_get_cached() added to client.py.","status":"active","created_at":"2026-03-04T01:49:46.269Z","source":"extraction","content_hash":"e7a2bc4fcabb907c","confidence":1,"last_reinforced":"2026-03-04T02:20:04.126Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uJl95lcVmjOM","mind":"default","section":"Architecture","content":"page_browser.py IPC fetch-page result now maps all error statuses (path_not_found, timeout, link_failed, not_found, error) through a single branch that checks for cached_content/cached_at fields in the result dict before showing the error, enabling offline fallback display.","status":"active","created_at":"2026-03-04T01:49:46.269Z","source":"extraction","content_hash":"1970fb04d21e7eb8","confidence":1,"last_reinforced":"2026-03-04T01:49:46.269Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2Tx2piwHBUPH","mind":"default","section":"Architecture","content":"extract_page_links() in page_cache.py normalizes URLs: strips colon prefix for self-references, skips cross-node links (those containing ':' after the first char), prepends '/page/' to relative paths, and appends '.mu' to extensionless path segments only when the last segment has no '.'.","status":"active","created_at":"2026-03-04T02:20:04.124Z","source":"extraction","content_hash":"1fe720e6cd70b6d5","confidence":1,"last_reinforced":"2026-03-04T02:20:04.124Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"SiORzzn9cK2Z","mind":"default","section":"Architecture","content":"PageCacheService.crawl_site() uses collections.deque for BFS queue (popleft = O(1)) as of v0.13.24. Previously used list.pop(0) which was O(n).","status":"active","created_at":"2026-03-04T02:31:08.597Z","source":"extraction","content_hash":"ea6d12b0b8edaba6","confidence":1,"last_reinforced":"2026-03-04T02:31:08.597Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"yUEGnad6w8S8"}
{"_type":"fact","id":"Z3RO7smxz0Jf","mind":"default","section":"Architecture","content":"Planned persistent direct data link for Styrene nodes: RNS.Link established per-peer in the device detail screen, renegotiated on path change, serving as foundation for high-bandwidth direct mesh datalink system. Initialization point for future streaming status, file transfer, and real-time RPC.","status":"active","created_at":"2026-03-04T02:54:28.016Z","source":"extraction","content_hash":"2040877a589aa50c","confidence":1,"last_reinforced":"2026-03-04T03:02:07.949Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"be-HDSwiJuyD","mind":"default","section":"Architecture","content":"DeviceStatusWidget renders RPC data in plain dashed-header sections with no cascade colors — hub returns uptime=0 (container uptime), ip=127.0.0.1 (no route to 8.8.8.8 in K8s), services=[] (no systemctl) as real but useless values rather than the fallback sentinel.","status":"active","created_at":"2026-03-04T02:58:34.889Z","source":"extraction","content_hash":"4a70926b37de6540","confidence":1,"last_reinforced":"2026-03-04T03:03:44.508Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ch6MRnPtVQwI","mind":"default","section":"Architecture","content":"DeviceStatusWidget renders RPC StatusResponse in plain dashed-header sections with no cascade colors — aesthetic mismatch with Imperial CRT theme. Hub returns uptime=0, ip=127.0.0.1, services=[] (real but useless K8s container values). MeshDevice announce data (name, type, capabilities, version, hops, via, last_seen, identity) not passed to status widget; only StatusResponse RPC data is shown.","status":"active","created_at":"2026-03-04T02:58:34.889Z","source":"extraction","content_hash":"18c100b0444e54ce","confidence":1,"last_reinforced":"2026-03-04T03:03:44.508Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"E58nMDhApbO1"}
{"_type":"fact","id":"aDGApxnZezjb","mind":"default","section":"Architecture","content":"RPC server _gather_status() uses /proc/uptime (Linux), sysctl kern.boottime (macOS), socket.connect('8.8.8.8') for IP, and systemctl is-active checks for services — all fail gracefully to 0/'127.0.0.1'/[] in containers. disk usage via os.statvfs('/').","status":"active","created_at":"2026-03-04T02:58:34.889Z","source":"extraction","content_hash":"375890a8eb9f8b69","confidence":1,"last_reinforced":"2026-03-04T03:00:16.496Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"4sV6bRq4ifCk","mind":"default","section":"Architecture","content":"Planned persistent direct data link for Styrene nodes: daemon registers RNS.Destination(identity, IN, SINGLE, 'styrene', 'datalink') to accept incoming links; DirectLinkService manages outgoing RNS.Links to peers. Link destination hash is deterministically computable from the peer's identity. Renegotiated on path change. Foundation for streaming status, file transfer, and real-time RPC multiplexing.","status":"active","created_at":"2026-03-04T02:58:34.889Z","source":"extraction","content_hash":"276b4571a860e75e","confidence":1,"last_reinforced":"2026-03-04T03:02:07.949Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Eiv9pAGeNe3d","mind":"default","section":"Architecture","content":"DirectLinkService at src/styrened/services/direct_link.py manages persistent outgoing RNS.Links to Styrene peers via the ('styrene','datalink') destination aspect. Links are cached by LXMF destination hash, auto-cleaned after 5min idle, and support /status and /ping requests over the link. The daemon registers a complementary IN SINGLE destination for accepting incoming links.","status":"active","created_at":"2026-03-04T03:00:16.495Z","source":"extraction","content_hash":"95a31fcd87e5ca4f","confidence":1,"last_reinforced":"2026-03-04T05:07:53.942Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"hDX7xzu_ZHbY","mind":"default","section":"Architecture","content":"RPC server at src/styrened/rpc/server_protocol.py (StyreneProtocol-based) supports PING, STATUS_REQUEST, EXEC, REBOOT, CONFIG_UPDATE, SELF_UPDATE, INBOX_QUERY, MESSAGES_QUERY. Rate limiting per identity (30 req/min), replay protection (300s window, 1000 recent IDs), authorization whitelist. Dangerous commands disabled by default.","status":"active","created_at":"2026-03-04T03:00:16.495Z","source":"extraction","content_hash":"942d413cdc0bc911","confidence":1,"last_reinforced":"2026-03-04T03:00:16.495Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qGfyPcwVkxKl","mind":"default","section":"Architecture","content":"_gather_status() in the RPC server now returns additional fields: styrened_version, hostname, arch, os_id, os_version, nixos_generation, available_commands — via get_os_info() from services/system_info.py.","status":"active","created_at":"2026-03-04T03:00:16.496Z","source":"extraction","content_hash":"320f0e6024f63e27","confidence":1,"last_reinforced":"2026-03-04T03:00:16.496Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"jQCSp3eYhe47","mind":"default","section":"Architecture","content":"Daemon gets self._direct_link_service and self._datalink_destination attributes added alongside existing service fields. DirectLinkService started in _start_direct_link() after page server. Reconnect handler calls direct_link_service.handle_reconnection().","status":"active","created_at":"2026-03-04T03:00:16.496Z","source":"extraction","content_hash":"ba38e737b9e06592","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"7MsyDxXoef9w","mind":"default","section":"Architecture","content":"DirectLinkService at src/styrened/services/direct_link.py integrated into daemon: daemon._direct_link_service started in _start_direct_link(), daemon._datalink_destination registers ('styrene','datalink') IN SINGLE destination with /status and /ping request handlers. _on_datalink_established() logs incoming peer links.","status":"active","created_at":"2026-03-04T03:02:07.948Z","source":"extraction","content_hash":"620148d0d3f0187d","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"R56m9Iygt3gL","mind":"default","section":"Architecture","content":"IPC message classes added for direct link: CmdDatalinkEstablishRequest, CmdDatalinkTeardownRequest, CmdDatalinkStatusRequest, CmdDatalinkQueryRequest — all with destination_hash field, deserialization in factory function.","status":"active","created_at":"2026-03-04T03:02:07.948Z","source":"extraction","content_hash":"f75a5573add40592","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"jsrFdleMFnTE","mind":"default","section":"Architecture","content":"IPC handler methods added for direct link: handle_cmd_datalink_establish, handle_cmd_datalink_teardown, handle_cmd_datalink_status, handle_cmd_datalink_query — all with _check_direct_link() guard checking for daemon._direct_link_service.","status":"active","created_at":"2026-03-04T03:02:07.948Z","source":"extraction","content_hash":"9a76ee301f92f11f","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"spYsMUeqmRaD","mind":"default","section":"Architecture","content":"_serve_datalink_status() handler in daemon.py delegates to self._rpc_server._gather_status() and returns JSON bytes — direct link status response reuses RPC gather_status data.","status":"active","created_at":"2026-03-04T03:02:07.949Z","source":"extraction","content_hash":"6a10e990b2c4137e","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cP7imRiE-HID","mind":"default","section":"Architecture","content":"DeviceStatusWidget rewritten as a two-column cascade-colored dashboard: left column (NODE: type/version/caps/alias, MESH: identity/dest/hops/via/nomadnet), right column (LINK: direct link status/RTT/age, SYSTEM: hostname/uptime/OS/arch/nixos/daemon version from RPC, NETWORK: IP/services/disk). MeshDevice passed to widget at construction time; announce data always present, RPC/link data are optional enrichment layers.","status":"active","created_at":"2026-03-04T03:03:44.508Z","source":"extraction","content_hash":"731b5a1c5252a602","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uOetIkWggCQC","mind":"default","section":"Architecture","content":"MeshDeviceDetailScreen adds 'l' (establish_link) keybinding alongside existing 'r' (refresh), 'a' (add_contact), 'y' (copy_hash). Status tab no longer has a Refresh Button widget — DeviceStatusWidget is mounted directly in the TabPane.","status":"active","created_at":"2026-03-04T03:03:44.509Z","source":"extraction","content_hash":"b87990a091c4ff79","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"eXvSunR_kA3Z","mind":"default","section":"Architecture","content":"IPC client (client.py) gains datalink_establish(), datalink_teardown(), datalink_status(), datalink_query() methods in a 'Direct data link methods' section, each calling _request() with the corresponding CmdDatalinkXxxRequest message.","status":"active","created_at":"2026-03-04T03:03:44.511Z","source":"extraction","content_hash":"7d54564d58303fbe","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mrXWrPVNTCGK","mind":"default","section":"Architecture","content":"IPCBridge (tui/services/ipc_bridge.py) gains datalink_establish(), datalink_teardown(), datalink_status(), datalink_query() bridge methods delegating to _call() — placed in a 'Direct data link methods' section before the subscriptions section.","status":"active","created_at":"2026-03-04T03:03:44.511Z","source":"extraction","content_hash":"e305a730de4a4662","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Jq98NeWgh5_f","mind":"default","section":"Architecture","content":"IPC handlers.py gains _check_direct_link() guard and four async handler methods: handle_cmd_datalink_establish, handle_cmd_datalink_teardown, handle_cmd_datalink_status, handle_cmd_datalink_query — all use DirectLinkService.establish()/teardown()/get_link_info()/request_status().","status":"active","created_at":"2026-03-04T03:03:44.511Z","source":"extraction","content_hash":"849401b9c197351f","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"d9JM2CIWX2lo","mind":"default","section":"Architecture","content":"DirectLinkService (v0.13.25): Persistent outgoing RNS.Links to Styrene peers via (\"styrene\", \"datalink\") destination aspect. Daemon registers IN destination with /status and /ping request handlers (JSON over link.request()). Client establishes OUT links identified by peer's LXMF dest hash, computes datalink dest hash from recalled identity. IPC: CMD_DATALINK_ESTABLISH(0x50), CMD_DATALINK_TEARDOWN(0x51), CMD_DATALINK_STATUS(0x52), CMD_DATALINK_QUERY(0x53). DirectLinkService in services/direct_link.py, daemon setup in _start_direct_link()/_setup_datalink_destination(). Idle link cleanup at 5min. Reconnect handling flags path re-discovery.","status":"active","created_at":"2026-03-04T03:06:07.157Z","source":"manual","content_hash":"6725dcbb0f4cc37d","confidence":1,"last_reinforced":"2026-03-04T05:07:53.942Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pDI03PP6Khnm","mind":"default","section":"Architecture","content":"IPC CMD_DATALINK_SPEEDTEST=0x54: CmdDatalinkSpeedtestRequest(destination_hash), handle_cmd_datalink_speedtest() in handlers.py calls DirectLinkService.run_speedtest() with timeout=90.0s, returns ResultResponse({results: list}). Client method datalink_speedtest(destination_hash, timeout=120.0). IPCBridge.datalink_speedtest() bridges to _call().","status":"active","created_at":"2026-03-04T03:11:19.022Z","source":"extraction","content_hash":"35202927f868afc3","confidence":1,"last_reinforced":"2026-03-04T03:11:19.022Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"jkvEvRwMXCdm","mind":"default","section":"Architecture","content":"DirectLinkService speedtest now RTT-adaptive: probes link.rtt, selects from PAYLOAD_SET_FAST/MEDIUM/SLOW/MINIMAL based on RTT thresholds (<200ms/<2s/<5s/>5s). Per-payload timeout auto-scales as max(30s, size*8/estimated_bps*4) or max(60s, size*8/1000*3) for LoRa. Early bail on failed transfers marks remaining sizes as 'skipped'. SPEEDTEST_DELAY_SLOW=2.0s for RTT>0.5s links.","status":"active","created_at":"2026-03-04T03:13:06.962Z","source":"extraction","content_hash":"b44e453e071eece6","confidence":1,"last_reinforced":"2026-03-04T03:13:06.962Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cPeoQiQsu9q2","mind":"default","section":"Architecture","content":"DirectLink timeouts increased for LoRa: PATH_DISCOVERY_TIMEOUT=45s, LINK_ESTABLISHMENT_TIMEOUT=45s, REQUEST_TIMEOUT=30s, CLEANUP_INTERVAL=120s, IDLE_LINK_TIMEOUT=900s (15min — link establishment expensive on LoRa).","status":"active","created_at":"2026-03-04T03:13:06.962Z","source":"extraction","content_hash":"7e5277d9d0560f6a","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"b2sGBzbsVxUQ","mind":"default","section":"Architecture","content":"MeshDeviceDetailScreen gains 'T' (run_speedtest) keybinding: checks for active link via datalink_status, runs _do_speedtest worker, displays results as notification with link RTT, per-payload throughput bar chart, and peak kbps. DeviceStatusWidget gains speedtest_results reactive that renders an ASCII bar chart in the right column.","status":"active","created_at":"2026-03-04T03:13:06.962Z","source":"extraction","content_hash":"c920d78615dc48ae","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2cXbspZyT2fk","mind":"default","section":"Architecture","content":"DeviceStatusWidget gains speedtest_results reactive (list|None) that renders ASCII bar chart in right column: per-payload throughput bar (█/░ chars scaled to ~1000kbps max), peak kbps summary, failed/skipped entries in dim. Watcher calls self.refresh() when mounted.","status":"active","created_at":"2026-03-04T03:18:12.184Z","source":"extraction","content_hash":"91a2cee6ab7790d7","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"e4iwropl_rzy","mind":"default","section":"Architecture","content":"_format_bytes(n: int) helper defined at module level in mesh_device_detail.py (after TYPE_CHECKING block) — formats int bytes as '256B', '4KB', '256MB' etc. Used in speedtest result formatting.","status":"active","created_at":"2026-03-04T03:18:12.184Z","source":"extraction","content_hash":"994bc49ab41a817a","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WxhArinyr1Us","mind":"default","section":"Architecture","content":"ReticumAnnounceTable and StyreneFleetTable both gain _hide_lost (default True), _hide_stale (default False), toggle_hide_lost()/toggle_hide_stale() methods, and status_counts property for per-status device counting. _rebuild_table() applies status filters before text filter.","status":"active","created_at":"2026-03-04T03:22:27.996Z","source":"extraction","content_hash":"5f7b23214fdf7cc1","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"NoD3z_V8h4XL","mind":"default","section":"Architecture","content":"ExplorationScreen status filter bar (#explore-status-bar Static) shows '● N active  ◐ N stale  ○ N lost (hidden)' above the TabbedContent. Tab labels show visible/total when filtered (e.g., 'LXMF (127/1725)'). _update_status_bar() reads active table's status_counts property and hiding_lost/hiding_stale flags.","status":"active","created_at":"2026-03-04T03:26:58.143Z","source":"extraction","content_hash":"c3bf8bbef0da6191","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"dxa80c6dk_RK","mind":"default","section":"Architecture","content":"MeshDeviceDetailScreen constructor accepts optional `device: MeshDevice | None` parameter — if supplied, skips _load_device() SQLite re-query. Exploration screen now passes pre-resolved MeshDevice on navigation.","status":"active","created_at":"2026-03-04T03:28:16.294Z","source":"extraction","content_hash":"c713eff133843619","confidence":1,"last_reinforced":"2026-03-04T03:28:16.294Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ek_dOilfNyr_","mind":"default","section":"Architecture","content":"Module-level status cache in mesh_device_detail.py: _STATUS_CACHE dict keyed by identity_hash with (StatusResponse, timestamp) tuples. TTL=120s. _cache_status()/get_cached_status() helpers. Cache checked before LXMF RPC in _auto_fetch_status(), hit renders immediately without loading=True wait.","status":"active","created_at":"2026-03-04T03:28:16.294Z","source":"extraction","content_hash":"4125c47e2c91f44b","confidence":1,"last_reinforced":"2026-03-04T03:33:16.124Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"_VnP6-KWSxN2","mind":"default","section":"Architecture","content":"v0.13.28 released: perf: device detail screen — MeshDevice passed from exploration table (skips NodeStore re-query), StatusResponse 120s TTL cache per identity hash (_STATUS_CACHE dict), cached status renders before loading spinner in _auto_fetch_status().","status":"active","created_at":"2026-03-04T03:32:31.884Z","source":"extraction","content_hash":"8328bda14d953509","confidence":1,"last_reinforced":"2026-03-04T03:32:31.884Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"z1Hn9jP4awz_","mind":"default","section":"Architecture","content":"CMD_TERMINAL_OPEN is dispatched via a special pre-dispatch branch in ipc/server.py (before the dispatch table lookup) because it requires the client reference for event routing. CMD_TERMINAL_INPUT/RESIZE/CLOSE are in the dispatch table. This means the 0x50-0x53 alias with datalink commands does not break terminal functionality at runtime.","status":"active","created_at":"2026-03-04T03:33:16.123Z","source":"extraction","content_hash":"79ac7bc54ba674aa","confidence":1,"last_reinforced":"2026-03-04T03:33:16.123Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MWgc7UYqz8kO","mind":"default","section":"Architecture","content":"IPC protocol constants for direct data link reassigned to 0x60-0x6F range to eliminate collision with terminal commands (0x50-0x5F): CMD_DATALINK_ESTABLISH=0x60, CMD_DATALINK_TEARDOWN=0x61, CMD_DATALINK_STATUS=0x62, CMD_DATALINK_QUERY=0x63, CMD_DATALINK_SPEEDTEST=0x64.","status":"active","created_at":"2026-03-04T03:35:29.299Z","source":"extraction","content_hash":"9ebde6b6a72837eb","confidence":1,"last_reinforced":"2026-03-04T03:35:29.299Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"5OpvlhxNMr67"}
{"_type":"fact","id":"cq8t3LwohBQk","mind":"default","section":"Architecture","content":"Styrene dual-layer mesh: BATMAN-ADV provides L2 physical connectivity (bat0 virtual switch over 802.11s WiFi, Ethernet, VPN), and Reticulum runs on top as an overlay using bat0 via AutoInterface. RNS.Links then provide encrypted destination-to-destination sessions over whatever transport is available. For nodes connected only via internet TCP (no local BATMAN), RNS.Links through the hub give direct encrypted channels — future work may tunnel L2 frames over these Links to extend the BATMAN domain across the internet, LoRa WAN, WiFi hotspots, and eventually LoRa radios, creating a unified IP mesh that spans all transport types.","status":"active","created_at":"2026-03-04T03:55:29.932Z","source":"manual","content_hash":"53ab2ba10dcac660","confidence":1,"last_reinforced":"2026-03-04T04:12:37.252Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"q1dvuMYATd7k"}
{"_type":"fact","id":"wb-PcBbrjyl3","mind":"default","section":"Architecture","content":"Styrene dual-layer mesh architecture confirmed in research docs: BATMAN-ADV (bat0) is the L2 physical layer, Reticulum runs on top via AutoInterface/UDP on bat0. Existing internet TCP connections use RNS.Links through the hub for direct encrypted channels. Future work: tunnel L2 Ethernet frames over RNS.Links to extend BATMAN domain across internet, LoRa WAN, WiFi hotspots, and LoRa radios — creating a unified IP mesh spanning all transport types. See styrene/docs/src/architecture.md and styrene/research/batman-adv.md.","status":"active","created_at":"2026-03-04T03:55:41.740Z","source":"extraction","content_hash":"ea2962987e1ff9f6","confidence":1,"last_reinforced":"2026-03-04T03:55:41.740Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ziopF6uNz1kC","mind":"default","section":"Architecture","content":"BATMAN-ADV over RNS overlay (future): each Styrene node gets a TAP interface managed by BATMAN-ADV, with RNS.Links serving as virtual point-to-point wires to neighbors. BATMAN-ADV then routes L2 frames across this virtual topology, giving nodes real IP addresses and arbitrary IP services (SSH, HTTP, file shares). Primary use case: extend the bat0 broadcast domain over transport types that don't support native 802.11s (internet TCP, LoRa WAN, WiFi hotspots).","status":"active","created_at":"2026-03-04T03:55:41.741Z","source":"extraction","content_hash":"329e16da6aa5c1a9","confidence":1,"last_reinforced":"2026-03-04T03:55:41.741Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"YrFwK1TtXxmi","mind":"default","section":"Architecture","content":"batman-mesh.nix at styrene-edge/sbc/common/ implements 802.11s+BATMAN-ADV for NixOS fleet devices: NetworkManager mesh profile, batctl setup systemd service, configurable meshId/meshKey/channel/gwMode (off/client/server). bat0 is the virtual switch interface exposed to the system.","status":"active","created_at":"2026-03-04T04:00:05.656Z","source":"extraction","content_hash":"d78abbf87d9aba76","confidence":1,"last_reinforced":"2026-03-04T04:00:05.656Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Y5ZBMDAokvJa","mind":"default","section":"Architecture","content":"Current gap in the BATMAN↔internet bridging plan: bat0 L2 domain is local-only (WiFi/Ethernet); internet-connected nodes (TCP to hub) are in a separate L2 domain with no bat0 bridge. RNS.Links exist (DirectLinkService) but carry only Styrene overlay traffic, not raw Ethernet frames. Next step: TAP-over-RNS.Link tunnel to extend bat0 across internet/LoRa transports.","status":"active","created_at":"2026-03-04T04:00:05.657Z","source":"extraction","content_hash":"181e4bbfe9755c6b","confidence":1,"last_reinforced":"2026-03-04T04:00:05.657Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"7WxWxIqVQ6hR","mind":"default","section":"Architecture","content":"batman-adv and TAP tunneling is Linux-only — a TunnelService extending bat0 over RNS.Links cannot run on macOS (operator workstation). The L2-over-RNS tunnel feature is fleet-device-only.","status":"active","created_at":"2026-03-04T04:02:43.623Z","source":"extraction","content_hash":"9201399a61146bba","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cKIATK7I0odw","mind":"default","section":"Architecture","content":"Styrene mesh VPN architecture: three tiers — (1) Internet → WireGuard to mesh gateway → bat0, (2) Local WiFi → BATMAN-ADV (802.11s) → bat0, (3) LoRa-only → RNS.Link L2 tunnel → bat0. BATMAN-ADV's role is creating IP from raw radios when no infrastructure exists; WireGuard handles the internet-connected tier.","status":"active","created_at":"2026-03-04T04:09:41.627Z","source":"extraction","content_hash":"22e72f2f89d53f70","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8JiIDp0CpsQv","mind":"default","section":"Architecture","content":"macOS cannot run batman-adv because: (1) Apple's proprietary WiFi driver doesn't expose 802.11s mesh point mode, (2) batman-adv is a Linux kernel module with no XNU port, (3) Apple deprecated kexts in favor of sandboxed Network Extensions. Hardware likely supports 802.11s but Apple chose not to implement it.","status":"active","created_at":"2026-03-04T04:09:41.627Z","source":"extraction","content_hash":"9da1eb1f8879d7c5","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"TnYtORIYYgR0","mind":"default","section":"Architecture","content":"daemon.py gains self._mesh_vpn_service: Any = None alongside _direct_link_service and _datalink_destination fields.","status":"active","created_at":"2026-03-04T04:12:37.250Z","source":"extraction","content_hash":"619a966b319b35d0","confidence":1,"last_reinforced":"2026-03-04T04:12:37.250Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WbwqQwVfA_Ax","mind":"default","section":"Architecture","content":"styrened NixOS module (styrene-edge/sbc/common/styrened.nix): service user is 'styrened' (isSystemUser), group 'users'. Now includes AmbientCapabilities=['CAP_NET_ADMIN'] and CapabilityBoundingSet=['CAP_NET_ADMIN'] for WireGuard interface management. wireguard-tools added to systemPackages.","status":"active","created_at":"2026-03-04T04:19:07.861Z","source":"extraction","content_hash":"713cf2942a6f4725","confidence":1,"last_reinforced":"2026-03-04T04:19:07.861Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"WUGiP3HC91Dv"}
{"_type":"fact","id":"_uDY6Jmr0rHo","mind":"default","section":"Architecture","content":"styrened.nix NixOS module: service user is 'styrened', group 'users', StateDirectory/ConfigurationDirectory 'styrened'. Now grants AmbientCapabilities=CAP_NET_ADMIN and CapabilityBoundingSet=CAP_NET_ADMIN for WireGuard interface management. Batman-mesh.nix adds bat0 via a separate oneshot service. wireguard-tools package added.","status":"active","created_at":"2026-03-04T04:19:07.861Z","source":"extraction","content_hash":"f4494afb7eb11394","confidence":1,"last_reinforced":"2026-03-04T04:19:07.861Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"E0BmCQmpICuc"}
{"_type":"fact","id":"FuS7gulBZNyg","mind":"default","section":"Architecture","content":"CoreConfig model has mesh_vpn: MeshVPNConfig field (enable, listen_port=51820, subnet_prefix='fd73:7479:7265:6e65', gateway, endpoint) in models/config.py. MeshVPNConfig defined once in models/config.py; services/mesh_vpn.py re-exports it via 'from styrened.models.config import MeshVPNConfig' for backward compatibility.","status":"active","created_at":"2026-03-04T04:22:31.590Z","source":"extraction","content_hash":"a002e32aff4d3f4b","confidence":1,"last_reinforced":"2026-03-04T04:38:52.705Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"KeFBIWrVBLfC"}
{"_type":"fact","id":"nAuZIxtmugpB","mind":"default","section":"Architecture","content":"MeshVPNService at src/styrened/services/mesh_vpn.py: WireGuard tunnels bootstrapped over RNS.Link. IPv6 ULA mesh IPs (fd73:7479:7265:6e65::/64) derived as interface ID from SHA-256 of identity hash — no collisions possible at any practical scale. WG key exchange over DirectLinkService /vpn/handshake path. Platform-aware: Linux kernel WG (ip+wg), macOS userspace (wg-quick). Gateway mode bridges wg-styrene into bat0. Endpoint auto-detected via UDP socket if not configured.","status":"active","created_at":"2026-03-04T04:22:31.590Z","source":"extraction","content_hash":"fe3f033b263ba9dc","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"PvZ6zB3szfhw"}
{"_type":"fact","id":"HU6U3v5udFak","mind":"default","section":"Architecture","content":"MeshVPNService topology modes: 'point-to-point' (no gateway peer), 'routed' (has a gateway peer with /64 allowed-ips), 'gateway' (this node is the gateway). get_status() returns mode, has_gateway, is_gateway, subnet fields. handle_peer_lost() removes WG peer and logs degradation warning when a gateway goes offline.","status":"active","created_at":"2026-03-04T04:22:31.592Z","source":"extraction","content_hash":"0f9ea35c683e1fb1","confidence":1,"last_reinforced":"2026-03-04T04:22:31.592Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"TR0dtOduVUfz","mind":"default","section":"Architecture","content":"WireGuard mesh VPN uses IPv6 ULA /64 subnets — macOS wg-quick config uses Address={mesh_ip}/64, Linux ip/wg uses /64. Subnet prefix exchanged in handshake payload to detect fleet isolation mismatches (peer_prefix != our_prefix returns error response with our_prefix).","status":"active","created_at":"2026-03-04T04:24:26.854Z","source":"extraction","content_hash":"5e74bdb06181432b","confidence":1,"last_reinforced":"2026-03-04T04:29:01.588Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ARua_ii_lUXX","mind":"default","section":"Architecture","content":"Mesh subnet: RFC 4193 ULA fd73:7479:7265:6e65::/64 ('styrene' ASCII hex). Interface ID (64 bits) from SHA-256 of identity hash. derive_mesh_ip(identity_hash, subnet_prefix) returns e.g. 'fd73:7479:7265:6e65:a1b2:c3d4:e5f6:7890'. subnet_prefix in MeshVPNConfig defaults to 'fd73:7479:7265:6e65' but is configurable per-fleet. Handshake now includes subnet_prefix field to detect cross-fleet IP collisions.","status":"active","created_at":"2026-03-04T04:24:26.854Z","source":"extraction","content_hash":"d44bb85f6a2f4692","confidence":1,"last_reinforced":"2026-03-04T04:29:01.588Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"zAJj2z8LNm9S"}
{"_type":"fact","id":"gOwpYDCDIEmB","mind":"default","section":"Architecture","content":"build_handshake_request/response now include subnet_prefix field derived via mesh_ip.rsplit(':', 4)[0] — extracts first 4 colon-groups as the /64 prefix. parse_handshake_request/response do NOT extract or validate subnet_prefix from the payload. See src/styrened/services/mesh_vpn.py.","status":"active","created_at":"2026-03-04T04:29:01.589Z","source":"extraction","content_hash":"0d8e68ada94c4cbc","confidence":1,"last_reinforced":"2026-03-04T04:29:01.589Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"GKtq1XgcD-JI","mind":"default","section":"Architecture","content":"NodeInfoPanel col_width=44 is hardcoded, meaning the two-column layout does not adapt to the actual widget width (~136 chars at 140-col terminal). Right column always starts at char 44 regardless of available space. #node-info-panel has max-height:28 and padding:0 1.","status":"active","created_at":"2026-03-04T04:45:00.314Z","source":"extraction","content_hash":"69f52586677d4d33","confidence":1,"last_reinforced":"2026-03-04T04:45:00.314Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"UaD-Q0uTwP2u","mind":"default","section":"Architecture","content":"mesh_vpn.py extract_prefix(mesh_ip, prefix_len=64) helper: uses ipaddress.IPv6Network(f'{mesh_ip}/{prefix_len}', strict=False).network_address.exploded to split into groups, returns ':'.join(parts[:prefix_len//16]). Replaces all rsplit-based prefix extraction.","status":"active","created_at":"2026-03-04T04:55:37.516Z","source":"extraction","content_hash":"d9492c1fd3cbade5","confidence":1,"last_reinforced":"2026-03-04T05:19:57.259Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"30Lffwj5XzCV","mind":"default","section":"Architecture","content":"mesh_vpn.py _create_interface_linux() uses mkstemp+fchmod(S_IRUSR|S_IWUSR)+os.write for secure private key temp file handling (not NamedTemporaryFile which creates world-readable file briefly).","status":"active","created_at":"2026-03-04T04:55:37.516Z","source":"extraction","content_hash":"c41a29eaedbf3958","confidence":1,"last_reinforced":"2026-03-04T05:25:23.529Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"C4U3vfg2nPvk","mind":"default","section":"Architecture","content":"MeshVPNService._on_peer_config_done(task) is a @staticmethod callback registered via task.add_done_callback() — logs task.exception() for unobserved exceptions from _configure_peer() background tasks.","status":"active","created_at":"2026-03-04T04:55:37.516Z","source":"extraction","content_hash":"78907ea87f5b2c70","confidence":1,"last_reinforced":"2026-03-04T05:07:53.941Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kSFwYZ1dD1M0","mind":"default","section":"Architecture","content":"DirectLinkService gained generic request(path, data, timeout) method; request_status() now delegates to it. Enables MeshVPNService to send /vpn/handshake via DirectLinkService.request(). See src/styrened/services/direct_link.py.","status":"active","created_at":"2026-03-04T05:07:53.940Z","source":"extraction","content_hash":"675caae07ef2e46a","confidence":1,"last_reinforced":"2026-03-04T05:07:53.940Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lDE6Ro8zJ6H_","mind":"default","section":"Architecture","content":"MeshVPNService BATMAN-ADV integration uses VXLAN-over-WireGuard: bat0→vxlan-<peer_prefix> (L2/UDP, per-peer)→wg-styrene (L3). _create_vxlan_for_peer()/destroy add/remove per-peer VXLAN interfaces. VXLAN_VNI=7379, dstport=4789. Only Linux/gateway mode. _vxlan_name(identity_hash)='vx-{hash[:10]}'. Old _add_to_batman/_remove_from_batman removed. See src/styrened/services/mesh_vpn.py.","status":"active","created_at":"2026-03-04T05:16:01.970Z","source":"extraction","content_hash":"964f6b0c1e2566b0","confidence":1,"last_reinforced":"2026-03-04T05:16:01.970Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"g08hoyFhEzGm"}
{"_type":"fact","id":"6zZxwTHU1uO6","mind":"default","section":"Architecture","content":"mesh_vpn.py macOS strategy: only wg-quick up/down require root (once, sudo credential cached 5min); subsequent wg set peer calls work without root via wireguard-go userspace socket at /var/run/wireguard/wg-styrene.sock. Interface creation prompts once, peer churn (handshakes) never re-prompts.","status":"active","created_at":"2026-03-04T05:25:23.529Z","source":"extraction","content_hash":"ab07aa6e99374e47","confidence":1,"last_reinforced":"2026-03-04T05:25:23.529Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6YrkVWX2sOud","mind":"default","section":"Constraints","content":"Non-human entities shall not receive author or collaborator credit - no Co-Authored-By trailers for AI tools in git commits","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"d53cf01cc38d24c2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"fs1LXKatIzRd","mind":"default","section":"Constraints","content":"Public hub deployment uses :latest tag (not version tag): kubectl deploy pulls ghcr.io/styrene-lab/public-hub:latest. Must push both version tag AND :latest when updating. Build with --no-cache to bust pip install cache for new PyPI versions.","status":"active","created_at":"2026-03-04T01:13:43.612Z","source":"manual","content_hash":"32239b592d6b49d2","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pJuu44Tg-APS","mind":"default","section":"Decisions","content":"styrened is adopting full authority as the primary interface for Reticulum configuration - the TUI Network tab will become a proper Reticulum interface manager for TCPClientInterface peers, AutoInterface toggle, and transport mode","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"5130561e1b6c1216","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"FMJZPtiJei1F","mind":"default","section":"Decisions","content":"Settings Network tab redesigned (2026-03-01): Replaced vestigial Hub selector (LXMF propagation layer) with proper Reticulum interface manager — Peers list (TCPClientInterface), AutoInterface toggle, Server interface, Transport mode. Save writes both core-config.yaml and regenerates ~/.reticulum/config. Hub LXMF fields (hub_enabled, hub_address, hub_announce_interval) remain in CoreConfig but are no longer exposed in settings UI.","status":"active","created_at":"2026-03-01T14:14:11.084Z","source":"manual","content_hash":"ca34db671416b4e4","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"omokzGZQzizO","mind":"default","section":"Decisions","content":"styrene-pypi dependency on styrened was changed to ~=0.10 compatible release specifier to fix publish race condition, but this caused pipx upgrade to not pull newer styrened (0.10.40 satisfies ~=0.10). The sync-version.yml workflow uses >=X.Y.Z pins which are semantically correct but require the workflow to run after styrened publishes. Both approaches have tradeoffs being resolved.","status":"active","created_at":"2026-03-01T15:10:06.595Z","source":"extraction","content_hash":"a5fc647a4b659c87","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"I2ESoKbhbIO4"}
{"_type":"fact","id":"5eZdg_064IUp","mind":"default","section":"Decisions","content":"styrene-pypi sync-version.yml cron changed from every 6 hours to hourly during active development period","status":"active","created_at":"2026-03-01T15:27:28.781Z","source":"extraction","content_hash":"e8bbb5c5e6accd93","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"B8hhmP-riu1O","mind":"default","section":"Decisions","content":"Announce interval changed from raw numeric Input to Select widget with 14 human-readable presets (15s through 1d). Uses _match_announce_interval() helper to map current config value to nearest preset. ANNOUNCE_INTERVALS constant defined at module level.","status":"active","created_at":"2026-03-01T16:14:58.144Z","source":"extraction","content_hash":"9a49b4e14185dfd9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8KXtJ0QpkV71","mind":"default","section":"Decisions","content":"TUI layout redesign underway: new CSS class hierarchy (.setting-input, .setting-input-narrow, .setting-select, .setting-checkbox) ensures widget width is proportional to semantic content size. Labels fixed-width right-aligned at 22 chars. This establishes the pattern before any Rust/ratatui port.","status":"active","created_at":"2026-03-01T16:14:58.145Z","source":"extraction","content_hash":"e109d0f7c03b66d5","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"m9GrHkEmqXCO"}
{"_type":"fact","id":"LoDw0J4lWLCA","mind":"default","section":"Decisions","content":"Announce interval UI now uses Select widget with 14 preset options (15s, 30s, 45s, 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h, 4h, 8h, 1d) via ANNOUNCE_INTERVALS constant. Replaced raw numeric Input widget. Uses _match_announce_interval() to find nearest preset for current config value.","status":"active","created_at":"2026-03-01T16:14:58.145Z","source":"extraction","content_hash":"79e355815453d7cd","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"zZxDlNAWIzuB"}
{"_type":"fact","id":"CWRCoXf-vta0","mind":"default","section":"Decisions","content":"TUI replaced all Checkbox widgets with Switch widgets across settings screens (v0.10.52) — Textual Switch renders as proper sliding toggle instead of X-in-a-box character. All query_one/query calls updated from Checkbox to Switch.","status":"active","created_at":"2026-03-01T16:30:29.812Z","source":"extraction","content_hash":"4c842d9ea25ec4c7","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"jBV2V-SUcu35"}
{"_type":"fact","id":"UOQILUYrHFfF","mind":"default","section":"Decisions","content":"Hierarchical mesh device tree implemented: MeshDevice.discovered_via populated from RNS.Transport.path_table[dest_hash][5].name post-announce. Dashboard uses Textual Tree widget with devices grouped under interface branches (named interfaces sorted alphabetically, 'Direct / Unknown' last). Tree leaf data carries device identity for selection/detail navigation.","status":"active","created_at":"2026-03-01T17:07:02.622Z","source":"extraction","content_hash":"66a5b1669bcaeaed","confidence":1,"last_reinforced":"2026-03-02T21:30:04.882Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"FgSegTxR6qtz"}
{"_type":"fact","id":"NyK4eDXojhw9","mind":"default","section":"Decisions","content":"Default peer config being expanded from just Styrene Community Hub (rns.styrene.io:4242) to include well-known upstream RNS community hubs. Operators on first launch should see Styrene hub plus ability to see/choose from well-known shared interfaces.","status":"active","created_at":"2026-03-01T18:16:43.038Z","source":"extraction","content_hash":"d47c646be1f3cb31","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"B-5H0ym2AAAF","mind":"default","section":"Decisions","content":"UpgradeScreen replaced HighlightedPanel with plain bordered Vertical container in v0.10.74 — HighlightedPanel's custom border Statics (hp-top/hp-bottom) are fundamentally incompatible with ModalScreen overlay contexts. Uses native Textual border:solid $panel, centered Static title, background:$surface.","status":"active","created_at":"2026-03-01T18:26:14.276Z","source":"extraction","content_hash":"25eaa19032a894eb","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"fh0gV5xO8l3F"}
{"_type":"fact","id":"vmlCTvf0IJCJ","mind":"default","section":"Decisions","content":"JXL (JPEG XL) accepted as native Styrene image format. Key factors: best compression at ≤50KB (mesh sweet spot), lossless JPEG repack (unique — 20% smaller, bit-exact reversible), progressive decode for slow links, pure Rust decoder (jxl-oxide 0.12, actively maintained), clean BSD-3 license. WebP rejected (less efficient at ≤50KB, no JPEG repack, pure Rust zenwebp only 0.3). AVIF rejected (patent complexity, 3x slower ARM decode, fragmented Rust crates). JPEG accepted as universal fallback.","status":"active","created_at":"2026-03-02T04:14:33.286Z","source":"extraction","content_hash":"a07ef067ff17a36e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"oZt_bI7U51Rj"}
{"_type":"fact","id":"IeIkbxhVzOP7","mind":"default","section":"Decisions","content":"Image format alternatives assessment completed and documented at styrene-rs/docs/image-format-assessment.md. Rejected: HEIF (patents, no Rust), AVIF (patent complexity, slow ARM decode, fragmented Rust), QOI (worse than JPEG at ≤50KB), FLIF (dead), BPG (dead, H.265 patents), WebP (not best at ≤50KB, no JPEG repack, less mature pure Rust). WebP and JPEG remain accepted receive formats.","status":"active","created_at":"2026-03-02T04:14:33.287Z","source":"extraction","content_hash":"d778e4b375693eea","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Y1CkDtWBm3Jp","mind":"default","section":"Decisions","content":"PyO3 hybrid daemon approach accepted to supersede 'no FFI, no PyO3 bindings' stance. Enables incremental module-by-module migration from Python to Rust within the running styrened daemon, with per-module compatibility verification via dual-path test fixtures (same tests run against both Python and Rust implementations).","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"36587190f3dad107","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uEJAqw4HOPY-","mind":"default","section":"Decisions","content":"Rust migration phased: Phase 1 (data layer, no RNS coupling) — styrene_wire.py, node_store.py, mesh_device.py, conversation_service.py. Phase 2 (crypto/protocol) — pqc_session.py, file_transfer.py, rns_service.py. Phase 3 (RNS integration) — reticulum.py, lxmf_service.py. Never port: TUI, config, doctor, CLI, auto_reply.","status":"active","created_at":"2026-03-02T04:17:53.411Z","source":"extraction","content_hash":"be88b0e1c2dbb649","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"oxzsNPN_OIXd","mind":"default","section":"Decisions","content":"styrene-rs structural decisions documented: (S1) Replace Rc<RpcDaemon> with Arc for multi-threaded tokio runtime — Mutex fields already Send+Sync, only Rc prevents it. (S2) Extract MeshTransport trait from concrete 500+ LOC Transport struct for testability and backend swapping.","status":"active","created_at":"2026-03-02T04:17:53.412Z","source":"extraction","content_hash":"4de9c0fb2633155a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wpze74d2rO9x","mind":"default","section":"Decisions","content":"macOS menu bar agent placed inside styrened.tui.menubar subpackage (option C) — part of [tui] extras, not a separate package. Launched via 'styrened menubar' CLI subcommand. rumps is the menu bar library (0.4.0 latest).","status":"active","created_at":"2026-03-02T22:34:26.751Z","source":"extraction","content_hash":"2f46483059e16be0","confidence":1,"last_reinforced":"2026-03-02T22:34:26.751Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tnpV86uZf0GK","mind":"default","section":"Decisions","content":"styrened PageServerService auto-generates index.mu only when the pages directory is empty (_ensure_default_index). Hub's existing index.mu is safe — NomadNet owns the pages directory and it is pre-populated from demo pages baked into the image.","status":"active","created_at":"2026-03-03T21:09:19.377Z","source":"extraction","content_hash":"8770d896a26ea933","confidence":1,"last_reinforced":"2026-03-03T21:09:19.377Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rNU5M9aUhTbE","mind":"default","section":"Decisions","content":"Pages tab discovery uses two strategies: (1) 'pages' capability in Styrene announce app_data (field 8: nomadnet_dest hash) — preferred; (2) NodeStore identity_hash cross-match for NOMADNET_NODE entries. Strategy 1 resolves hub multi-process identity mismatch by embedding the NomadNet destination hash directly in the Styrene announce.","status":"active","created_at":"2026-03-03T21:55:44.831Z","source":"extraction","content_hash":"751f306a9598b97f","confidence":1,"last_reinforced":"2026-03-03T22:28:27.377Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"DJJqJsEk31IW"}
{"_type":"fact","id":"nGrtwQxmgf1n","mind":"default","section":"Decisions","content":"TUI auto-starts daemon on mount (v0.13.22): _check_daemon() tries _ping_daemon(), if fails runs _auto_start_daemon() via Popen detached, polls up to 8s. DaemonSetupScreen only shown if auto-start fails. Eliminates setup screen after upgrades and daemon crashes.","status":"active","created_at":"2026-03-04T01:13:43.131Z","source":"manual","content_hash":"37d3b959b6275d4a","confidence":1,"last_reinforced":"2026-03-04T01:15:01.020Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"b-56pPnuzUqf","mind":"default","section":"Decisions","content":"TCP reconnection detection debounced at 30s (v0.13.20): _RECONNECT_DEBOUNCE=30.0 on RNSService prevents reconnection storm when hub has many TCP peers. Previously id(iface) change on every poll triggered ~2500 reconnect events/2min; now ~2/min. Page browser no longer tears down active links on reconnect — just flags path re-discovery.","status":"active","created_at":"2026-03-04T01:13:43.239Z","source":"manual","content_hash":"c0529559f34c55dd","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rVkZydsjFReQ","mind":"default","section":"Decisions","content":"Status tab redesign proposed: two-column cascade-colored layout (NodeInfoPanel pattern) with Left (NODE capabilities/version/type from announce, MESH hops/via/last_seen, SYSTEM uptime/OS from RPC) and Right (NETWORK ip/services from RPC, STORAGE disk, RPC STATUS connection state). Announce data as always-present baseline, RPC as enrichment layer.","status":"active","created_at":"2026-03-04T02:54:28.015Z","source":"extraction","content_hash":"d6c86802e2556476","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ei70ef7r-hBF","mind":"default","section":"Decisions","content":"Status tab full redesign proposed: two-column cascade-colored layout showing MeshDevice announce data as always-present baseline (NODE: type/caps/version, MESH: hops/via/last_seen, IDENTITY) with RPC data layered as enrichment (SYSTEM: uptime/OS/arch, NETWORK: ip/services, STORAGE: disk, RPC STATUS: connection state). Refresh button removed — 'r' keybinding already bound. MeshDevice passed to DeviceStatusWidget.","status":"active","created_at":"2026-03-04T02:58:34.889Z","source":"extraction","content_hash":"9c674496c58d2e2f","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"QQcoLyGS9Atj","mind":"default","section":"Decisions","content":"Speedtest LoRa fixes implemented: RTT-adaptive payload selection (PAYLOAD_SET_FAST/MEDIUM/SLOW/MINIMAL), scaled timeouts (PATH=45s, LINK=45s, REQUEST=30s), per-payload timeout proportional to size/estimated-throughput, SPEEDTEST_DELAY_SLOW=2s for RTT>0.5s, early bail on failed transfer, idle timeout increased to 15min. Progress still not streamed via IPC events (future work).","status":"active","created_at":"2026-03-04T03:13:06.962Z","source":"extraction","content_hash":"cfa02776703ddaeb","confidence":1,"last_reinforced":"2026-03-04T03:13:06.962Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"eGDN6LOee1HQ"}
{"_type":"fact","id":"W5r_87DMyLbn","mind":"default","section":"Decisions","content":"TAP-over-RNS.Link tunnel lifecycle must be managed by a privileged sidecar or via styrened.nix granting AmbientCapabilities=CAP_NET_ADMIN to the styrened service unit — two viable paths: (1) extend styrened.nix with AmbientCapabilities + pyroute2 netlink calls in Python, (2) a separate 'styrene-tunnel' helper binary (setuid or systemd PrivilegedGroup) that styrened communicates with over a Unix socket.","status":"active","created_at":"2026-03-04T04:02:43.624Z","source":"extraction","content_hash":"592b4345aff5d640","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"PhZ9z7Oaa4JX","mind":"default","section":"Decisions","content":"macOS nodes (operator workstations) participate in the mesh via RNS over TCP (tier 1 WireGuard gateway path), not via batman-adv. WireGuard to a gateway node bridges into bat0 for IP access. RNS.Link L2 tunneling reserved for LoRa-only nodes with no IP path — the gap WireGuard can't fill.","status":"active","created_at":"2026-03-04T04:09:41.628Z","source":"extraction","content_hash":"c7123c3452f33aeb","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"FSGIilzNJXjW","mind":"default","section":"Decisions","content":"Mesh VPN tier 1 implementation: use RNS.Link (DirectLinkService) to exchange WireGuard keys and endpoints, then WireGuard handles IP traffic. Gateway nodes bridge wg-styrene into bat0 via batctl. Mac connects via WireGuard to gateway, no batman-adv needed on macOS.","status":"active","created_at":"2026-03-04T04:12:37.251Z","source":"extraction","content_hash":"8e4a20ca25953384","confidence":1,"last_reinforced":"2026-03-04T04:12:37.251Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2JpPc35dHhaT","mind":"default","section":"Decisions","content":"Mesh IP space uses IPv6 ULA (RFC 4193) fd73:7479:7265:6e65::/64. Each node derives its /128 from SHA-256 of RNS identity hash — stable, collision-free, works point-to-point or with BATMAN gateway. When gateway present: Macs route full /64 through it into bat0. When gateway dies: direct /128 routes still work (graceful degradation to point-to-point).","status":"active","created_at":"2026-03-04T04:22:31.591Z","source":"extraction","content_hash":"66b1a8c4462fccd9","confidence":1,"last_reinforced":"2026-03-04T04:22:31.591Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"QCmgwf-sfu74"}
{"_type":"fact","id":"A36-2HG8Vfi0","mind":"default","section":"Decisions","content":"macOS mesh VPN privilege model: call sudo wg-quick up once at VPN start (triggers password prompt at most once per 5min window), then use wg set peer without sudo for all per-peer operations. This avoids repeated prompts as mesh nodes come online.","status":"active","created_at":"2026-03-04T05:25:23.529Z","source":"extraction","content_hash":"eee4572a98e0464e","confidence":1,"last_reinforced":"2026-03-04T05:25:23.529Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pfBnA0wZsxf8","mind":"default","section":"Known Issues","content":"Network settings changes require daemon restart to take effect - the RNS config is regenerated on save but the running Reticulum instance is not reloaded","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"fbae3f955f9b8d8b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"nb61YAvke_gx","mind":"default","section":"Known Issues","content":"GitHub Dependabot found 1 high-severity vulnerability on styrened's default branch (as of 2026-03-01)","status":"active","created_at":"2026-03-01T14:36:14.011Z","source":"extraction","content_hash":"e97a1ae20ad5bc49","confidence":1,"last_reinforced":"2026-03-03T20:42:53.971Z","reinforcement_count":37,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ktqoxJHBgbzR","mind":"default","section":"Known Issues","content":"TUI Network tab peer rows allocate disproportionate space to host/IP field while peer name is truncated — community hub label (e.g., rns.styrene.io) appears as truncated gross label despite available real estate","status":"active","created_at":"2026-03-01T14:52:15.557Z","source":"extraction","content_hash":"648a72dbecbeec10","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":8,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rLqiVDLi3QVM","mind":"default","section":"Known Issues","content":"Identity at ~/.config/styrene/identity is separate from RNS transport identity in ~/.reticulum/storage/. The upgrade path (pipx upgrade + pkill daemon) does NOT touch identity files. User-reported identity loss on upgrade needs investigation — may be related to ~/.reticulum/config regeneration causing RNS storage directory recreation","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"7f3f92b258b287f8","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ft_fzZmUet2-","mind":"default","section":"Known Issues","content":"Users may get a new RNS transport identity upon upgrade to v0.10.42+. Operator identity (~/.config/styrene/identity) is NOT affected by upgrade. The upgrade path (pipx upgrade styrene + pkill daemon) doesn't touch config/identity. Possible cause: config save regenerating ~/.reticulum/config which may trigger RNS storage recreation on restart. Needs investigation.","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"7398ade0979688fa","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"FqoKcyUN177M"}
{"_type":"fact","id":"mw59kQdPr0k0","mind":"default","section":"Known Issues","content":"TUI Network tab had CSS rendering bugs: v0.10.40 port/remove-btn truncated (fixed with wider min-widths), v0.10.42 peer name truncated at 20 chars (fixed in v0.10.43 by switching to proportional 2fr sizing and adding column headers)","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"f27a7216e5c2b00c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"o7hHGTQJaiJ6"}
{"_type":"fact","id":"nZcHWNsJnXY2","mind":"default","section":"Known Issues","content":"styrene-pypi publish workflow has a race condition: smoke test does pip install of the wheel which requires styrened>=X.Y.Z from PyPI, but styrened hasn't been published yet by Argo CI. All styrene-pypi releases after 0.10.40 failed (0.10.41-0.10.44) because of this.","status":"active","created_at":"2026-03-01T15:02:17.861Z","source":"extraction","content_hash":"bf5b7da060836e01","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"v23UxwOTJ3ic","mind":"default","section":"Known Issues","content":"TUI upgrade restart implemented via os.execvp in _restart_tui() method (v0.10.44) — finds styrene entry point and replaces current process. Uses atexit handler to exec after Textual event loop teardown. However, upgrade itself fails because styrene-pypi is stuck at 0.10.40 on PyPI.","status":"active","created_at":"2026-03-01T15:02:17.862Z","source":"extraction","content_hash":"60dbbe599ea444ae","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"wQ03_rQUwSMr"}
{"_type":"fact","id":"96QneSZFMbKf","mind":"default","section":"Known Issues","content":"styrene-pypi publish race condition (fixed 2026-03-01): Meta-package was pinning `styrened>=X.Y.Z` with exact version, but its GitHub Actions publish ran before the Argo styrened publish completed. The smoke test failed because styrened wasn't on PyPI yet. Fixed by using `~=0.10` compatible release specifier instead of exact version floor.","status":"active","created_at":"2026-03-01T15:03:12.073Z","source":"manual","content_hash":"8002982f8bf34c11","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"yPZv8RGSfSLA","mind":"default","section":"Known Issues","content":"pipx upgrade styrene with ~=0.10 specifier doesn't upgrade styrened because pip's default --upgrade-strategy=only-if-needed sees existing 0.10.40 as satisfying the constraint. Fix: _build_upgrade_cmd() now passes --pip-args='--upgrade-strategy=eager' to force transitive dep upgrades.","status":"active","created_at":"2026-03-01T15:10:06.594Z","source":"extraction","content_hash":"0820bbd6f4fb55d5","confidence":1,"last_reinforced":"2026-03-02T21:30:04.883Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mJ1hVHS9QSFG","mind":"default","section":"Known Issues","content":"pipx upgrade styrene to 0.10.45 upgrades the meta-package but leaves styrened at 0.10.40 in the venv — pip's default --upgrade-strategy=only-if-needed doesn't pull transitive deps when the constraint (~=0.10) is already satisfied. The eager strategy fix in 0.10.45 is a catch-22: users need 0.10.45 installed to get the fix that would have installed 0.10.45.","status":"active","created_at":"2026-03-01T15:28:20.788Z","source":"extraction","content_hash":"aab975ca0652ed94","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"_PD1tAgM5AFt","mind":"default","section":"Known Issues","content":"on_add_peer mounted new peer rows inside Add Peer button's parent Horizontal instead of hp-content Vertical, causing horizontal stacking — fixed in v0.10.47 by mounting into add_btn_row.parent before the button row","status":"active","created_at":"2026-03-01T15:59:52.149Z","source":"extraction","content_hash":"9b849da2a6e1004e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XruKUYHxxq83","mind":"default","section":"Known Issues","content":"TUI Settings Network tab peer row horizontal stacking bug (delete+re-add) was caused by on_add_peer mounting into wrong container (button's parent Horizontal instead of hp-content Vertical) — fixed in v0.10.47","status":"active","created_at":"2026-03-01T15:59:52.149Z","source":"extraction","content_hash":"67a87b586bdc7f0d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"uwTQP_TNLa3q"}
{"_type":"fact","id":"a5J8xavcDSpQ","mind":"default","section":"Known Issues","content":"pipx upgrade --pip-args passed as two separate list elements ('--pip-args', '--upgrade-strategy=eager') instead of single combined arg ('--pip-args=--upgrade-strategy=eager'), causing pipx to not apply eager strategy — fix in progress for v0.10.47+","status":"active","created_at":"2026-03-01T15:59:52.149Z","source":"extraction","content_hash":"2291e4d22ab6b47a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"PQ9MlGLu05Zx","mind":"default","section":"Known Issues","content":"styrene-pypi sync-version.yml created GitHub releases via GITHUB_TOKEN which can't trigger other workflows (GitHub anti-cascade protection). This broke the release→publish event chain. Fixed 2026-03-01 by merging build+publish directly into sync-version.yml instead of relying on a separate publish.yml triggered by release event.","status":"active","created_at":"2026-03-01T16:08:22.610Z","source":"manual","content_hash":"861fd385c5816ec0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"i5jhyBH27NJy","mind":"default","section":"Known Issues","content":"styrene-pypi sync-version.yml creates GitHub releases via GITHUB_TOKEN (github-actions[bot]), which cannot trigger the separate publish.yml workflow due to GitHub's anti-cascade protection. This broke automatic PyPI publishing for synced versions.","status":"active","created_at":"2026-03-01T16:08:27.288Z","source":"extraction","content_hash":"f5f81bf7ec5cb691","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tcJlk3RdT3M4","mind":"default","section":"Known Issues","content":"styrened 0.10.48 and styrene 0.10.48 both confirmed on PyPI (2026-03-01T16:05Z). Previous issue of version mismatch (styrene 0.10.45 with styrened 0.10.40) resolved by eager upgrade strategy in styrened 0.10.45+.","status":"active","created_at":"2026-03-01T16:08:27.289Z","source":"extraction","content_hash":"c8e062a479f07761","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"qdW9X-QpnRD9"}
{"_type":"fact","id":"vnkyJasX9LGY","mind":"default","section":"Known Issues","content":"styrene meta-package 0.10.48 on PyPI uses >=0.10.48 specifier for styrened dependency (bumped by sync-version.yml). Eager upgrade strategy built into styrened 0.10.45+ handles transitive dep upgrades via pipx.","status":"active","created_at":"2026-03-01T16:08:27.289Z","source":"extraction","content_hash":"13728b0bea9dc4d6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"nVxU5D_I914f"}
{"_type":"fact","id":"7nllX1SwT9LG","mind":"default","section":"Known Issues","content":"styrene-pypi 0.10.48 and styrened 0.10.48 both on PyPI. The >=0.10.48 pin in meta-package plus eager upgrade strategy in styrened's _build_upgrade_cmd() should resolve the version drift issue where pipx left styrened at older version.","status":"active","created_at":"2026-03-01T16:08:27.289Z","source":"extraction","content_hash":"a92ecdf44c40e594","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"Ezhz7_nzE3gD"}
{"_type":"fact","id":"O6buNmXiUMCh","mind":"default","section":"Known Issues","content":"styrene meta-package was stuck at 0.10.40 on PyPI (versions 0.10.41-0.10.44 failed due to smoke test race condition). Fixed with ~=0.10 specifier, then sync-version.yml bumped to 0.10.45, then 0.10.48. Now current.","status":"active","created_at":"2026-03-01T16:08:27.289Z","source":"extraction","content_hash":"aa4c18a8f9d47c5d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"3hmtSqzYV7Kt"}
{"_type":"fact","id":"EsqJu25MXoaE","mind":"default","section":"Known Issues","content":"sync-version.yml correctly detected 0.10.48 > 0.10.45 and bumped. However the release created by github-actions[bot] via GITHUB_TOKEN didn't trigger publish.yml (anti-cascade). Fixed by merging publish step directly into sync-version.yml.","status":"active","created_at":"2026-03-01T16:08:27.290Z","source":"extraction","content_hash":"dafcd5df4ea7a2bc","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"pXbbjuFWmKg_"}
{"_type":"fact","id":"swJSfGyS90o6","mind":"default","section":"Known Issues","content":"TUI first-run config generator (tui/services/config.py) had hardcoded 192.168.0.102 (internal LAN IP) for community hub peer instead of rns.styrene.io. Fixed in v0.10.49. The daemon-side config generator (services/config.py) already had the correct hostname.","status":"active","created_at":"2026-03-01T16:13:12.910Z","source":"extraction","content_hash":"f6b781282c7a6ca7","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qCazU7xbOKqR","mind":"default","section":"Known Issues","content":"TUI settings screen layout problem (all widgets stretching full width via .setting-input class) is being fixed: checkboxes now use .setting-checkbox (auto width), selects use .setting-select (width:40), numeric inputs use .setting-input-narrow (width:16). Announce interval changed from raw Input to preset Select. Work in progress for v0.10.50.","status":"active","created_at":"2026-03-01T16:14:58.144Z","source":"extraction","content_hash":"e8bf433282283552","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"Fh7iA-1M2ARy"}
{"_type":"fact","id":"2H8ZAvOoB0ox","mind":"default","section":"Known Issues","content":"TUI settings layout regression in v0.10.50: CSS class changes (.setting-input, .setting-input-narrow, .setting-select, .setting-checkbox) caused visual regression from the previously-fixed state — controls not rendering at expected widths despite class assignments","status":"active","created_at":"2026-03-01T16:19:52.407Z","source":"extraction","content_hash":"b73e7963e628ec87","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"v8CVLKvcpKGk","mind":"default","section":"Known Issues","content":"Potential CSS fall-through risks identified in TUI audit: ActivityFeedWidget RichLog has no explicit background (inherits from Textual DEFAULT_CSS which uses $surface), ForgeLog scroll areas have no background set, ConfigForm uses no explicit background, multiple screen-level Statics set background:$background explicitly which could mismatch if theme variable changes","status":"active","created_at":"2026-03-01T16:32:58.196Z","source":"extraction","content_hash":"3246e3e8298d0a11","confidence":1,"last_reinforced":"2026-03-02T21:30:04.884Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"O1FZl_dExIhH","mind":"default","section":"Known Issues","content":"Textual TUI black bar root cause (fixed v0.10.54): Zero global scrollbar styling existed in imperial_crt.tcss. Every scrollable widget (Log, RichLog, VerticalScroll, overflow-y:scroll) fell through to Textual's built-in defaults which render scrollbar tracks as opaque black. Fix: global Screen scrollbar rules + blanket transparent background on all scrollable widget types + RichLog/Log background:transparent to inherit from parent chain.","status":"active","created_at":"2026-03-01T16:37:22.752Z","source":"manual","content_hash":"afc393ed3382dd5d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"STyuZQpKuWsy","mind":"default","section":"Known Issues","content":"pipx upgrade doesn't upgrade transitive deps by default. pipx upgrade styrene upgrades the meta-package but styrened stays at old version because pip's --upgrade-strategy=only-if-needed sees existing version as satisfying ~=0.10. Fix baked into styrened 0.10.48+: _build_upgrade_cmd() uses --pip-args=--upgrade-strategy=eager (single string, NOT two separate args). Users on pre-0.10.48 need one-time: pipx runpip styrene install --upgrade --upgrade-strategy=eager styrened","status":"active","created_at":"2026-03-01T16:37:51.300Z","source":"manual","content_hash":"27ff6e8999ad935a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CmaFYp0NGCyQ","mind":"default","section":"Known Issues","content":"TUI first-run config (tui/services/config.py) had hardcoded 192.168.0.102 LAN IP for community hub peer instead of rns.styrene.io. Fixed in v0.10.49. The daemon config (services/config.py) already had the correct public hostname. Both must be kept in sync when default peer config changes.","status":"active","created_at":"2026-03-01T16:38:03.636Z","source":"manual","content_hash":"9271009603a0d461","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uYVgk8wbVb4k","mind":"default","section":"Known Issues","content":"TUI Switch toggle widgets have invisible track/rail background — .setting-checkbox CSS sets background:transparent which causes the slider track to disappear against the dark screen background, leaving just a floating colored square handle with no visual context. Needs explicit track background color (e.g., $surface or $panel-darken-2) instead of transparent.","status":"active","created_at":"2026-03-01T16:40:15.044Z","source":"extraction","content_hash":"dd29e9d1f300e6e9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CgCBRtjCG6Ay","mind":"default","section":"Known Issues","content":"v0.10.55 Switch toggle track fix (border: tall $border, background: $background) was tagged and pushed but hadn't reached PyPI via Argo pipeline, leaving users on 0.10.54 with invisible toggle tracks — led to adding direct twine publish to justfile","status":"active","created_at":"2026-03-01T16:48:45.058Z","source":"extraction","content_hash":"dadf150df0e89a1f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uQXbv4PIIIi2","mind":"default","section":"Known Issues","content":"Switch toggle styling progression: v0.10.55 fixed invisible track, v0.10.57 fixed comically tall Switch (border:none, height:1), v0.10.58 fixed off-state indistinguishable handle (track=$background, off-handle=$panel, on-handle=$primary). Switch styling now correct for both states.","status":"active","created_at":"2026-03-01T17:02:47.647Z","source":"extraction","content_hash":"f9b8deaccfbfddd9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.885Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"s16REGvME4bn"}
{"_type":"fact","id":"LaZ7V-LdXNbW","mind":"default","section":"Known Issues","content":"TUI dashboard tests (test_dashboard_tui.py) still reference DataTable API (row_count, cursor_row) after MeshDeviceTree migration — sed replaced class names but DataTable-specific property assertions remain broken. Tests need full rewrite for Tree widget API.","status":"active","created_at":"2026-03-01T17:14:51.521Z","source":"extraction","content_hash":"87362fe7d7e722f2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IciOxoZ0PMrX","mind":"default","section":"Known Issues","content":"NodeInfoPanel on dashboard had max-height:16 CSS constraint causing RETICULUM and STYRENE sections to be clipped. Fixed in v0.10.61: two-column layout (left: SYSTEM/DAEMON/IDENTITY, right: RETICULUM/STYRENE/VERSION), max-height bumped to 20, styrened version now displayed. Rich markup side-by-side via regex-based padding in render().","status":"active","created_at":"2026-03-01T17:28:18.285Z","source":"extraction","content_hash":"3c4cee0ea11343cf","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"1mk6NiBcX7hk"}
{"_type":"fact","id":"4N1r8szIYlRK","mind":"default","section":"Known Issues","content":"Dashboard MeshDeviceTree row truncation fixed in v0.10.60 — identity hash removed from tree labels (visible in detail screen instead), announce count compacted to ×N suffix, unread badge uses ✉N format.","status":"active","created_at":"2026-03-01T17:28:18.286Z","source":"extraction","content_hash":"57d69f59cbe6ba95","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"z3yeAil4-Sgh"}
{"_type":"fact","id":"vFkQAPAD3KAJ","mind":"default","section":"Known Issues","content":"HighlightedPanel bottom border was invisible when panel used fr-based height (fixed v0.10.64): hp-content had `height: auto` which didn't expand inside fr-sized panels, pushing the hp-bottom Static below visible area. Fix: hp-content uses `height: 1fr`. Also: margin-bottom gap between panels was black because parent Container had no explicit background — fixed by adding `background: $background` to #dashboard-container.","status":"active","created_at":"2026-03-01T17:36:47.245Z","source":"manual","content_hash":"7286d58086d58cca","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tPNOZJGBh1G6","mind":"default","section":"Known Issues","content":"Dashboard HighlightedPanel had missing bottom border (hp-content height:auto didn't expand under fr-sized panels, pushing hp-bottom out of view) and black gaps between panels (margin-bottom:1 rendered in parent Container with no explicit background). Fixed in v0.10.64: hp-content changed to height:1fr, #dashboard-container given background:$background.","status":"active","created_at":"2026-03-01T17:36:58.544Z","source":"extraction","content_hash":"27afe6d6ccd78b3e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"e2ZORq8O5W6A","mind":"default","section":"Known Issues","content":"Textual Container widget has no default background in its DEFAULT_CSS — margin/padding gaps between child widgets fall through to black unless the container explicitly sets background. #dashboard-container now has background:$background.","status":"active","created_at":"2026-03-01T17:36:58.546Z","source":"extraction","content_hash":"33b10ed4861bd5b0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"nbSGyu93MQZI","mind":"default","section":"Known Issues","content":"Dashboard MeshDeviceTree refresh still broken after v0.10.63/v0.10.65 fixes. Tree.clear()+root.add()+_clear_line_cache()+refresh(layout=True) still does not visually update on 'r' keypress. Additionally, operator identity saved in settings is not reflected on dashboard — 'Anonymous Styrene' persists after save and return to main screen.","status":"active","created_at":"2026-03-01T17:47:39.091Z","source":"extraction","content_hash":"9d534c0eebed2569","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":"khpQXeUerXtK"}
{"_type":"fact","id":"HPNEi4bRpiKG","mind":"default","section":"Known Issues","content":"Textual Tree._clear_line_cache() + refresh(layout=True) still insufficient to force visual update after clear()+root.add() — stale rendering persists despite cache invalidation. May need _invalidate() or full widget replacement.","status":"active","created_at":"2026-03-01T17:47:39.092Z","source":"extraction","content_hash":"bc4bab2e83189f33","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lBjRgAX98omg","mind":"default","section":"Known Issues","content":"DashboardScreen._fetch_daemon_status() only pushes daemon_connected, daemon_uptime, and daemon_version to NodeInfoPanel — no identity, mesh count, or hub status data is sent via IPC. In local mode, identity_hash and security_tier were also missing but now fixed by loading operator identity from get_operator_identity() in _load_styrene_data().","status":"active","created_at":"2026-03-01T18:00:40.003Z","source":"extraction","content_hash":"4cba9ac4439d5578","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"014P1Pxvj5z3"}
{"_type":"fact","id":"7L2Num38EIYN","mind":"default","section":"Known Issues","content":"TUI app_lifecycle._announce_node() was using socket.gethostname() for announce display name instead of config.identity.display_name and config.identity.short_name. Fixed in v0.10.69 to use identity config values, matching daemon announce behavior. Socket import removed from app_lifecycle.py.","status":"active","created_at":"2026-03-01T18:02:39.952Z","source":"extraction","content_hash":"75c3aa1877583c78","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"dNeSuRQXi6to"}
{"_type":"fact","id":"DHgcyf9oa32q","mind":"default","section":"Known Issues","content":"get_operator_identity() and get_operator_identity_object() were file-only (bypassed YubiKey provider) — FIXED in v0.10.70: both now load IdentityConfig, check provider field, and derive from YubiKey when provider=='yubikey'. NodeInfoPanel security tier also shows 'YubiKey/FIDO2' for yubikey provider.","status":"active","created_at":"2026-03-01T18:06:40.422Z","source":"extraction","content_hash":"586d5ab60bf84721","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"u3oQ7eJiXWzL"}
{"_type":"fact","id":"pM4l8-mGT5uK","mind":"default","section":"Known Issues","content":"NodeInfoPanel.identity_hash populated in both IPC and local mode via get_operator_identity(). As of v0.10.70, get_operator_identity() is provider-aware — YubiKey identities are now correctly shown. Security tier displays 'YubiKey/FIDO2' when provider is yubikey.","status":"active","created_at":"2026-03-01T18:06:40.422Z","source":"extraction","content_hash":"013745abb073c761","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"gORUjMSxsbgr"}
{"_type":"fact","id":"EBAnyRkttPl0","mind":"default","section":"Known Issues","content":"Dashboard 'r' refresh key fix in v0.10.72: Textual 8.0 binding resolution gives focused widget (Tree) first crack at keys. Even without a key_r handler, the event was consumed before bubbling to Screen. Fix: Binding(\"r\", \"refresh\", \"Refresh\", priority=True) ensures Screen binding takes precedence. Also added visible 'Refreshed' toast notification for user feedback.","status":"active","created_at":"2026-03-01T18:16:43.037Z","source":"extraction","content_hash":"ee2e9a8a028f5f9e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"YgQMSU1dpOP-","mind":"default","section":"Known Issues","content":"Dashboard 'r' refresh keybinding fixed in v0.10.72 with priority=True on Binding. Root cause: Textual 8.0 binding resolution let focused Tree widget consume the key before it bubbled to Screen. Toast notification added for user feedback. Tree visual refresh after clear()+add() may still be a separate rendering issue.","status":"active","created_at":"2026-03-01T18:16:43.037Z","source":"extraction","content_hash":"556d7b41b295aab9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"1uqNkpYtORzq"}
{"_type":"fact","id":"frzyY0CuWOk7","mind":"default","section":"Known Issues","content":"HighlightedPanel height:auto doesn't properly propagate children with explicit heights (Textual limitation). Dashboard panels use height:1fr override via CSS specificity. Upgrade modal fixed in v0.10.71 with explicit height:24 on container + hp-content 1fr + max-height:80%.","status":"active","created_at":"2026-03-01T18:16:43.038Z","source":"extraction","content_hash":"524ca6c03a76c6bd","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"HezgE3q4hdCo"}
{"_type":"fact","id":"9Cw--KFhVz2W","mind":"default","section":"Known Issues","content":"Peer status dots in Network tab have been replaced with enabled/disabled Switch toggles. Live connection status (RNS.Transport.interfaces[n].online UP/DOWN indication) is still not wired — the Switch controls config-level enable/disable, not runtime connection state.","status":"active","created_at":"2026-03-01T18:18:24.438Z","source":"extraction","content_hash":"b2df177f659beb43","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"c_-Nzs5fiDzS"}
{"_type":"fact","id":"vTG_lPUAeSUv","mind":"default","section":"Known Issues","content":"UpgradeScreen HighlightedPanel layout was fundamentally broken — content rendered outside panel on ModalScreen. FIXED in v0.10.74: replaced HighlightedPanel with plain Vertical container using native Textual border:solid $panel, centered Static title, height:1fr on Log. No more custom border widget layout fights.","status":"active","created_at":"2026-03-01T18:26:14.275Z","source":"extraction","content_hash":"7221b71cd4a7888a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.886Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"U8vii5BYiVzX"}
{"_type":"fact","id":"scxOZFrwbCSk","mind":"default","section":"Known Issues","content":"UpgradeScreen HighlightedPanel layout was persistently broken across v0.10.66/v0.10.71/v0.10.73. RESOLVED in v0.10.74 by abandoning HighlightedPanel entirely — replaced with plain bordered Vertical container (border:solid $panel) + centered Static title + height:1fr Log. HighlightedPanel's custom border Statics are fundamentally incompatible with Textual ModalScreen layout.","status":"active","created_at":"2026-03-01T18:26:14.276Z","source":"extraction","content_hash":"df48129de797a2dc","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"bHqgBZoYHbKB"}
{"_type":"fact","id":"jt0aYB4WttB_","mind":"default","section":"Known Issues","content":"Config round-trip tests (test_nested_interfaces_round_trip, test_peer_config_round_trip) FIXED in v0.10.76 — tests updated to account for WELL_KNOWN_HUBS merge behavior, asserting explicit peers preserved at start + len(WELL_KNOWN_HUBS) disabled entries appended.","status":"active","created_at":"2026-03-01T18:35:33.230Z","source":"extraction","content_hash":"857f1c7d4699fe4e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"ELfgi6vHb2tB"}
{"_type":"fact","id":"IBBd5pQ37Xly","mind":"default","section":"Known Issues","content":"styrene-pypi sync-version.yml hourly cron was hitting stale PyPI CDN edges on GitHub Actions runners, causing it to see old styrened versions and skip sync. Versions 0.10.67–0.10.76 published to styrened but meta-package stayed at 0.10.66. Fixed by: (1) just publish fires repository_dispatch with exact version bypassing CDN, (2) cache-bust query param + Cache-Control:no-cache header added to PyPI JSON API query as defense-in-depth.","status":"active","created_at":"2026-03-01T18:53:14.159Z","source":"extraction","content_hash":"06048c249b9b4d2f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"JUeoJ-U0mtiy","mind":"default","section":"Known Issues","content":"styrene meta-package PyPI sync gap (0.10.66→0.10.76) was caused by stale PyPI CDN on GitHub Actions runners. Fixed: just publish now fires repository_dispatch to styrene-pypi with exact version. Cache-bust headers added to hourly cron fallback. Meta-package now at 0.10.76+ on PyPI.","status":"active","created_at":"2026-03-01T18:53:14.160Z","source":"extraction","content_hash":"5e2b62bccc096868","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"X0tiw8DxpIvW"}
{"_type":"fact","id":"PqKeHHfjctQA","mind":"default","section":"Known Issues","content":"User's config at ~/.styrene/config.yaml has hardcoded 192.168.0.200:4242 as Styrene Community Hub peer instead of rns.styrene.io — suggests config was generated before v0.10.49 fix and has never been regenerated.","status":"active","created_at":"2026-03-02T03:34:35.543Z","source":"extraction","content_hash":"2fcacb5c94c0391c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"N3RnNbGDBocD","mind":"default","section":"Known Issues","content":"Two config file locations exist: ~/.config/styrene/config.yaml (current, written by save_core_config) and ~/.styrene/config.yaml (legacy, 1276 bytes from Feb 23). Possible that TUI load_config() reads from legacy path while save writes to new path, or legacy path interferes with identity loading. Investigation in progress.","status":"active","created_at":"2026-03-02T03:35:51.713Z","source":"extraction","content_hash":"42d21466f2385431","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cWfFQ47-0Ks6","mind":"default","section":"Known Issues","content":"Before _overlay_core_config fix, TUI settings screen was showing DEFAULT values for ALL CoreConfig fields — empty peer list, no propagation destination, hub_enabled=False, enable_transport=None, identity='Anonymous Styrene'. Every settings save would overwrite core-config.yaml with these defaults, wiping real configuration. Fixed by replacing _overlay_core_identity() with _overlay_core_config() that loads full CoreConfig.","status":"active","created_at":"2026-03-02T04:51:08.866Z","source":"extraction","content_hash":"5d79fd418e9a1ec6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mg5WEjPsOPM4","mind":"default","section":"Known Issues","content":"tui.yaml contains a full stale copy of reticulum/rpc/discovery/api sections that duplicates core-config.yaml. The TUI's _parse_config_dict parses these, then _overlay_core_config discards them by replacing config.core entirely. The TUI's _serialize_config_dict still writes them back to tui.yaml. This duplication is dead weight but not harmful since core-config.yaml always wins.","status":"active","created_at":"2026-03-02T04:51:08.866Z","source":"extraction","content_hash":"f944eb45f78ca1e0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"_iqBefbpMWV9","mind":"default","section":"Known Issues","content":"TUI _config_to_dict() serializes peers to tui.yaml without the 'enabled' field (only host/port/name), losing PeerConfig.enabled state in tui.yaml. Not harmful since core-config.yaml (which uses core serializer with enabled field) is the source of truth after _overlay_core_config().","status":"active","created_at":"2026-03-02T05:04:28.780Z","source":"extraction","content_hash":"f6cdc82eb5d39632","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"PewUD7hLh0G2","mind":"default","section":"Known Issues","content":"NodeInfoPanel VERSION section was using local __version__ instead of self.daemon_version when connected via IPC — FIXED: now prefers self.daemon_version when set, falls back to __version__. Test test_render_ipc_connected updated accordingly.","status":"active","created_at":"2026-03-02T05:05:38.493Z","source":"extraction","content_hash":"286dcafcf91ad1e1","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"S8XkkiiJSNnG"}
{"_type":"fact","id":"cxvi7nqoRdna","mind":"default","section":"Known Issues","content":"test_mesh_device_tree_filters_old_lost_nodes in test_dashboard_tui.py PASSES — correctly expects 2 leaf nodes because _deduplicate_by_identity() filters devices older than 30 min (24h-old lost device filtered, active+stale within 30min kept). All 19 dashboard TUI tests pass.","status":"active","created_at":"2026-03-02T05:15:29.117Z","source":"extraction","content_hash":"8fe5df71477c99d7","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"zfsT4GI5C9Zf"}
{"_type":"fact","id":"OdfYF4Ppuecr","mind":"default","section":"Known Issues","content":"User's core-config.yaml has no identity section — previous settings saves (before _overlay_core_config fix) wiped identity from core-config.yaml by overlaying with defaults. User must re-enter display name in settings. config.yaml has display_name: '' (empty) and config.yaml.bak has display_name: hellolololololo.","status":"active","created_at":"2026-03-02T06:48:31.424Z","source":"extraction","content_hash":"9675d60eb7905bb4","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pDog4qInCiDP","mind":"default","section":"Known Issues","content":"FIXED in v0.10.81: Identity silently lost on TUI launch — _parse_config_dict never parsed identity/lxmf from config.yaml, and _overlay_core_config replaced config.core wholesale from core-config.yaml which had no identity section. Now parses legacy identity, auto-migrates to core-config.yaml, with 8 unit tests.","status":"active","created_at":"2026-03-02T06:54:34.681Z","source":"manual","content_hash":"c7179327fc3fdd2b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"N8Xj_3cDaXTO","mind":"default","section":"Known Issues","content":"_parse_config_dict now reads 'identity' section from config.yaml/tui.yaml. _overlay_core_config still replaces config.core entirely from core-config.yaml, but if core-config.yaml has only default identity AND legacy config had a real one, it migrates the legacy value forward and persists it to core-config.yaml via save_core_config.","status":"active","created_at":"2026-03-02T06:54:39.865Z","source":"extraction","content_hash":"e2e66f1e300df778","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"DmrSjETZofPe"}
{"_type":"fact","id":"nPShLHQ38inc","mind":"default","section":"Known Issues","content":"_overlay_core_config identity migration implemented and tested: if core-config.yaml yields default identity ('Anonymous Styrene') but the in-memory config (from legacy config.yaml) had a real one, the legacy value is preserved and auto-migrated to core-config.yaml. Verified with 3 unit tests in tests/unit/tui/test_identity_migration.py.","status":"active","created_at":"2026-03-02T06:54:39.866Z","source":"extraction","content_hash":"47773fb3e75e57d2","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"C8-a_GzXoYxD"}
{"_type":"fact","id":"Fcny6kfmX1X8","mind":"default","section":"Known Issues","content":"Minor ruff lint errors remain in src/styrened/tui/screens/settings.py (E402 module-level import after module-level code), tui/widgets/node_info_panel.py (B905 zip without strict=), and test files (E741 ambiguous variable names, F841 unused variable) — being cleaned up incrementally","status":"active","created_at":"2026-03-02T16:14:00.939Z","source":"extraction","content_hash":"f1be3067485f6856","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"KyDYOcRAH3W3","mind":"default","section":"Known Issues","content":"ConversationScreen chat input bar had border rendering issues: Input widget's default 'border: tall $border-blurred' and focus override were not being suppressed, causing visible bounding box around input and gap between input bar and Footer. Fixed in v0.11.2 with 'border: none !important' on both normal and focus states, height:1 on Input, and removing padding from container to Input.","status":"active","created_at":"2026-03-02T22:19:52.804Z","source":"extraction","content_hash":"0046a400f0bb3817","confidence":1,"last_reinforced":"2026-03-02T22:24:08.654Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"UsK0Bod-hjWy","mind":"default","section":"Known Issues","content":"memory_store tool returned 'Project memory not initialized' at session start (2026-03-02) — resolved by re-initializing memory infrastructure. Manually-appended jsonl entries were cleaned up after tool was restored.","status":"active","created_at":"2026-03-02T22:19:52.805Z","source":"extraction","content_hash":"e3dc0797859a34af","confidence":1,"last_reinforced":"2026-03-02T22:19:52.805Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8ARNyCPLtpN6","mind":"default","section":"Known Issues","content":"Failed message delivery in chat was visually indistinct — same color as normal messages, only italic text style. Fixed in v0.11.3: failed messages render red italic with bold red ✗ icon, applied to initial render, live status updates (_update_bubble_status), and optimistic send failures (_update_optimistic_status).","status":"active","created_at":"2026-03-02T22:24:08.653Z","source":"extraction","content_hash":"bcfb9cea0f972dbf","confidence":1,"last_reinforced":"2026-03-02T22:24:08.653Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"DTo8sJun7wDw","mind":"default","section":"Known Issues","content":"ContactsScreen presence/last-message display was missing — FIXED in v0.11.4: columns now show STATUS (●/◐/○ with relative time), LAST MESSAGE (preview + relative timestamp), cross-referencing get_devices() and get_conversations() from IPCBridge.","status":"active","created_at":"2026-03-02T22:34:26.751Z","source":"extraction","content_hash":"15833275d9423a83","confidence":1,"last_reinforced":"2026-03-02T22:34:26.751Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"sv7FB1PRkctH"}
{"_type":"fact","id":"PzRF4g0RNytV","mind":"default","section":"Known Issues","content":"Q25VyHe0cipc — clipboard image paste workflow now implemented; screenshots via Cmd+Shift+4→clipboard detected via NSPasteboardTypePNG and staged automatically on paste.","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"d592d10807d16b8a","confidence":1,"last_reinforced":"2026-03-02T22:52:57.809Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"R4if8RsQb9JD","mind":"default","section":"Known Issues","content":"TIFF→PNG conversion in clipboard.py requires Pillow — if Pillow is not installed the conversion silently returns None and TIFF clipboard content is skipped. PNG path still works.","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"9d8630b6d28e1bc2","confidence":1,"last_reinforced":"2026-03-02T22:52:57.809Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"7Y-BEc4cBsSb","mind":"default","section":"Known Issues","content":"TestPasteHandler tests updated to patch has_clipboard_image/read_clipboard_attachment at chat_widget module level; pdf/txt/md/zip/tar/gz path paste now prevents default (expanded file support), so test_paste_pdf_path_does_not_prevent_default was renamed to test_paste_pdf_path_prevents_default.","status":"active","created_at":"2026-03-02T22:54:56.414Z","source":"extraction","content_hash":"d2fa8e218910bd17","confidence":1,"last_reinforced":"2026-03-02T22:54:56.414Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XsdjZia4dk_1","mind":"default","section":"Known Issues","content":"Textual on_paste event only receives text content — binary clipboard image data never reaches on_paste handlers. Ctrl+V binding is consumed by Input widget before widget-level bindings fire. This makes clipboard image attachment via paste/ctrl+v impossible in the current implementation.","status":"active","created_at":"2026-03-02T23:38:27.778Z","source":"extraction","content_hash":"e2f493b97cabbb4d","confidence":1,"last_reinforced":"2026-03-02T23:57:47.911Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"5hHT4ucQkSDU","mind":"default","section":"Known Issues","content":"ChatWidget 'a' keybinding for attach_file only fires when ChatWidget itself has focus (not the Input child). Users focused on the chat Input type 'a' as text instead of triggering the attach action. Ctrl+A binding added as workaround since Input doesn't intercept it.","status":"active","created_at":"2026-03-02T23:38:27.779Z","source":"extraction","content_hash":"4c646d4c29abcbe0","confidence":1,"last_reinforced":"2026-03-02T23:55:54.377Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2aV1dfcSRQq0","mind":"default","section":"Known Issues","content":"Clipboard attachment system (v0.11.6) non-functional from user perspective: clipboard.read_clipboard_attachment() works in Python (confirmed has_clipboard_image=True, returns ClipboardAttachment), but Textual TUI integration has three failure modes: on_paste only receives text, Ctrl+V eaten by Input, 'a' key only fires when ChatWidget (not Input) has focus.","status":"active","created_at":"2026-03-02T23:38:27.779Z","source":"extraction","content_hash":"2b2974f68d17d109","confidence":1,"last_reinforced":"2026-03-02T23:57:47.915Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cR7NF0fGFISJ","mind":"default","section":"Known Issues","content":"Ctrl+O binding for clipboard attach is non-functional on macOS because the terminal driver intercepts Ctrl+O as DISCARD before Textual receives it (even in raw mode). Fixed in v0.11.8 by switching primary binding to ctrl+p; ctrl+o kept as hidden fallback.","status":"active","created_at":"2026-03-02T23:59:51.294Z","source":"extraction","content_hash":"ba195c441f8ebd26","confidence":1,"last_reinforced":"2026-03-02T23:59:51.294Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"xlVK5YAvb0tx"}
{"_type":"fact","id":"tiwGrdM8Q-4E","mind":"default","section":"Known Issues","content":"Ctrl+O binding for clipboard attach was non-functional on macOS (terminal DISCARD interception). Resolved in v0.11.8: ctrl+p is now primary visible binding, ctrl+o retained as hidden fallback.","status":"active","created_at":"2026-03-02T23:59:51.294Z","source":"extraction","content_hash":"7b2c22874b6c78f2","confidence":1,"last_reinforced":"2026-03-02T23:59:51.294Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"TbjPLVyK7ly_"}
{"_type":"fact","id":"EFH-z1HzGLVc","mind":"default","section":"Known Issues","content":"Textual run_worker + run_in_executor dispatches PyObjC calls to thread pool threads where macOS NSPasteboard requires the main thread — causes silent failure of clipboard reads regardless of binding used.","status":"active","created_at":"2026-03-03T00:33:18.437Z","source":"extraction","content_hash":"eac715fceae75736","confidence":1,"last_reinforced":"2026-03-03T01:46:20.001Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lGDxhaw_ZK5D","mind":"default","section":"Known Issues","content":"Textual App.COMMAND_PALETTE_BINDING = 'ctrl+p' — using ctrl+p as a custom binding in ChatWidget conflicts with the command palette and is silently overridden. Custom bindings must avoid ctrl+p.","status":"active","created_at":"2026-03-03T00:33:18.438Z","source":"extraction","content_hash":"662f1c835d991a21","confidence":1,"last_reinforced":"2026-03-03T00:33:18.438Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qP4sunc6x81P","mind":"default","section":"Known Issues","content":"PyObjC NSPasteboard.generalPasteboard() must be called from the main thread. Using asyncio run_in_executor (thread pool) causes it to silently return empty/stale data. Fixed in v0.11.10 by moving clipboard reads to synchronous calls in action handlers (which run on main thread). [2026-03-02]","status":"active","created_at":"2026-03-03T00:35:06.078Z","source":"manual","content_hash":"201f93777c6e0984","confidence":1,"last_reinforced":"2026-03-03T01:46:20.001Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9MMHuRSIPXnw","mind":"default","section":"Known Issues","content":"v0.11.13 released: fix(tui,ipc) allow sending attachment-only messages without text — IPC handler allowed empty content when attachment_data_b64 present, optimistic message shows '📎 filename' for attachment-only sends, input placeholder restored after attachment send.","status":"active","created_at":"2026-03-03T00:53:47.492Z","source":"extraction","content_hash":"9a1dc01a55e438f4","confidence":1,"last_reinforced":"2026-03-03T00:53:47.492Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vLi8k3xWJ9vA","mind":"default","section":"Known Issues","content":"IPC handler CMD_SEND_CHAT empty-content validation fixed in v0.11.13: changed `if not req.content` to `if req.content is None` and added guard `if not req.content and not req.attachment_data_b64: return error`. Allows image-only sends.","status":"active","created_at":"2026-03-03T00:53:47.492Z","source":"extraction","content_hash":"d66373e8b714c7ea","confidence":1,"last_reinforced":"2026-03-03T00:53:47.492Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"NtVU2xaWxL2L"}
{"_type":"fact","id":"8ILRO-rMz5np","mind":"default","section":"Known Issues","content":"Clipboard attachment flow fully fixed in v0.11.13: on_input_submitted no longer bails on empty message when non-path attachment is staged, IPC handler allows empty content when attachment present, optimistic display shows '📎 filename'.","status":"active","created_at":"2026-03-03T00:53:47.493Z","source":"extraction","content_hash":"d9f1a9d0e4553b3e","confidence":1,"last_reinforced":"2026-03-03T00:53:47.493Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"FGu0EUlGHpzq"}
{"_type":"fact","id":"A9V8icRKEuyc","mind":"default","section":"Known Issues","content":"Ctrl+Y clipboard attach send-on-empty fixed in v0.11.13: IPC handler now accepts empty content string when attachment_data_b64 is provided. Attachment-only messages fully functional.","status":"active","created_at":"2026-03-03T00:53:47.493Z","source":"extraction","content_hash":"88c1e0af43713f54","confidence":1,"last_reinforced":"2026-03-03T00:53:47.493Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"9xKkZzEKKhPk"}
{"_type":"fact","id":"vV-pMEPol3ne","mind":"default","section":"Known Issues","content":"IPC handler test test_handle_cmd_send_chat_requires_content renamed to test_handle_cmd_send_chat_requires_content_or_attachment with updated assertion 'content or attachment is required' to match v0.11.13 validation change.","status":"active","created_at":"2026-03-03T01:00:45.612Z","source":"extraction","content_hash":"f0a1d0110ea46ba1","confidence":1,"last_reinforced":"2026-03-03T01:00:45.612Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"i2ac_stZXw-A","mind":"default","section":"Known Issues","content":"styrened 0.12.0 published to PyPI (2026-03-02); styrene-pypi repository_dispatch triggered to sync meta-package to 0.12.0","status":"active","created_at":"2026-03-03T01:00:45.613Z","source":"extraction","content_hash":"6fff65de8402f1dc","confidence":1,"last_reinforced":"2026-03-03T01:00:45.613Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"udMt7YKVzzXP"}
{"_type":"fact","id":"23wCAJFZ_YJk","mind":"default","section":"Known Issues","content":"ChatWidget had no date separators between messages from different days — messages at 07:07 (today) appeared after 18:34/20:26 (yesterday) with correct DB ordering but confusing display due to missing date context","status":"active","created_at":"2026-03-03T01:29:47.184Z","source":"extraction","content_hash":"610708e89c79620d","confidence":1,"last_reinforced":"2026-03-03T01:29:47.184Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"aUN1oDl1Ic4T","mind":"default","section":"Known Issues","content":"ChatWidget _refresh_messages() incremental re-render fully fixed in v0.12.2: only new messages appended (no full teardown+rebuild), existing bubbles preserved, date separators added between day boundaries","status":"active","created_at":"2026-03-03T01:34:46.547Z","source":"extraction","content_hash":"2bf321650507f8a0","confidence":1,"last_reinforced":"2026-03-03T01:34:46.547Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"heHhuwTtLaqa"}
{"_type":"fact","id":"qZqXxoOIIfnA","mind":"default","section":"Known Issues","content":"ConversationScreen title bar now shows just the display name (or first 12 chars of hash), no longer shows 'CONVERSATION - fullhash'. Fixed in v0.13.0.","status":"active","created_at":"2026-03-03T01:43:46.904Z","source":"extraction","content_hash":"6fe6962298b93d37","confidence":1,"last_reinforced":"2026-03-03T01:43:46.904Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"MaBu1SrAbo11"}
{"_type":"fact","id":"eEDvp03YyBnD","mind":"default","section":"Known Issues","content":"Chat message gravity investigation complete — researched in v0.13.0 but pinning-to-bottom via Textual VerticalScroll may not be fully solved; messages still start at top when few exist. No confirmed Textual mechanism for content-align-vertical on scroll containers.","status":"active","created_at":"2026-03-03T01:43:46.904Z","source":"extraction","content_hash":"671ecea4fc17ba1a","confidence":1,"last_reinforced":"2026-03-03T01:44:49.132Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"XJ5sy42fKcqG"}
{"_type":"fact","id":"eQcQdAh3d5fN","mind":"default","section":"Known Issues","content":"ConversationScreen/ChatWidget chat UX holistic redesign completed in v0.13.0: no ME: prefix, sender name above incoming content, bottom gravity, simplified title bar, 2-line input bar with heavy separator.","status":"active","created_at":"2026-03-03T01:43:46.904Z","source":"extraction","content_hash":"d6a574f3d6510bad","confidence":1,"last_reinforced":"2026-03-03T01:43:46.904Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"6O-1VSfBhyMq"}
{"_type":"fact","id":"M4GdDxWKMndp","mind":"default","section":"Known Issues","content":"ConversationScreen/ChatWidget: incremental refresh (v0.12.2) and date separators fixed; chat UX redesigned in v0.13.0 (layout, input flow, message formatting). Failed message rendering updated: content shown normally with red ✗, not full-line red italic.","status":"active","created_at":"2026-03-03T01:43:46.905Z","source":"extraction","content_hash":"a3bc8d5a542a6572","confidence":1,"last_reinforced":"2026-03-03T01:43:46.905Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"dyHt6wXClFUn"}
{"_type":"fact","id":"K9x6urxk3pul","mind":"default","section":"Known Issues","content":"_stage_from_clipboard() docstring claims NSPasteboard access happens via call_from_thread/call_later on main thread — this is false. The method runs inside run_worker (off-main-thread) and calls NSPasteboard directly. Docstring is actively misleading.","status":"active","created_at":"2026-03-03T01:54:16.098Z","source":"extraction","content_hash":"0c106691102944a9","confidence":1,"last_reinforced":"2026-03-03T01:54:16.098Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"i2T72rCOH9ny","mind":"default","section":"Known Issues","content":"Chat incremental refresh message deletion now handled: if old_ids_set - new_ids_set is non-empty (messages deleted), resets _message_ids and _last_displayed_date and triggers full rebuild instead of silently leaving ghost bubbles.","status":"active","created_at":"2026-03-03T02:19:10.728Z","source":"extraction","content_hash":"063a4c9c4e8542d1","confidence":1,"last_reinforced":"2026-03-03T02:19:10.728Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"bvn85J07y_ID"}
{"_type":"fact","id":"hrSA1RIgZG7M","mind":"default","section":"Known Issues","content":"_last_displayed_date now reset to None on full rebuild (both explicit full-rebuild path and deletion-triggered rebuild) — date separator continuity fixed.","status":"active","created_at":"2026-03-03T02:19:10.728Z","source":"extraction","content_hash":"6d667bb7647c8bd6","confidence":1,"last_reinforced":"2026-03-03T02:19:10.728Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"cTE2oGtEhXNq"}
{"_type":"fact","id":"fpC95ZWORznd","mind":"default","section":"Known Issues","content":"NodeInfoPanel _load_comms_data() contact_count fixed: replaced nonexistent get_contacts() with get_all_nodes() from NodeStore. contact_count now reflects known mesh nodes in standalone mode.","status":"active","created_at":"2026-03-03T02:19:10.729Z","source":"extraction","content_hash":"779ec6adde9baa25","confidence":1,"last_reinforced":"2026-03-03T02:19:10.729Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"1GVdxXnQEzQk"}
{"_type":"fact","id":"mCu7kH_2JUTr","mind":"default","section":"Known Issues","content":"_load_comms_data() local_hash=None guard fixed: when local_hash is None, both source_hash and destination_hash are added to peer_hashes set (conservative overcount) rather than always adding source_hash.","status":"active","created_at":"2026-03-03T02:19:10.729Z","source":"extraction","content_hash":"a87259b2a519ce70","confidence":1,"last_reinforced":"2026-03-03T02:19:10.729Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"TgTWj_Dg6hc9"}
{"_type":"fact","id":"p6b-5ww5K15N","mind":"default","section":"Known Issues","content":"align-vertical:bottom removed from #chat-message-container in imperial_crt.tcss — property was confirmed non-functional for scrollable overflow containers and has been deleted.","status":"active","created_at":"2026-03-03T02:19:10.729Z","source":"extraction","content_hash":"bdd42076f84676ab","confidence":1,"last_reinforced":"2026-03-03T02:19:10.729Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"G_f_2_lwRnin"}
{"_type":"fact","id":"w7VnyLoPZ9aP","mind":"default","section":"Known Issues","content":"align-vertical:bottom removed from #chat-message-container CSS. Property was confirmed to do nothing for overflow scroll containers in Textual. Scroll gravity achieved only via scroll_end() calls.","status":"active","created_at":"2026-03-03T02:19:10.729Z","source":"extraction","content_hash":"c5c886ad4b4b85b4","confidence":1,"last_reinforced":"2026-03-03T02:19:10.729Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"F_TmvFraMOft"}
{"_type":"fact","id":"u9V3nymBa42d","mind":"default","section":"Known Issues","content":"_update_optimistic_status was fixed to match outgoing bubbles by content (raw_content comparison) rather than just reversed position, preventing wrong-bubble updates on concurrent sends.","status":"active","created_at":"2026-03-03T02:21:07.812Z","source":"extraction","content_hash":"97fafe3936cd85ca","confidence":1,"last_reinforced":"2026-03-03T02:21:07.812Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"i7Yjm8xjiNv3","mind":"default","section":"Known Issues","content":"_stage_clipboard_attachment dead guard (unreachable 'if attachment is not None') removed; also fixed indentation regression from the removal.","status":"active","created_at":"2026-03-03T02:21:07.813Z","source":"extraction","content_hash":"e940662e5f3bd62c","confidence":1,"last_reinforced":"2026-03-03T02:21:07.813Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"x3nVOT2G86KE","mind":"default","section":"Known Issues","content":"Chat input bar separator border changed from 'heavy' to 'wide' in imperial_crt.tcss for broader terminal compatibility.","status":"active","created_at":"2026-03-03T02:21:07.814Z","source":"extraction","content_hash":"c16f11a80ee27bf5","confidence":1,"last_reinforced":"2026-03-03T02:21:07.814Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"JDkGESLAxBOe","mind":"default","section":"Known Issues","content":"Exploration table empty-state row now renders message in NAME column (first column) with empty strings for remaining columns, instead of '-' placeholders with message in STATUS column.","status":"active","created_at":"2026-03-03T02:21:07.814Z","source":"extraction","content_hash":"1e606cc5a1a46c7e","confidence":1,"last_reinforced":"2026-03-03T02:21:07.814Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8UBnAZ4vbuS7","mind":"default","section":"Known Issues","content":"MeshDevice.hops field persistence fixed in v0.13.1: hops column added via ALTER TABLE migration to NodeStore, included in save_node() INSERT/UPDATE, read back in _row_to_device().","status":"active","created_at":"2026-03-03T03:06:17.090Z","source":"extraction","content_hash":"6abf595e28f35015","confidence":1,"last_reinforced":"2026-03-03T03:06:17.090Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"DWT1fdWI5fzF"}
{"_type":"fact","id":"iAG99PNzdg3X","mind":"default","section":"Known Issues","content":"_load_comms_data() refactored in v0.13.1 to use SQL aggregates (func.count, case) instead of loading all rows — no longer blocks event loop on large message history.","status":"active","created_at":"2026-03-03T03:06:17.090Z","source":"extraction","content_hash":"05ce1bbd5d09d451","confidence":1,"last_reinforced":"2026-03-03T03:06:17.090Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"kEffi1klGntW"}
{"_type":"fact","id":"gobj6ir2YDYu","mind":"default","section":"Known Issues","content":"_load_comms_data() SQL blocking fixed in v0.13.1: replaced .all() with SQL aggregate queries, no rows loaded into Python memory.","status":"active","created_at":"2026-03-03T03:06:17.090Z","source":"extraction","content_hash":"305885fb761b6454","confidence":1,"last_reinforced":"2026-03-03T03:06:17.090Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"Pss7F4le-PYA"}
{"_type":"fact","id":"ppgSSJjhnPwq","mind":"default","section":"Known Issues","content":"TRAFFIC section in NodeInfoPanel IPC mode partially fixed in v0.13.1: messages_received now populated from message_count total; sent/pending remain 0 pending IPC endpoint changes for per-direction breakdown.","status":"active","created_at":"2026-03-03T03:06:17.091Z","source":"extraction","content_hash":"9ef3b18543ffae67","confidence":1,"last_reinforced":"2026-03-03T03:06:17.091Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"zLq_mmICc6Ea"}
{"_type":"fact","id":"1r3ln7A8pJl5","mind":"default","section":"Known Issues","content":"_fetch_daemon_status IPC calls parallelized in v0.13.1: conversations, contacts, and auto_reply launched as asyncio.create_task concurrently, all runs use group='dashboard-status' to prevent concurrent worker races.","status":"active","created_at":"2026-03-03T03:06:17.091Z","source":"extraction","content_hash":"6e9f6b02bb60ebd3","confidence":1,"last_reinforced":"2026-03-03T03:06:17.091Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"bojzwcH4YmfP"}
{"_type":"fact","id":"AxDA9JTQccLd","mind":"default","section":"Known Issues","content":"Multiple concurrent _fetch_daemon_status workers fixed in v0.13.1: all run_worker() calls use group='dashboard-status', ensuring only one worker runs at a time.","status":"active","created_at":"2026-03-03T03:06:17.091Z","source":"extraction","content_hash":"9503cb46ed21cced","confidence":1,"last_reinforced":"2026-03-03T03:06:17.091Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"YrefnR8puenK"}
{"_type":"fact","id":"Vyqwej_XHtGO","mind":"default","section":"Known Issues","content":"_stage_from_clipboard() main-thread fix fully applied in v0.13.1: renamed to _do_stage_from_clipboard(), all callers (on_paste, action_paste_attachment) call it synchronously on main thread. NSPasteboard threading bug resolved in all chat clipboard code paths.","status":"active","created_at":"2026-03-03T03:06:17.091Z","source":"extraction","content_hash":"57df5a9cd8c1bae4","confidence":1,"last_reinforced":"2026-03-03T03:06:17.091Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"6wqZbjGBw6Zp"}
{"_type":"fact","id":"mz1MxZ5ZrEyU","mind":"default","section":"Known Issues","content":"NSPasteboard threading bug fixed in v0.13.1: _do_stage_from_clipboard() called synchronously from action handlers and on_paste (main thread), never inside run_worker or asyncio tasks.","status":"active","created_at":"2026-03-03T03:06:17.091Z","source":"extraction","content_hash":"e8b5a11b6039776d","confidence":1,"last_reinforced":"2026-03-03T03:06:17.091Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"XgvdJ15whZsi"}
{"_type":"fact","id":"08t1hP4VTQPe","mind":"default","section":"Known Issues","content":"RNS announce callback does not receive the receiving interface — interface info must be read post-hoc from RNS.Transport.path_table[destination_hash][5] after the callback fires, which may reflect a different announce's winner if multiple hubs compete.","status":"active","created_at":"2026-03-03T03:08:26.505Z","source":"extraction","content_hash":"0b8cabdce73462e5","confidence":1,"last_reinforced":"2026-03-03T03:08:26.505Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9SakN-srugP-","mind":"default","section":"Known Issues","content":"v0.13.2 released: fix VIA column in exploration/dashboard now shows 'Interface Name → next_hop_hash' (e.g., 'Styrene Community Hub → a3f6f4c8') to disambiguate which transport node relayed an announce when multiple hubs are connected.","status":"active","created_at":"2026-03-03T03:11:41.065Z","source":"extraction","content_hash":"0c18502adb09fc8b","confidence":1,"last_reinforced":"2026-03-03T03:11:41.065Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"1DBPCKipmYQr","mind":"default","section":"Known Issues","content":"Dashboard VIA column showed local interface name only (e.g., 'Styrene Community Hub') for all devices — fixed in v0.13.2: discovered_via now includes next-hop transport hash suffix (e.g., 'Styrene Community Hub → a3f6f4c8'), disambiguating which transport node relayed announces when multiple peers are enabled.","status":"active","created_at":"2026-03-03T03:11:41.066Z","source":"extraction","content_hash":"1ff584e8f16e428f","confidence":1,"last_reinforced":"2026-03-03T03:11:41.066Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"mtR5Nl_yVaWH"}
{"_type":"fact","id":"i_r1dcmXFFB8","mind":"default","section":"Known Issues","content":"Dashboard mesh devices grouped under a single interface name — partially improved in v0.13.2 by adding next-hop hash suffix to VIA label; most announces legitimately arrive through Styrene Community Hub (best connectivity), but the suffix now shows which specific transport node relayed each.","status":"active","created_at":"2026-03-03T03:11:41.066Z","source":"extraction","content_hash":"e9b8867b17777193","confidence":1,"last_reinforced":"2026-03-03T03:11:41.066Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"-idGdS5Xw3Sp"}
{"_type":"fact","id":"rnHxp1crhio7","mind":"default","section":"Known Issues","content":"Settings screen stacked on repeated backtick press — fixed in v0.13.4: action_push_screen_settings now checks isinstance(self.screen, SettingsScreen) and returns early if already on top.","status":"active","created_at":"2026-03-03T03:16:51.850Z","source":"extraction","content_hash":"916197c94827e952","confidence":1,"last_reinforced":"2026-03-03T03:16:51.850Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WpeDgW7F4gwK","mind":"default","section":"Known Issues","content":"Chat send failure while node is actively announcing: LXMF delivery failures can occur even when destination is visible in mesh because path establishment and announce propagation are decoupled — path may not be established despite announce arriving. No UI explanation of this to the user.","status":"active","created_at":"2026-03-03T19:48:09.513Z","source":"extraction","content_hash":"757183300bf25ace","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"eoOgNScfrv5I","mind":"default","section":"Known Issues","content":"send_message returns None causing retry failures when the peer's identity is not in RNS cache or NodeStore — _resolve_identity() exhausts all 4 strategies. This is the primary cause of both initial send failures and retry failures when destination is unreachable.","status":"active","created_at":"2026-03-03T19:49:16.773Z","source":"extraction","content_hash":"b995b1714488a3f1","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"aajeBdEANC94","mind":"default","section":"Known Issues","content":"ChatWidget Phase 3 retry design: R binding calls action_retry_message → auto-selects last failed bubble if none selected → _retry_single(message_id). ctrl+r retries all failed with per-message groups. _retry_after_refresh handles message_id=0 optimistic bubbles by refreshing from DB first. message_id=0 optimistic bubble issue now addressed.","status":"active","created_at":"2026-03-03T19:51:13.590Z","source":"extraction","content_hash":"c071bdcb6897a204","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"KlMm5NftKqCj"}
{"_type":"fact","id":"lV0CaPkJoIZ4","mind":"default","section":"Known Issues","content":"Chat failure/retry system partially fixed: R now auto-selects last failed bubble without requiring Tab+navigation; optimistic bubbles (id=0) trigger _retry_after_refresh() which refreshes from DB first. User still needs to know R key exists — no inline UI affordance yet. Footer shows 'R Retry' only when ChatWidget (not Input) has focus.","status":"active","created_at":"2026-03-03T19:51:13.590Z","source":"extraction","content_hash":"6839b660306e6316","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"QdDHsrIItoAS"}
{"_type":"fact","id":"hnqHFh2jk-cW","mind":"default","section":"Known Issues","content":"Failed messages now show red ✗ + '↻ press R to retry' hint on the line below. R key auto-selects last failed message if none is selected. Still no clickable button — keyboard-only affordance.","status":"active","created_at":"2026-03-03T19:51:13.590Z","source":"extraction","content_hash":"1a79e0f71caf60b6","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"994UywtYF5UI"}
{"_type":"fact","id":"opxVAeg9psyM","mind":"default","section":"Known Issues","content":"_update_optimistic_status for attachment-only messages (empty content) was broken — content matching skipped empty string, so it matched wrong or no bubble. Fixed: empty content matches most recent pending outgoing bubble instead of content-matching.","status":"active","created_at":"2026-03-03T19:51:13.590Z","source":"extraction","content_hash":"3ef78c6b54fad77a","confidence":1,"last_reinforced":"2026-03-03T19:51:13.590Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"crn_XtMyRIXn","mind":"default","section":"Known Issues","content":"action_retry_all_failed was silently cancelling all-but-last retry workers because all used group='chat-retry'. Fixed: each retry worker now uses group='chat-retry-{message_id}'.","status":"active","created_at":"2026-03-03T19:51:13.590Z","source":"extraction","content_hash":"7910a22be63b4545","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ugqdy1gAROWJ","mind":"default","section":"Known Issues","content":"After send failure, optimistic bubble (message_id=0) was never replaced with real DB record because _refresh_messages() was not called on the failure path. Fixed: both TimeoutError and generic Exception paths now call _refresh_messages() after 0.3s delay.","status":"active","created_at":"2026-03-03T19:51:13.591Z","source":"extraction","content_hash":"9da2faf342738dec","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"yLD3yoIfpRyN","mind":"default","section":"Known Issues","content":"send_message returns None when RNS.Identity.recall() evicts the identity cache for a recently-announcing peer — identity was stored during announce processing but cache flushed before send. Root cause of 'Server c0cd' chat send failures despite active announces.","status":"active","created_at":"2026-03-03T20:02:49.776Z","source":"extraction","content_hash":"cdcdbcd7d99fceec","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pvxR4dqGlEE_","mind":"default","section":"Known Issues","content":"_resolve_identity Strategy 5 (path request + 10s blocking poll) blocks the IPC handler thread. Since async IPC handlers call synchronous send_message() directly (not in an executor), this risks blocking the asyncio event loop if ever called from coroutine context without thread isolation.","status":"active","created_at":"2026-03-03T20:02:49.777Z","source":"extraction","content_hash":"f32c64ddab740eb1","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9r0fGYGVGHe8","mind":"default","section":"Known Issues","content":"UpgradeScreen changelog implemented in progress: WHAT'S NEW panel added to UpgradeScreen showing changes between current and latest. fetch_changelog() in tui/services/changelog.py uses local git log → GitHub API → PyPI version list fallback chain. GitHub compare API fails for private repo without auth; git log strategy added to resolve.","status":"active","created_at":"2026-03-03T20:07:55.846Z","source":"extraction","content_hash":"e56bf42ba138fac8","confidence":1,"last_reinforced":"2026-03-03T20:07:55.846Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"Fue7-iB7klf7"}
{"_type":"fact","id":"A49r8ILQB7Fs","mind":"default","section":"Known Issues","content":"fetch_changelog() GitHub compare API strategy fails for private repos without auth token — git log strategy (now Strategy 1) resolves this for local git checkouts.","status":"active","created_at":"2026-03-03T20:39:15.906Z","source":"extraction","content_hash":"968d0d5c70c83673","confidence":1,"last_reinforced":"2026-03-03T20:39:15.906Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CDwFYQiUp-rd","mind":"default","section":"Known Issues","content":"Pages tab in MeshDeviceDetailScreen was incorrectly shown for all devices in v0.13.9 — corrected in same session to only show for NomadNet nodes or devices whose RNS identity also has a NomadNet announce (via node store lookup by identity_hash).","status":"active","created_at":"2026-03-03T20:42:53.970Z","source":"extraction","content_hash":"4226b5bbfbf4e6e9","confidence":1,"last_reinforced":"2026-03-03T20:52:21.736Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rcvOBCJYVw2M","mind":"default","section":"Known Issues","content":"CMD_PAGE_REGENERATE IPC command implemented: CmdPageRegenerateRequest in messages.py, handle_cmd_page_regenerate() in handlers.py, page_regenerate_index() in client.py, registered in server.py dispatch table. PageServerService.regenerate_index() is now exposed via IPC.","status":"active","created_at":"2026-03-03T20:55:29.386Z","source":"extraction","content_hash":"9a6e485ad89c4d02","confidence":1,"last_reinforced":"2026-03-03T20:55:29.386Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"ZKaUbZDy6Z51"}
{"_type":"fact","id":"auIR-GFM971W","mind":"default","section":"Known Issues","content":"page_server.py 'About Styrene' section fixed in v0.13.12: docs URL corrected to https://docs.styrene.io (was /styrened/api/ subdirectory), install recommendation changed to 'pipx install styrene'.","status":"active","created_at":"2026-03-03T21:05:40.680Z","source":"extraction","content_hash":"0ec64db79c705e92","confidence":1,"last_reinforced":"2026-03-03T21:05:40.680Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"r7akD64YRt3S"}
{"_type":"fact","id":"OhpApz3C8eLz","mind":"default","section":"Known Issues","content":"fetch_changelog() PyPI fallback (Strategy 4 previously Strategy 3) fired for pipx users because the package is not installed from a git checkout, returning only version numbers with no commit messages — anemic changelog. Fixed in v0.13.13 by embedding _changelog.py at build time (Strategy 1).","status":"active","created_at":"2026-03-03T21:05:40.680Z","source":"extraction","content_hash":"c0b1a8dde42b4608","confidence":1,"last_reinforced":"2026-03-03T22:29:43.637Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ybrFgFPARI-H","mind":"default","section":"Known Issues","content":"public-hub justfile had STYRENED_VERSION hardcoded to 0.10.21 in the build recipe (not just Containerfile default ARG) — both locations must be kept in sync when bumping versions. Fixed for 0.13.13.","status":"active","created_at":"2026-03-03T21:52:31.300Z","source":"extraction","content_hash":"f18b9a2e7a1bf288","confidence":1,"last_reinforced":"2026-03-03T21:52:31.300Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mWaYsUFLBKeq","mind":"default","section":"Known Issues","content":"Pages tab missing in MeshDeviceDetailScreen when navigating from StyreneFleetTable (Styrene tab in ExplorationScreen) — _resolve_nomadnet_destination() fails to find the NomadNet announce entry because the hub's NomadNet destination hash differs from the Styrene node hash, requiring identity_hash-based NodeStore lookup that may not be finding the entry.","status":"active","created_at":"2026-03-03T21:52:31.301Z","source":"extraction","content_hash":"2a2be3edb6b40c95","confidence":1,"last_reinforced":"2026-03-04T00:19:21.939Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zI50JEHCUpk4","mind":"default","section":"Known Issues","content":"_resolve_nomadnet_destination() searched only NodeStore but NomadNet and Styrene on the same hub use different RNS identities (separate ~/.nomadnetwork/storage/identity vs ~/.styrene/identity), so identity_hash matching across sibling aspects never finds a match. Live discovered_devices is also unchecked in IPC mode (always empty). Root cause: identity_hash assumption is wrong for multi-process hubs.","status":"active","created_at":"2026-03-03T21:53:53.371Z","source":"extraction","content_hash":"be2a85e553708620","confidence":1,"last_reinforced":"2026-03-03T21:53:53.371Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"K1rRLtBaTWd3","mind":"default","section":"Known Issues","content":"_resolve_nomadnet_destination() resolution order: (1) device is NOMADNET_NODE directly, (2) device advertises 'pages' capability — compute NomadNet destination from RNS.Identity.recall + RNS.Destination.hash, (3) NodeStore identity_hash cross-match. Strategy 2 only works when styrened owns the destination (same identity). Hub multi-process case now fixed via 8th announce field (nomadnet_dest).","status":"active","created_at":"2026-03-03T21:55:44.831Z","source":"extraction","content_hash":"fe47bd2b3a0528db","confidence":1,"last_reinforced":"2026-03-03T21:55:44.831Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"0r-i-uIWJAqk"}
{"_type":"fact","id":"fnr90zo165WT","mind":"default","section":"Known Issues","content":"Multiple unit test files (test_announce_fingerprint.py, test_daemon_announce.py, test_parse_announce_autoreply.py, test_short_name.py) have stale tuple unpacking expecting 7-element returns from parse_announce_data() — failing after 8-tuple extension with nomadnet_dest field. Being fixed incrementally with sed replacements.","status":"active","created_at":"2026-03-03T21:58:21.512Z","source":"extraction","content_hash":"43d55134f812dfdc","confidence":1,"last_reinforced":"2026-03-03T22:28:27.379Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"svhcXcoZW3xw","mind":"default","section":"Known Issues","content":"Upgrade screen WHAT'S NEW changelog empty for pipx installs FIXED in v0.13.15: new Strategy 2 downloads the target version's wheel from PyPI (~200KB), extracts its _changelog.py via zipfile, and parses entries. Target version's bundled changelog always covers itself and prior versions.","status":"active","created_at":"2026-03-03T23:04:04.524Z","source":"extraction","content_hash":"bdb67f894ddccecc","confidence":1,"last_reinforced":"2026-03-03T23:04:04.524Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"2hT4RNFZNAwj"}
{"_type":"fact","id":"EUte9AUdnAg5","mind":"default","section":"Known Issues","content":"UpgradeScreen changelog fully fixed in v0.13.15: Strategy 2 (_fetch_from_pypi_wheel) downloads target version's wheel from PyPI and extracts _changelog.py — works for all install methods including pipx. Strategy 1 (bundled) still fires first for cases where installed version already has the entries.","status":"active","created_at":"2026-03-03T23:04:04.525Z","source":"extraction","content_hash":"0081e8e07b591cf6","confidence":1,"last_reinforced":"2026-03-03T23:04:04.525Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"S9LMtRlGfRuk"}
{"_type":"fact","id":"Le8J79cN9pH2","mind":"default","section":"Known Issues","content":"_resolve_nomadnet_destination() adds hub capability heuristic in v0.13.15: if device has 'pages' capability, use its own destination_hash optimistically — page browser handles connection errors. Hub capability ('hub') alone is NOT used; only 'pages' capability triggers this fallback.","status":"active","created_at":"2026-03-03T23:04:04.525Z","source":"extraction","content_hash":"c564069096c1f6f0","confidence":1,"last_reinforced":"2026-03-03T23:04:04.525Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"xNzXhuhZFZ1T","mind":"default","section":"Known Issues","content":"v0.13.16 fix: caps_str was generated before NomadNet identity probe, so dynamically added 'pages' capability was not included in the announce wire format. Fixed by moving caps_str join after the NomadNet block.","status":"active","created_at":"2026-03-03T23:34:09.593Z","source":"extraction","content_hash":"414c6873ee391f4b","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"eqopEfvAr_uD","mind":"default","section":"Known Issues","content":"Hub pages capability fix shipped in v0.13.16: daemon now probes NomadNet identity files directly (including /app/.nomadnetwork/storage/identity) regardless of page_server.enabled, adds 'pages' capability and nomadnet_dest to announce even in multi-process hub mode.","status":"active","created_at":"2026-03-03T23:34:09.593Z","source":"extraction","content_hash":"54394e492770cc69","confidence":1,"last_reinforced":"2026-03-03T23:45:53.981Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"rGcQoPjh7Alh"}
{"_type":"fact","id":"pZCjQpqcq0g2","mind":"default","section":"Known Issues","content":"Pages tab in MeshDeviceDetailScreen was missing for hub v0.13.15 because page_server.is_started gate prevented NomadNet identity probe. Fixed in v0.13.16 which probes NomadNet identity files directly regardless of page_server.enabled.","status":"active","created_at":"2026-03-03T23:45:53.977Z","source":"extraction","content_hash":"64a8bba317170c63","confidence":1,"last_reinforced":"2026-03-03T23:45:53.977Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"mPqUEmluR5mj","mind":"default","section":"Known Issues","content":"Micron renderer in page_browser produces anemic output: headings are just bold with no color, links have no color differentiation, no visual hierarchy. Improvement in progress — adding colored heading styles (#5af0ce h1, #3fbfa0 h2, #6a9a8e h3).","status":"active","created_at":"2026-03-03T23:45:53.978Z","source":"extraction","content_hash":"2aca7ff18479554c","confidence":1,"last_reinforced":"2026-03-04T00:19:21.939Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"EuGRoJ514-_A","mind":"default","section":"Known Issues","content":"public-hub NomadNet pages (index.mu, connect.mu, about.mu, docs/) are persisted on the nomadnet-data PVC, not baked into the image — they were created manually in a previous session. Only demo/ pages are copied from the image at startup.","status":"active","created_at":"2026-03-03T23:47:58.459Z","source":"extraction","content_hash":"b3b2a74fce4205c9","confidence":1,"last_reinforced":"2026-03-03T23:47:58.459Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"a1MKog1bRJxQ","mind":"default","section":"Known Issues","content":"Micron color codes `Fg5af0ce (6-char hex foreground) were being misread as grayscale when prefixed with extra characters. The correct format is `Fg50 for grayscale-50 or `F + 6-char hex for named color. Renderer now emits correct Rich markup for h1 (#5af0ce), h2 (#3fbfa0), h3 (#6a9a8e) headings and cyan (#5ac8fa) clickable links.","status":"active","created_at":"2026-03-03T23:47:58.460Z","source":"extraction","content_hash":"9c6baaabf3495c4f","confidence":1,"last_reinforced":"2026-03-04T00:19:21.939Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cV4UxcUtOKFK","mind":"default","section":"Known Issues","content":"UpgradeScreen 'Restart TUI' button removed in v0.13.18 — _finish_success() now auto-dismisses with True immediately after daemon kill, triggering automatic TUI restart with no user interaction required.","status":"active","created_at":"2026-03-04T00:19:21.938Z","source":"extraction","content_hash":"f9b14f60b8a2b890","confidence":1,"last_reinforced":"2026-03-04T01:01:26.922Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XAt2XMX5Qbf1","mind":"default","section":"Known Issues","content":"Pages tab in MeshDeviceDetailScreen shows correct destination hash (af1ec9...) but page requests time out on /page/demo/index.mu — page browser cannot fetch from hub's NomadNet destination despite correct hash resolution.","status":"active","created_at":"2026-03-04T00:19:21.939Z","source":"extraction","content_hash":"a9068c4ead98d978","confidence":1,"last_reinforced":"2026-03-04T00:41:34.175Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ZkDqgyqauz-G","mind":"default","section":"Known Issues","content":"NomadNet announces from the hub's own NomadNet process do NOT get delivered to styrened's announce handler because they share the same RNS instance — RNS does not deliver local announces to local handlers. Remote nodes receive the announce over the network normally.","status":"active","created_at":"2026-03-04T00:25:17.413Z","source":"extraction","content_hash":"8f856c6a2be42478","confidence":1,"last_reinforced":"2026-03-04T00:39:06.215Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"e4AjlKSrrdnG","mind":"default","section":"Known Issues","content":"Page browser 'Page request timed out' error on first attempt is transient — NomadNet destination path not yet cached in local daemon. Subsequent attempts after path caches succeed. Confirmed working end-to-end: /page/index.mu returns 963 bytes from hub's NomadNet.","status":"active","created_at":"2026-03-04T00:33:05.112Z","source":"extraction","content_hash":"d8df6cd59515f45b","confidence":1,"last_reinforced":"2026-03-04T01:44:41.587Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"36dx_Y913NYK","mind":"default","section":"Known Issues","content":"Page browser 'Link establishment failed' error observed in TUI despite end-to-end page fetch working in standalone Python test — root cause under investigation: daemon's shared RNS instance may have different link establishment behavior than standalone RNS.Reticulum() client instances.","status":"active","created_at":"2026-03-04T00:36:39.523Z","source":"extraction","content_hash":"0d7672061e6377d1","confidence":1,"last_reinforced":"2026-03-04T01:44:41.587Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kvUko1cM_8-v","mind":"default","section":"Known Issues","content":"Page browser 'Link establishment failed' debugging: added INFO-level logging to page_browser.py _get_or_create_link() — logs path found + identity recall result, link object creation with initial status, and ESTABLISHED/CLOSED callback events including teardown_reason. Released in v0.13.19 to diagnose daemon vs standalone RNS link behavior difference.","status":"active","created_at":"2026-03-04T00:39:06.214Z","source":"extraction","content_hash":"335140177043a584","confidence":1,"last_reinforced":"2026-03-04T00:41:34.175Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8xn_bH2gLf-Q","mind":"default","section":"Known Issues","content":"asyncio.Future() in _get_or_create_link() is created without explicit loop parameter — in Python 3.10+ this binds to the running loop. If RNS callbacks fire from a different thread (RNS uses daemon threads), run_coroutine_threadsafe scheduling onto self._event_loop is correct but requires _event_loop to be captured correctly at service start.","status":"active","created_at":"2026-03-04T00:39:06.215Z","source":"extraction","content_hash":"b86792b1e40e610d","confidence":1,"last_reinforced":"2026-03-04T00:41:34.175Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"awJtaYc9fB-k","mind":"default","section":"Known Issues","content":"PageBrowserService fetch_page() fails intermittently with INITIATOR_CLOSED (teardown_reason=2) — confirmed transient: same test code fails then succeeds on re-run. Root cause may be stale path entries after hub reconnection storm or RNS state from previous instances.","status":"active","created_at":"2026-03-04T00:41:34.174Z","source":"extraction","content_hash":"6cbd41171f1e416e","confidence":1,"last_reinforced":"2026-03-04T00:41:34.174Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"3qRbBLMGGW7J","mind":"default","section":"Known Issues","content":"PageBrowserService works correctly when run standalone with asyncio.run() + asyncio.Future + run_coroutine_threadsafe pattern — all link establishment patterns tested and functional in isolation. The intermittent failure in the TUI daemon context is not a code correctness issue.","status":"active","created_at":"2026-03-04T00:41:34.175Z","source":"extraction","content_hash":"00f687c2864b7085","confidence":1,"last_reinforced":"2026-03-04T00:41:34.175Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uVIl4G-RH2hT","mind":"default","section":"Known Issues","content":"Hub RNS config has 10 TCPClientInterface peers (Dublin, BetweenTheBorders, ON6ZQ, Beleth, Chicago Nomad, Acehoss, Quad4, Interloper, g00n Dallas, Loopback) — any peer cycling its TCP connection triggers id() change detection, producing the reconnect storm logged in hub pods.","status":"active","created_at":"2026-03-04T00:43:29.360Z","source":"extraction","content_hash":"5c1f9144c02eb7e8","confidence":1,"last_reinforced":"2026-03-04T00:43:29.360Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"j_nFHoIld082","mind":"default","section":"Known Issues","content":"Hub TCP reconnect debounce in v0.13.20 does not suppress 're-announce' triggers — _handle_reconnection calls are debounced but log messages and re-announce still fire ~20/10s on the hub. 'churn suppressed' log never appears, indicating the debounce path is not being hit.","status":"active","created_at":"2026-03-04T00:50:09.949Z","source":"extraction","content_hash":"ec4f49aa9bfce928","confidence":1,"last_reinforced":"2026-03-04T01:01:26.922Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"EmOiPsJyv7B5","mind":"default","section":"Known Issues","content":"public-hub build cached old styrened version (0.13.16) despite Containerfile updated to 0.13.20 — pip layer was cached. Requires --no-cache flag on podman build to force fresh pip install.","status":"active","created_at":"2026-03-04T00:50:09.949Z","source":"extraction","content_hash":"664035d11c614afd","confidence":1,"last_reinforced":"2026-03-04T00:50:09.949Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"btZ2C5dLTcp1","mind":"default","section":"Known Issues","content":"public-hub Containerfile.consolidated and justfile both updated to STYRENED_VERSION=0.13.20 (was 0.13.16). Image build in progress with --no-cache to force fresh PyPI install.","status":"active","created_at":"2026-03-04T00:50:09.950Z","source":"extraction","content_hash":"6adb40ed7c0706e0","confidence":1,"last_reinforced":"2026-03-04T00:50:09.950Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"vSsnX8zHzCwA"}
{"_type":"fact","id":"CTtqKf8L491W","mind":"default","section":"Known Issues","content":"_finish_success() in UpgradeScreen kills daemon then attempts to start new daemon via _start_daemon() before TUI re-exec — _start_daemon() uses shutil.which('styrened') which may not find the binary in pipx venv PATH, causing silent failure and TUI re-launch into DaemonSetupScreen. Under investigation in v0.13.21.","status":"active","created_at":"2026-03-04T01:10:57.256Z","source":"extraction","content_hash":"0dbc2645d2f24f08","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"Uk5EeJR_YsKF"}
{"_type":"fact","id":"MQ_xszpv2kPF","mind":"default","section":"Known Issues","content":"After successful upgrade (v0.13.21), TUI kills old daemon, attempts to start new daemon via Popen, then re-execs. If shutil.which('styrened') fails to locate the binary (pipx venv PATH issue), daemon never starts and re-launched TUI shows DaemonSetupScreen instead of connecting to running daemon.","status":"active","created_at":"2026-03-04T01:10:57.257Z","source":"extraction","content_hash":"94dc36760a5dced2","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"7hBjR9JJJzmB"}
{"_type":"fact","id":"1LWHCrJiqGY0","mind":"default","section":"Known Issues","content":"Hub TCP interface churn: Hub with many TCPClientInterface peers (10+) sees rapid interface object recreation by RNS. The id(iface) tracking in _check_interface_states() detects these as reconnections. Fixed with 30s debounce in v0.13.20, but root cause is RNS recreating interface objects during normal operation.","status":"active","created_at":"2026-03-04T01:13:43.329Z","source":"manual","content_hash":"5cda7245e337fda2","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6QQOWio8WaEQ","mind":"default","section":"Known Issues","content":"PageCacheService._crawl_loop() has a 60-second initial sleep before the first crawl refresh — no immediate crawl on start. Background refresh only fires for saved sites past their refresh_interval (default 3600s). Sites never refresh until their interval elapses.","status":"active","created_at":"2026-03-04T02:20:04.125Z","source":"extraction","content_hash":"1e4cf92bfe7a2c06","confidence":1,"last_reinforced":"2026-03-04T02:20:04.125Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9Quw-iUBsR5Z","mind":"default","section":"Known Issues","content":"IPC handler for CMD_PAGE_FETCH has write-through caching and fallback: on status=='ok' calls cache_svc.cache_page(); on non-ok status checks cache_svc.get_cached_page() and adds cached_content/cached_at/cached_content_length to the response dict. The TUI widget reads these fields in _load_page() correctly.","status":"active","created_at":"2026-03-04T02:20:04.125Z","source":"extraction","content_hash":"974ee83e71bd3a9a","confidence":1,"last_reinforced":"2026-03-04T02:20:04.125Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"GC1eHquEc9UH","mind":"default","section":"Known Issues","content":"Page cache tables (page_cache, saved_sites) are created via Base.metadata.create_all(engine) in daemon._start_page_browser() using init_db(). They do NOT use the ALTER TABLE migration pattern used by other NodeStore columns — they require the SQLAlchemy models to be present when init_db() first runs. Existing installs that previously ran init_db() without these models will have the tables missing until their DB is re-created or migrations run explicitly.","status":"active","created_at":"2026-03-04T02:20:04.125Z","source":"extraction","content_hash":"c8e9e3a8c67d8f3a","confidence":1,"last_reinforced":"2026-03-04T02:20:04.125Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kmgb9kmkiEFY","mind":"default","section":"Known Issues","content":"Reconnect debounce is global (single _last_reconnect_time) not per-interface — a reconnect on interface A suppresses all reconnects on other interfaces for the full 30s window, potentially silently dropping legitimate re-announces.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"11acc16ce0191253","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"hndajUSerDSE","mind":"default","section":"Known Issues","content":"page_cache has no eviction policy or TTL enforcement — cache grows unbounded. The PageResponse.cache_ttl field parsed from micron #!c=N header is never used by PageCacheService.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"0c6c8b2aa9089a15","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zptOOvqxci36","mind":"default","section":"Known Issues","content":"crawl_site() fires sequential fetch_page requests with no delay between pages — holds RNS.Link open for the entire crawl; no backoff on link drop mid-crawl.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"0bb504f67370c77d","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"R1PsMUwJZHdk","mind":"default","section":"Known Issues","content":"No IPC handler tests for five new page cache commands (save/remove/list/crawl/get_cached). No test for write-through cache integration in IPC handler. No test for reconnect debounce timing.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"0eb57180ee5a69b6","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Avx9g6DqsXYx","mind":"default","section":"Known Issues","content":"_start_daemon in upgrade.py and _auto_start_daemon in app.py are duplicate implementations of the same shutil.which + Popen pattern — must keep both in sync.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"85ccd6a71fc6d000","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"pBKfCFOnKueY","mind":"default","section":"Known Issues","content":"No CMD_PAGE_CLEAR_CACHE command exists — users cannot clear cached pages from the TUI or IPC.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"f26239c23813f310","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"NyAEntuusOJT","mind":"default","section":"Known Issues","content":"Background _crawl_loop has no circuit breaker for database errors — SQLite lock/corruption causes repeated warnings every 5 minutes forever.","status":"active","created_at":"2026-03-04T02:24:09.561Z","source":"extraction","content_hash":"a5c5ae0563257f66","confidence":1,"last_reinforced":"2026-03-04T02:24:09.561Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wmTOnekuhVwa","mind":"default","section":"Known Issues","content":"Adversarial code review (v0.13.23) found: crawl IPC timeout (fixed fire-and-forget), _last_reconnect_time uninit (fixed), global debounce suppresses all interfaces for 30s window, unbounded cache growth no eviction, no CMD_PAGE_CLEAR_CACHE, no IPC handler tests for 5 new page cache commands, _start_daemon duplicated in upgrade.py and app.py.","status":"active","created_at":"2026-03-04T02:27:56.702Z","source":"extraction","content_hash":"90f18de440b16d50","confidence":1,"last_reinforced":"2026-03-04T02:27:56.702Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"5uF7xl6po39C","mind":"default","section":"Known Issues","content":"Three separate SQLAlchemy engines created to same SQLite file in daemon (lines 398, 1043, 1208 of daemon.py) — three connection pools sharing one SQLite file risks 'database is locked' errors during concurrent crawl+message writes.","status":"active","created_at":"2026-03-04T02:27:56.702Z","source":"extraction","content_hash":"aa9affe3547f9a4b","confidence":1,"last_reinforced":"2026-03-04T02:27:56.702Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ZtGXXL0Euvi8","mind":"default","section":"Known Issues","content":"_last_reconnect_time initialized to 0.0 in RNSService.__init__ as of v0.13.24; getattr fallback removed from _check_interface_states(). Fixed.","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"3f3d99d731cd93cd","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"D8NzZL03Qd-i"}
{"_type":"fact","id":"6SfKUmW3A2lk","mind":"default","section":"Known Issues","content":"CMD_PAGE_CRAWL_SITE IPC handler fire-and-forget via asyncio.create_task fixed in v0.13.24 (was v0.13.23 partial fix in handlers.py only). Returns {started:true} immediately, bypassing 30s IPC timeout.","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"699b77e622890af1","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"HqiHXTfs-jHc"}
{"_type":"fact","id":"1UJu9O-pbb9P","mind":"default","section":"Known Issues","content":"CMD_PAGE_CRAWL_SITE IPC handler confirmed fire-and-forget in v0.13.24: asyncio.create_task launches crawl_site() in background and returns {started:true} immediately. Crawl progress not observable via IPC.","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"110b34455d852c25","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"aghAG80KOVeI"}
{"_type":"fact","id":"TCfzfCkmSRCj","mind":"default","section":"Known Issues","content":"_load_page exception handler now attempts cache fallback via bridge.page_get_cached() on IPC-level failures (fixed v0.13.24). Fallback works for both daemon non-ok responses AND connection/timeout exceptions when bridge is available.","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"072a86491e7682c9","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"1Obbmg4YyL02"}
{"_type":"fact","id":"7F_OZ9DJdVKg","mind":"default","section":"Known Issues","content":"_load_page exception handler now attempts cache fallback as of v0.13.24 — queries bridge.page_get_cached() before showing error. Only fails silently if bridge is None.","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"e56cb68d5999f402","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"6HqhRC2XGGjQ"}
{"_type":"fact","id":"GU9DPlRYXjq4","mind":"default","section":"Known Issues","content":"page_cache.py: PageResponse unused import removed, quoted type annotation unquoted, f-string without placeholder removed — all fixed in v0.13.24.","status":"active","created_at":"2026-03-04T02:31:08.597Z","source":"extraction","content_hash":"4d104e86f8dff78c","confidence":1,"last_reinforced":"2026-03-04T02:31:08.597Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"axpc-vta06pj"}
{"_type":"fact","id":"6VHznqqEVWr-","mind":"default","section":"Known Issues","content":"crawl_site() BFS queue switched from list.pop(0) to collections.deque.popleft() in v0.13.24 — O(1) dequeue. Fixed.","status":"active","created_at":"2026-03-04T02:31:08.597Z","source":"extraction","content_hash":"3e8c8291a9ae4a86","confidence":1,"last_reinforced":"2026-03-04T02:31:08.597Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"Ul8BG1hw2US_"}
{"_type":"fact","id":"ejbjvHPbWelY","mind":"default","section":"Known Issues","content":"Status tab redesign complete: Refresh button removed, DeviceStatusWidget now two-column cascade-colored layout showing announce data (NODE/MESH) and RPC+link enrichment (LINK/SYSTEM/NETWORK). Hub uptime=0/ip=127.0.0.1 shown with dim color to indicate useless-but-real K8s values.","status":"active","created_at":"2026-03-04T03:03:44.512Z","source":"extraction","content_hash":"2fc6a52b43ecfdbc","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"bufEk1LynO17"}
{"_type":"fact","id":"78pZDffScxLJ","mind":"default","section":"Known Issues","content":"Default speedtest payload sizes [256B→256KB] are LoRa-unsafe: 256KB at SF10=36min, SF12=2hr. PATH_DISCOVERY_TIMEOUT=15s and LINK_ESTABLISHMENT_TIMEOUT=15s are too short for multi-hop LoRa (needs 30-60s). REQUEST_TIMEOUT=10s is tight for LoRa. IPC speedtest timeout (120s) covers only ~2 payloads at SF9. No adaptive sizing, no progress reporting, no inter-test duty-cycle delay. BEING FIXED in current session.","status":"active","created_at":"2026-03-04T03:13:06.961Z","source":"extraction","content_hash":"0710f09a67617faf","confidence":1,"last_reinforced":"2026-03-04T03:13:06.961Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"5eboPilgLdUU"}
{"_type":"fact","id":"6EzYAZ44hkOg","mind":"default","section":"Known Issues","content":"MeshDeviceTree/exploration tables show 1725+ LXMF nodes, almost all STALE/LOST, drowning useful signal — UI filtering work in progress (hide-lost default, dim-stale, status counts bar).","status":"active","created_at":"2026-03-04T03:22:27.995Z","source":"extraction","content_hash":"45e6aa0eaaebfa1a","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"iJHW7leaMFel","mind":"default","section":"Known Issues","content":"Exploration LXMF tab drowning in STALE/LOST nodes (1725+ observed). Fix in progress: hide_lost=True by default, hide_stale toggle, status count bar. ReticumAnnounceTable and StyreneFleetTable getting toggle_hide_lost()/toggle_hide_stale() methods and status_counts property.","status":"active","created_at":"2026-03-04T03:22:27.995Z","source":"extraction","content_hash":"71f6d1dbaa33389f","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"aDcQCHNhMiZ1"}
{"_type":"fact","id":"U1TovjXgMuz-","mind":"default","section":"Known Issues","content":"IPC command collision FIXED: CMD_DATALINK_ESTABLISH/TEARDOWN/STATUS/QUERY were 0x50-0x53, aliasing CMD_TERMINAL_OPEN/INPUT/RESIZE/CLOSE. Datalink commands moved to 0x60-0x64 range; terminal commands remain at 0x50-0x53. No more IntEnum deduplication shadowing.","status":"active","created_at":"2026-03-04T03:35:29.300Z","source":"extraction","content_hash":"889f1b9c89bdab7a","confidence":1,"last_reinforced":"2026-03-04T03:35:29.300Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"fBWe04RJHWiK"}
{"_type":"fact","id":"E0D46R7DLtDs","mind":"default","section":"Known Issues","content":"_STATUS_CACHE in mesh_device_detail.py had unbounded growth — FIXED: now capped at 64 entries (_STATUS_CACHE_MAX=64), evicts oldest on overflow, and removes expired entries on read in _get_cached_status().","status":"active","created_at":"2026-03-04T03:37:22.115Z","source":"extraction","content_hash":"6c97f799def1da3b","confidence":1,"last_reinforced":"2026-03-04T03:37:22.115Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"5LslnJG_l5MD"}
{"_type":"fact","id":"nW74ma-2dYNf","mind":"default","section":"Known Issues","content":"_auto_fetch_status cache-hit path comment 'Still try to refresh in background' was false — FIXED: got_status is no longer set on cache hit, so datalink and LXMF fetch paths still execute for a fresh update. Cached data shows immediately, then is replaced with fresh data.","status":"active","created_at":"2026-03-04T03:37:22.115Z","source":"extraction","content_hash":"885ffe0d4d9b2a4a","confidence":1,"last_reinforced":"2026-03-04T03:37:22.115Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"vjq4irNH9qfJ"}
{"_type":"fact","id":"OuDVPpn1r3Mp","mind":"default","section":"Known Issues","content":"DirectLinkService._resolve future race fixed: set_result/set_exception wrapped in try/except asyncio.InvalidStateError instead of checking future.done() first — eliminates concurrent resolution window.","status":"active","created_at":"2026-03-04T03:37:22.116Z","source":"extraction","content_hash":"45a2b34e6c36ced7","confidence":1,"last_reinforced":"2026-03-04T03:37:22.116Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"WrwzRSIE6xbo"}
{"_type":"fact","id":"cz3Y512GvZGQ","mind":"default","section":"Known Issues","content":"Concurrency guard added to speedtest: DirectLinkService._speedtest_lock (asyncio.Lock created on start()) prevents concurrent speedtests — returns {status: busy} immediately if lock already held. _run_speedtest_inner() holds the lock for the duration.","status":"active","created_at":"2026-03-04T03:37:22.116Z","source":"extraction","content_hash":"935568592e854163","confidence":1,"last_reinforced":"2026-03-04T03:37:22.116Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"MtXyk1Dqa3Au"}
{"_type":"fact","id":"xGw9BHj0vJFB","mind":"default","section":"Known Issues","content":"Speedtest ASCII bar chart in DeviceStatusWidget now scales relative to peak result (max_kbps) not hardcoded 1000kbps — bar_len = int(kbps/max_kbps*20), giving meaningful visual differentiation across all link speeds.","status":"active","created_at":"2026-03-04T03:37:22.116Z","source":"extraction","content_hash":"2a1168c8d172269a","confidence":1,"last_reinforced":"2026-03-04T03:37:22.116Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"OZmx4WwBFw2i"}
{"_type":"fact","id":"YRR5Bkhc6ptJ","mind":"default","section":"Known Issues","content":"DirectLinkService.stop() now called in daemon.stop() — shutdown sequence stops direct link service before page server, sets _direct_link_service to None, logs errors gracefully.","status":"active","created_at":"2026-03-04T03:37:22.117Z","source":"extraction","content_hash":"aa0bb4a9683e4fec","confidence":1,"last_reinforced":"2026-03-04T03:37:22.117Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"ngM3RcZcSMnL"}
{"_type":"fact","id":"j4jsx7PDPrDu","mind":"default","section":"Known Issues","content":"NodeInfoPanel MESH count fixed (v0.13.28+): now uses merged node_store.get_styrene_nodes() + discover_devices() + _deduplicate_by_identity(), matching the data source used by MeshDeviceTree. Label changed from 'peers' to 'nodes', RNS 'no peers' changed to 'no interfaces'.","status":"active","created_at":"2026-03-04T03:54:47.216Z","source":"extraction","content_hash":"e653eeee54bc2ce6","confidence":1,"last_reinforced":"2026-03-04T03:55:41.741Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"k6BVZkbdrBTh","mind":"default","section":"Known Issues","content":"NodeInfoPanel MESH node count mismatch FIXED: now uses same data source as MeshDeviceTree (merged node_store.get_styrene_nodes() + discover_devices() + _deduplicate_by_identity()), so stored/historical nodes are included in the count.","status":"active","created_at":"2026-03-04T03:54:47.216Z","source":"extraction","content_hash":"fd98beae76ae5fd3","confidence":1,"last_reinforced":"2026-03-04T03:55:41.741Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"J5J2EoS0Kgf0"}
{"_type":"fact","id":"7fUuwMQyyANz","mind":"default","section":"Known Issues","content":"NodeInfoPanel RETICULUM section 'peers' count reflects RNS interface count (physical interfaces), not mesh device count. The STYRENE MESH count is now correct (fixed: uses node_store + live + dedup, same as MeshDeviceTree).","status":"active","created_at":"2026-03-04T03:54:47.216Z","source":"extraction","content_hash":"89d82d5c71ce747c","confidence":1,"last_reinforced":"2026-03-04T03:54:47.216Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"aVJMMKod1I0B"}
{"_type":"fact","id":"rOzG6b9yw9mn","mind":"default","section":"Known Issues","content":"WG allowed-ips routing gap: Macs use /128 per direct peer (IPv6 ULA) but need to switch to fd73:7479:7265:6e65::/64 via gateway when a Linux gateway joins, and revert to /128 when gateway dies. Dynamic route promotion/demotion not yet implemented in MeshVPNService._configure_peer().","status":"active","created_at":"2026-03-04T04:22:31.591Z","source":"extraction","content_hash":"f39d5c16455d1fd5","confidence":1,"last_reinforced":"2026-03-04T04:22:31.591Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"YawZbWuxx69N"}
{"_type":"fact","id":"91-FExWGaP3N","mind":"default","section":"Known Issues","content":"MeshDevice.last_seen_display appends announce count without space: `seen += f\"×{device.announce_count}\"` produces '4s agox8' instead of '4s ago ×8'. Two issues: missing space before ×, and × (U+00D7) may not render in SVG export font. File: models/mesh_device.py area around last_seen_display property.","status":"active","created_at":"2026-03-04T04:28:38.789Z","source":"extraction","content_hash":"2667f13984450a97","confidence":1,"last_reinforced":"2026-03-04T04:38:52.705Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"a5dVPsHtuwUE","mind":"default","section":"Known Issues","content":"Dashboard 'c' key calls action_open_chat which requires a device selected in MeshDeviceTree — silently fails (no notification, no error) when tree has no selected leaf node with identity data.","status":"active","created_at":"2026-03-04T04:28:38.790Z","source":"extraction","content_hash":"4a0cd886a20d125c","confidence":1,"last_reinforced":"2026-03-04T04:28:38.790Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ZXy3AnuWCOG6","mind":"default","section":"Known Issues","content":"mesh_vpn.py: handle_handshake is called from RNS thread while _peers dict is accessed from asyncio thread — no lock. CPython GIL protects simple gets/sets but compound operations are a race.","status":"active","created_at":"2026-03-04T04:32:22.083Z","source":"extraction","content_hash":"50d19e8414bef9e1","confidence":1,"last_reinforced":"2026-03-04T04:32:22.083Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XIH4FTls76ax","mind":"default","section":"Known Issues","content":"mesh_vpn.py: _detect_local_endpoint uses socket.connect(8.8.8.8) which returns LAN IP — useless for internet peers. Users must set endpoint manually with no documentation or error.","status":"active","created_at":"2026-03-04T04:32:22.083Z","source":"extraction","content_hash":"bbed5cfc000789d5","confidence":1,"last_reinforced":"2026-03-04T04:32:22.083Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"I27q2kEP1hon","mind":"default","section":"Known Issues","content":"mesh_vpn.py: No IPC commands for VPN status or handshake trigger — TUI cannot observe or control mesh VPN state.","status":"active","created_at":"2026-03-04T04:32:22.084Z","source":"extraction","content_hash":"e84deac7ff48127b","confidence":1,"last_reinforced":"2026-03-04T04:32:22.084Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"YeNJ271koiRC","mind":"default","section":"Known Issues","content":"mesh_vpn.py: No firewall rule added in styrened.nix for WireGuard UDP listen_port (51820) — connections silently dropped on NixOS fleet devices with default firewall.","status":"active","created_at":"2026-03-04T04:32:22.084Z","source":"extraction","content_hash":"3a918c074dd16111","confidence":1,"last_reinforced":"2026-03-04T04:32:22.084Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"toVuUztxjndl","mind":"default","section":"Known Issues","content":"Bug fixes from tui-bug-handoff.zip: (1) '4s ago×8' spacing fixed (space added before ×, × character preserved as U+00D7). (2) NodeInfoPanel two-column drift: `rich.cells.cell_len()` replaces `wcwidth.wcswidth()` for accurate width measurement; module-level `import re` and `from rich.cells import cell_len as rich_cell_len` moved out of hot loop. (3) HighlightedPanel on_resize: width-change guard restored — height-only redraws are no-ops since borders depend only on width.","status":"active","created_at":"2026-03-04T04:33:47.114Z","source":"extraction","content_hash":"c8cf15089d5f1a72","confidence":1,"last_reinforced":"2026-03-04T04:48:20.048Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"9fUSxD6js1D-"}
{"_type":"fact","id":"235ifVahIYYw","mind":"default","section":"Known Issues","content":"wcswidth('●') returns 1 (same as len) — the alignment drift from Unicode symbols is a terminal/font rendering issue not fixable via wcwidth. rich.cells.cell_len() matches Rich's actual rendering, making it the correct choice.","status":"active","created_at":"2026-03-04T04:33:47.114Z","source":"extraction","content_hash":"9322e82a1fce18a0","confidence":1,"last_reinforced":"2026-03-04T04:45:00.314Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"nmaWx0XZI1D6","mind":"default","section":"Known Issues","content":"Regression tests for tui-bug-handoff written at tests/unit/test_tui_bug_handoff.py — covers Bug 1 (announce count spacing '4s ago ×8') and Bug 3 (rich_cell_len usage for two-column alignment).","status":"active","created_at":"2026-03-04T04:35:35.038Z","source":"extraction","content_hash":"4cee3cfac032c4c3","confidence":1,"last_reinforced":"2026-03-04T04:48:20.047Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lupeCc8H-Pqb","mind":"default","section":"Known Issues","content":"mesh_vpn.py bugs itemized in v0.13.28 session are now partially addressed: double @dataclass on MeshVPNConfig fixed, subnet_prefix default mismatch ('10.73' vs 'fd73:7479:7265:6e65') fixed in services/config.py loader.","status":"active","created_at":"2026-03-04T04:38:52.703Z","source":"extraction","content_hash":"dd7a92c228f28ccf","confidence":1,"last_reinforced":"2026-03-04T04:38:52.703Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"QsybVf0_teTi","mind":"default","section":"Known Issues","content":"Capture script in tui-bug-handoff uses 'c' key which opens chat/contacts shortcut, not 'b' — contacts screen never opens. Bug 2 is a test script issue, not a TUI defect.","status":"active","created_at":"2026-03-04T04:43:27.124Z","source":"extraction","content_hash":"c1c8e50e1fefb178","confidence":1,"last_reinforced":"2026-03-04T04:43:27.124Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"m0mlx_uaNI8z","mind":"default","section":"Known Issues","content":"NodeInfoPanel two-column alignment fix implemented: render() now returns a Rich Table (Table.grid) instead of str with manual padding — Rich's layout engine measures column widths during render, eliminating all Unicode width drift. Table.grid(padding=(0,2)), min_width=42 on left column, Text.from_markup() for each cell.","status":"active","created_at":"2026-03-04T04:46:29.842Z","source":"extraction","content_hash":"752d04ff6a7ce2d9","confidence":1,"last_reinforced":"2026-03-04T04:48:20.048Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"eL0RhBTNiRPV"}
{"_type":"fact","id":"5gVGWpATVZir","mind":"default","section":"Known Issues","content":"NodeInfoPanel column drift (Bug 3) FIXED: render() now returns Rich Table instead of manual col_width=44 string padding — Rich measures widths internally, immune to Unicode ambiguity. Tests updated to use _render_panel() helper (Rich Console capture) since render() returns RenderableType not str.","status":"active","created_at":"2026-03-04T04:46:29.843Z","source":"extraction","content_hash":"77f0c8bb30e41189","confidence":1,"last_reinforced":"2026-03-04T04:48:20.048Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"pKb2uMZlxSs4"}
{"_type":"fact","id":"8mBn5dvfDTGX","mind":"default","section":"Known Issues","content":"test_rns_online_no_interfaces_shows_no_peers now asserts 'no interfaces' (updated in session) — test was stale asserting 'no peers' before the label text changed.","status":"active","created_at":"2026-03-04T04:48:20.046Z","source":"extraction","content_hash":"f262c8ab4f815f0f","confidence":1,"last_reinforced":"2026-03-04T04:48:20.046Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"CW72AhzvZEdU"}
{"_type":"fact","id":"bnKfqhTlylRw","mind":"default","section":"Known Issues","content":"HighlightedPanel border overflow (Bug 4) root-cause traced: math is correct (inner_width+2 == panel width), so visual overflow in screenshots is an SVG font-metrics artifact with box-drawing characters — not fixable at the Python/CSS code level.","status":"active","created_at":"2026-03-04T04:48:20.047Z","source":"extraction","content_hash":"f6fe94325a5fa965","confidence":1,"last_reinforced":"2026-03-04T04:48:20.047Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"upClhXCdCeu1","mind":"default","section":"Known Issues","content":"HighlightedPanel bottom-right border overflow (Bug 4) FIXED in v0.13.31 — on_mount uses call_after_refresh() to avoid width=0 race; on_resize re-renders when width transitions from 0 to non-zero.","status":"active","created_at":"2026-03-04T04:50:42.633Z","source":"extraction","content_hash":"5f6253dd86789f8f","confidence":1,"last_reinforced":"2026-03-04T04:50:42.633Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"XBMOyIjh3glt"}
{"_type":"fact","id":"WLlKsakLtkA6","mind":"default","section":"Known Issues","content":"DeviceDetailScreen SELECT DEVICE panel (Bug 5) FIXED in v0.13.31 — HighlightedPanel mount-time race fixed; provision.py placeholder Static removed (not needed).","status":"active","created_at":"2026-03-04T04:50:42.633Z","source":"extraction","content_hash":"eb3d68bf59ceacd7","confidence":1,"last_reinforced":"2026-03-04T04:50:42.633Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"sdrFaeMj-iTK"}
{"_type":"fact","id":"0WtoGv-O1wCR","mind":"default","section":"Known Issues","content":"HighlightedPanel border rendering root cause confirmed and fixed: on_mount fires with width=0 (widget not laid out), on_resize width guard then skips first valid resize. Fixed via call_after_refresh() + guard extended to catch 0→nonzero width transition.","status":"active","created_at":"2026-03-04T04:50:42.633Z","source":"extraction","content_hash":"ef4bfe38054cf441","confidence":1,"last_reinforced":"2026-03-04T04:50:42.633Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"yKSNKrETaDNH"}
{"_type":"fact","id":"2PKrZDDQn8xj","mind":"default","section":"Known Issues","content":"HighlightedPanel on_resize guard extended: re-renders when width changes OR when width transitions from 0 to non-zero (first valid layout after mount-time zero-size). on_mount uses call_after_refresh().","status":"active","created_at":"2026-03-04T04:50:42.634Z","source":"extraction","content_hash":"0f8c24348bbb7040","confidence":1,"last_reinforced":"2026-03-04T04:50:42.634Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"TJijdGz6dvmR"}
{"_type":"fact","id":"op8RPuZXzWpP","mind":"default","section":"Known Issues","content":"HighlightedPanel on_resize: call_after_refresh() on mount + guard for 0→nonzero transition — borders rendered exactly once after first valid layout. No unnecessary re-renders on height-only resizes.","status":"active","created_at":"2026-03-04T04:50:42.634Z","source":"extraction","content_hash":"7fa427a0ea63002c","confidence":1,"last_reinforced":"2026-03-04T04:50:42.634Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"9tUsbYIEONqw"}
{"_type":"fact","id":"YoIqiULlkhuU","mind":"default","section":"Known Issues","content":"Dashboard 'c' key action_open_chat() now notifies user 'Select a device in the mesh tree first.' when no tree node is selected, instead of silently doing nothing.","status":"active","created_at":"2026-03-04T04:50:42.634Z","source":"extraction","content_hash":"af57b555bb591602","confidence":1,"last_reinforced":"2026-03-04T04:50:42.634Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Lo1tZYJxg52S","mind":"default","section":"Known Issues","content":"mesh_vpn.py self._identity_hash crash FIXED: _start_mesh_vpn() now calls get_operator_identity_object() directly to obtain identity_hash instead of referencing the non-existent self._identity_hash attribute.","status":"active","created_at":"2026-03-04T04:53:56.712Z","source":"extraction","content_hash":"01e380aca5715bc4","confidence":1,"last_reinforced":"2026-03-04T05:07:53.942Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"VnAydJ83t8rp"}
{"_type":"fact","id":"INGcd4TOpbEE","mind":"default","section":"Known Issues","content":"mesh_vpn.py cryptography dependency FIXED: 'cryptography>=41.0' added to pyproject.toml core dependencies.","status":"active","created_at":"2026-03-04T04:53:56.712Z","source":"extraction","content_hash":"6d8ffe47d38ae0c2","confidence":1,"last_reinforced":"2026-03-04T05:07:53.942Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"OCS17edvu_r2"}
{"_type":"fact","id":"jRYyUL-CT2Gv","mind":"default","section":"Known Issues","content":"mesh_vpn.py fixes in progress: identity_hash AttributeError fixed, cryptography dep added, rsplit prefix extraction replaced with ipaddress-based extract_prefix() helper function.","status":"active","created_at":"2026-03-04T04:53:56.713Z","source":"extraction","content_hash":"4e078c099293650a","confidence":1,"last_reinforced":"2026-03-04T04:53:56.713Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"dd4EvlSHus8l","mind":"default","section":"Known Issues","content":"mesh_vpn.py: parse_handshake_response() now checks for 'error' key before field validation — raises ValueError('Handshake rejected: {error}') on subnet_prefix_mismatch or other errors. Version field validation also added to both parse functions.","status":"active","created_at":"2026-03-04T04:55:37.514Z","source":"extraction","content_hash":"9dbad72ac2113edf","confidence":1,"last_reinforced":"2026-03-04T04:55:37.514Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"gkSxWdLBy-0I"}
{"_type":"fact","id":"C6weUNxxJDa0","mind":"default","section":"Known Issues","content":"mesh_vpn.py subnet prefix extraction FIXED: replaced rsplit-based approach with extract_prefix() helper using ipaddress.IPv6Network — handles compressed IPv6, zero groups, all forms correctly. extract_prefix(mesh_ip, prefix_len) uses IPv6Network.network_address.exploded and takes first N groups based on prefix_len//16.","status":"active","created_at":"2026-03-04T04:55:37.515Z","source":"extraction","content_hash":"804d949651bce777","confidence":1,"last_reinforced":"2026-03-04T05:19:57.259Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"MKPmvxSjljSW"}
{"_type":"fact","id":"jkOBTMiF6qnN","mind":"default","section":"Known Issues","content":"mesh_vpn.py: _create_interface() now returns bool and _start() logs 'degraded mode' warning on False — failure no longer silently passes. _create_interface_linux() and _create_interface_macos() both return bool and propagate failure reason.","status":"active","created_at":"2026-03-04T04:55:37.515Z","source":"extraction","content_hash":"0dbd5d1be168b532","confidence":1,"last_reinforced":"2026-03-04T05:07:53.941Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"5peALPH_kDBQ"}
{"_type":"fact","id":"NYlIaeMXBgXr","mind":"default","section":"Known Issues","content":"mesh_vpn.py: _create_interface_linux() and _create_interface_macos() now check for required binaries (ip/wg on Linux, wg-quick on macOS) via shutil.which() and return False with error log if missing. macOS wg-quick calls now use sudo prefix.","status":"active","created_at":"2026-03-04T04:55:37.515Z","source":"extraction","content_hash":"534d91f7dee7955a","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"_l1sNpSxQ1Vp"}
{"_type":"fact","id":"w9nDktcAjCU8","mind":"default","section":"Known Issues","content":"mesh_vpn.py: Binary detection added in _create_interface_linux() (checks ip, wg, batctl if gateway) and _create_interface_macos() (checks wg-quick) via shutil.which() — missing binaries now produce clear error messages and return False.","status":"active","created_at":"2026-03-04T04:55:37.515Z","source":"extraction","content_hash":"41dd7784ad403250","confidence":1,"last_reinforced":"2026-03-04T04:55:37.515Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"c1NAU5n3bDEU"}
{"_type":"fact","id":"iS9AEq33qPus","mind":"default","section":"Known Issues","content":"mesh_vpn.py: handle_handshake() task exception now observed — loop.create_task() result stored and _on_peer_config_done() callback added via task.add_done_callback(), logging exceptions from _configure_peer().","status":"active","created_at":"2026-03-04T04:55:37.516Z","source":"extraction","content_hash":"dc98fad4cefedd76","confidence":1,"last_reinforced":"2026-03-04T04:55:37.516Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"ggsTipGZWiQ8"}
{"_type":"fact","id":"h5fnG9YqMGUm","mind":"default","section":"Known Issues","content":"mesh_vpn.py: parse_handshake_response() now checks 'error' key first and raises ValueError('Handshake rejected: {error}') — subnet_prefix_mismatch returns proper error. Both parse functions also validate HANDSHAKE_VERSION field.","status":"active","created_at":"2026-03-04T04:55:37.516Z","source":"extraction","content_hash":"7da6f2e9d8dfce0a","confidence":1,"last_reinforced":"2026-03-04T05:07:53.941Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"lif8D1o7g2t5"}
{"_type":"fact","id":"4ALiGrxktfu7","mind":"default","section":"Known Issues","content":"mesh_vpn.py: No tests for handle_handshake or initiate_handshake — core handshake flow now has test_handshake_handler_rejects_prefix_mismatch and version/error-response tests. initiate_handshake flow still has zero test coverage.","status":"active","created_at":"2026-03-04T05:07:53.941Z","source":"extraction","content_hash":"38e8e97f058a79b9","confidence":1,"last_reinforced":"2026-03-04T05:07:53.941Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"GmNWn2uVvxwJ"}
{"_type":"fact","id":"U0TS1_esUVbY","mind":"default","section":"Known Issues","content":"mesh_vpn.py: BATMAN-ADV batctl if add wg-styrene won't work — WireGuard is L3, BATMAN-ADV hard interfaces need L2 Ethernet. FIXED: replaced direct WG→bat0 bridging with VXLAN-over-WG per-peer tunnel architecture: bat0→vxlan-<peer> (L2/UDP)→wg-styrene (L3). Each peer gets a vxlan-<hash[:10]> interface added to bat0. VNI=7379, dstport=4789, local/remote=WG mesh IPs.","status":"active","created_at":"2026-03-04T05:13:16.304Z","source":"extraction","content_hash":"fd8064f9cf38cf20","confidence":1,"last_reinforced":"2026-03-04T05:13:16.304Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"ofKGg37YS2dE"}
{"_type":"fact","id":"PkeXDOQIgS-5","mind":"default","section":"Known Issues","content":"mesh_vpn.py Issue 9 remaining: _peers dict accessed from RNS thread (handle_handshake) and asyncio thread (_configure_peer) — no asyncio.Lock. CPython GIL protects simple dict ops but compound read-modify-write is a race.","status":"active","created_at":"2026-03-04T05:13:16.305Z","source":"extraction","content_hash":"2fe812a1b791d6a9","confidence":1,"last_reinforced":"2026-03-04T05:19:57.259Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Gn5pqpmg7DTT","mind":"default","section":"Known Issues","content":"MeshVPNService VXLAN-over-WG: VXLAN uses WG mesh IPv6 (ULA) addresses as local/remote endpoints, but VXLAN encapsulates in UDP which must traverse the WG tunnel — sending VXLAN UDP over a WG interface whose route is only the peer's /128 works, but routing of the VXLAN multicast/broadcast frames depends on BATMAN-ADV's own MAC learning, not on WG routing. The 'dev wg-styrene' parameter roots the VXLAN in the WG interface correctly.","status":"active","created_at":"2026-03-04T05:16:01.971Z","source":"extraction","content_hash":"2a7347dccbf6c2b9","confidence":1,"last_reinforced":"2026-03-04T05:16:01.971Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"LV9QHkuUuiB6","mind":"default","section":"Known Issues","content":"MeshVPNService _configure_peer() calls _create_vxlan_for_peer() only when self.config.gateway is True — non-gateway nodes never get VXLAN overlays, meaning non-gateway Linux nodes that connect to a gateway won't extend bat0 on their end. Only the gateway side creates VXLANs, leaving a half-tunnel.","status":"active","created_at":"2026-03-04T05:16:01.971Z","source":"extraction","content_hash":"a3c242c50ba928d4","confidence":1,"last_reinforced":"2026-03-04T05:16:01.971Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Nrqgw62onjL0","mind":"default","section":"Known Issues","content":"VXLAN for_peer uses ALL_VXLANS share VNI=7379 — all peers share the same VNI. On the gateway, all per-peer VXLAN interfaces have the same VNI but different remote IPs (point-to-point unicast mode), which is correct. However if two peers somehow share the same VNI with different remote addresses on the same VXLAN namespace, kernel behavior varies. The point-to-point (unicast remote) configuration avoids multicast but VNI isolation between Styrene fleets using same VNI is absent.","status":"active","created_at":"2026-03-04T05:16:01.972Z","source":"extraction","content_hash":"61a0e19e095267fb","confidence":1,"last_reinforced":"2026-03-04T05:16:01.972Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"impsGrv8VyzX","mind":"default","section":"Known Issues","content":"_destroy_all_vxlans() iterates self._peers but only destroys VXLANs that match entries still in _peers at teardown time. If a peer was manually removed from _peers without calling _destroy_vxlan_for_peer, the VXLAN interface leaks and is not cleaned up.","status":"active","created_at":"2026-03-04T05:16:01.972Z","source":"extraction","content_hash":"c59b42ce0cf805a9","confidence":1,"last_reinforced":"2026-03-04T05:16:01.972Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Rb4Zrya0apcA","mind":"default","section":"Known Issues","content":"VXLAN UDP port 4789 must be open in the firewall on Linux gateway nodes for VXLAN frames to traverse the WG tunnel — the existing styrened.nix only adds WG listen_port (51820) firewall rule issue; no VXLAN port exception is documented or implemented.","status":"active","created_at":"2026-03-04T05:16:01.972Z","source":"extraction","content_hash":"1591c42ac6eac970","confidence":1,"last_reinforced":"2026-03-04T05:16:01.972Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"otYPTSvfTIdC","mind":"default","section":"Known Issues","content":"mesh_vpn.py Issue 10 FIXED: second gateway evicts first gateway's /64 allowed-ips via _update_peer_allowed_ips() before assigning /64 to new gateway. Old gateway downgraded to /128. _destroy_all_vxlans() called in stop(). _create_vxlan_for_peer() called from _configure_peer() when self.config.gateway and platform==LINUX.","status":"active","created_at":"2026-03-04T05:16:01.972Z","source":"extraction","content_hash":"386059dcb74d3538","confidence":1,"last_reinforced":"2026-03-04T05:16:01.972Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"Q_trvPka9rpl"}
{"_type":"fact","id":"t4VzUfDJauHW","mind":"default","section":"Known Issues","content":"mesh_vpn.py: extract_prefix returns zero-padded groups via IPv6Address.exploded (e.g., 'fd73:0079:...') but config subnet_prefix may use shortened form ('fd73:79:...'). String comparison in handshake will falsely reject peers on same subnet if config prefix isn't fully zero-padded. Normalize config prefix through extract_prefix before comparing.","status":"active","created_at":"2026-03-04T05:19:57.258Z","source":"extraction","content_hash":"07f4fb82302ff0a2","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6j2E4JQq0p6s","mind":"default","section":"Known Issues","content":"mesh_vpn.py: fd leak in _create_interface_linux() — if os.fchmod() or os.write() raises between mkstemp and os.close(fd), fd is never closed. finally block only unlinks key_path, not closes fd.","status":"active","created_at":"2026-03-04T05:19:57.258Z","source":"extraction","content_hash":"334595c099808963","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Xi4u5PcfkSQ2","mind":"default","section":"Known Issues","content":"mesh_vpn.py: unused import 'field' from dataclasses being removed. Also 'import field' was unused since no dataclass fields use field().","status":"active","created_at":"2026-03-04T05:19:57.259Z","source":"extraction","content_hash":"1be6bf83f05477bb","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2xF-bHATyPCp","mind":"default","section":"Known Issues","content":"mesh_vpn.py: no MTU clamping on VXLAN interfaces — VXLAN+WG adds ~110 bytes overhead, so VXLAN MTU should be ≤1330 to avoid fragmentation. LoRa-bridged links need much lower MTU. No MTU set.","status":"active","created_at":"2026-03-04T05:19:57.259Z","source":"extraction","content_hash":"b9f728de99320f17","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WNKaFgGJ9r7G","mind":"default","section":"Known Issues","content":"mesh_vpn.py: no bat0 existence check before 'batctl meshif bat0 if add' — if batman-adv is not loaded or bat0 doesn't exist, batctl fails silently and the VXLAN interface is left up but not bridged into bat0.","status":"active","created_at":"2026-03-04T05:19:57.259Z","source":"extraction","content_hash":"42abac49b35f6241","confidence":1,"last_reinforced":"2026-03-04T05:25:23.530Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"R94OC_9pTqFu","mind":"default","section":"Known Issues","content":"mesh_vpn.py: wg-quick on macOS requires root only for initial interface creation (wg-quick up/down). Subsequent 'wg set peer' calls to add/remove peers work without root via the wireguard-go userspace socket. Strategy: sudo wg-quick up once at VPN start (5-min sudo cache means single password prompt), then per-peer wg set without sudo. macOS path now writes .conf + logs instructions; full sudo integration being revisited.","status":"active","created_at":"2026-03-04T05:25:23.529Z","source":"extraction","content_hash":"bc6701db725e7f03","confidence":1,"last_reinforced":"2026-03-04T05:25:23.529Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"2BRdcVp0HIt7"}
{"_type":"fact","id":"GHrkDauE4FDc","mind":"default","section":"Known Issues","content":"Settings overlay stacking bug FIXED in v0.13.32: all action_open_* guards replaced with _screen_in_stack(T) helper (any(isinstance(s, T) for s in screen_stack)) to prevent double-push during Textual screen transitions. Applied to Settings, Inbox, Contacts, Provision, and UpgradeScreen.","status":"active","created_at":"2026-03-04T05:26:28.104Z","source":"extraction","content_hash":"dd70ee9ab0cc1e8c","confidence":1,"last_reinforced":"2026-03-04T05:26:28.104Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"TPNXW151QcJy","mind":"default","section":"Known Issues","content":"Dashboard 'r' refresh now triggers _check_for_updates() in v0.13.32 — previously only ran on app.on_mount; _check_for_updates uses @work(exclusive=True) so repeated 'r' presses are safe.","status":"active","created_at":"2026-03-04T05:26:28.106Z","source":"extraction","content_hash":"420e1adf750ed391","confidence":1,"last_reinforced":"2026-03-04T05:26:28.106Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"paekfB9Nazcx","mind":"default","section":"Patterns & Conventions","content":"Configuration hierarchy: ~/.styrene/config.yaml → /etc/styrene/config.yaml → defaults. Version canonical in src/styrened/__init__.py, mirrored in VERSION file for Nix","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"01717750612bcea9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.887Z","reinforcement_count":6,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"sjDw3fPk8CX3","mind":"default","section":"Patterns & Conventions","content":"Strict TDD practiced: red-green-refactor cycle, test hierarchy of unit/integration/scenario/k8s tests","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"9ea8b8694629d74f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"NaPqmfNZzO4X","mind":"default","section":"Patterns & Conventions","content":"For verification after changes use 'just test-unit' (~5s). Do NOT glob tests/tui/ into unit test runs - IPC integration tests have 30-second socket timeouts","status":"active","created_at":"2026-03-01T14:09:41.817Z","source":"extraction","content_hash":"1b4ab34c1a56fa3a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rKE_dp1Jv780","mind":"default","section":"Patterns & Conventions","content":"Network settings test file at tests/unit/tui/screens/test_network_settings.py covers: generate_rns_config output for peers/auto/server/transport, config serialization round-trip via save_core_config/load_core_config, and validation (empty host, invalid port, hub mode without interfaces). All 17 tests pass.","status":"active","created_at":"2026-03-01T14:11:25.101Z","source":"extraction","content_hash":"bb95fd4a30ba6370","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":10,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9KCcy7M_DYp4","mind":"default","section":"Patterns & Conventions","content":"Peer rows in Network tab are dynamically composed via _compose_peer_rows() and managed with on_add_peer/on_remove_peer handlers. CSS classes: .peer-row, .peer-name-input, .peer-host-input, .peer-port-input, .peer-remove-btn","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"ff8d7996b63327de","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":12,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"NsUN2y573DCp","mind":"default","section":"Patterns & Conventions","content":"TUI settings tests at tests/tui/screens/test_settings_tui.py has 24 async Textual tests covering composition, form interaction, validation, persistence, keyboard bindings, identity icon validation, IPC failure handling. All pass after Network tab redesign.","status":"active","created_at":"2026-03-01T14:14:13.349Z","source":"extraction","content_hash":"4e61acc0c7bb93db","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ei0tS7pwTZSb","mind":"default","section":"Patterns & Conventions","content":"Release flow requires: version bump in src/styrened/__init__.py and VERSION, commit, tag (vX.Y.Z), push to main, which triggers Argo CI to build wheel + OCI + publish to PyPI. Work not released upstream via this flow 'does not exist'.","status":"active","created_at":"2026-03-01T14:36:14.011Z","source":"extraction","content_hash":"9976d73f250f60c2","confidence":1,"last_reinforced":"2026-03-04T04:33:47.112Z","reinforcement_count":59,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6-qkp2QWNiHO","mind":"default","section":"Patterns & Conventions","content":"PR #10 merged feat(tui): replace hub selector with Reticulum interface manager — squash-merged to main via gh CLI, fast-forwarded locally. 3 files changed, 505 insertions, 116 deletions.","status":"active","created_at":"2026-03-01T14:36:14.011Z","source":"extraction","content_hash":"e54215e26ae7dada","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"R50nupgRA-ER","mind":"default","section":"Patterns & Conventions","content":"v0.10.42 released: fix(tui) illegible settings description text — removed [dim] markup, changed .setting-description color from $secondary to #7a9a9a","status":"active","created_at":"2026-03-01T14:52:15.558Z","source":"extraction","content_hash":"13b146f7e652ac96","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"oDBPxaRSWuRP","mind":"default","section":"Patterns & Conventions","content":"Peer row CSS changed from fixed-width .peer-name-input (width:20) to proportional 2fr sizing for both name and host fields, with port at 1fr max-width:14 and remove btn at width:5 — v0.10.43","status":"active","created_at":"2026-03-01T14:55:06.495Z","source":"extraction","content_hash":"df0c90caa5dc3e00","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vkb5AEe6slbX","mind":"default","section":"Patterns & Conventions","content":"v0.10.43 released: fix(tui) peer name truncated — changed peer row from fixed to proportional sizing (2fr/2fr/1fr), added column headers","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"d264cce92a1508b7","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"QMj-RkjZLCAu","mind":"default","section":"Patterns & Conventions","content":"TUI CSS lives in src/styrened/tui/styles/imperial_crt.tcss — peer row layout uses proportional fr units: name 2fr, host 2fr, port 1fr (max-width:14), remove-btn width:5. Header row (.peer-header-row) at height:1 with .peer-header labels in #7a9a9a color.","status":"active","created_at":"2026-03-01T14:55:06.496Z","source":"extraction","content_hash":"4e3259f288f47054","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"HSPChLnW_KW1"}
{"_type":"fact","id":"9-DT-_BrQP-v","mind":"default","section":"Patterns & Conventions","content":"v0.10.44 released: fix(tui) upgrade modal black artifact CSS and restart TUI via os.execvp instead of exit-to-terminal","status":"active","created_at":"2026-03-01T15:02:17.863Z","source":"extraction","content_hash":"e355335b6cb85d09","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"j3yV8DCiPTXN","mind":"default","section":"Patterns & Conventions","content":"styrene-pypi sync-version.yml compares meta-package version against latest stable styrened on PyPI; skips bump if meta-package version >= styrened version (e.g., 0.10.45 >= 0.10.45 = nothing to do)","status":"active","created_at":"2026-03-01T15:27:28.780Z","source":"extraction","content_hash":"5b761401ec733d45","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XCG-FIBJ3Uoc","mind":"default","section":"Patterns & Conventions","content":"v0.10.46 released: feat(tui) add [ ] keyboard shortcuts for settings tab navigation — arrow keys consumed by Input widgets, so screen-level bindings delegate to TabbedContent","status":"active","created_at":"2026-03-01T15:57:41.287Z","source":"extraction","content_hash":"fe76997610de79e0","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"x4aPGeL72yyc","mind":"default","section":"Patterns & Conventions","content":"v0.10.47 released: fix(tui) new peer rows mounted inside button container instead of panel — on_add_peer now mounts into hp-content Vertical before the add-button row","status":"active","created_at":"2026-03-01T15:59:52.149Z","source":"extraction","content_hash":"df0c9e7685d4ea93","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"jjZnx8x0iiVx","mind":"default","section":"Patterns & Conventions","content":"styrene-pypi sync-version.yml redesigned to publish directly to PyPI in the same workflow instead of relying on release event → publish.yml cascade. Single 'publish' job builds wheel, publishes via pypa/gh-action-pypi-publish, then creates GitHub release.","status":"active","created_at":"2026-03-01T16:08:27.288Z","source":"extraction","content_hash":"f66963580bb30c45","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"wqPEt_q3dBwB","mind":"default","section":"Patterns & Conventions","content":"styrene-pypi sync-version.yml auto-syncs version to match latest stable styrened on PyPI. Runs on schedule (hourly during active dev), repository_dispatch, or manual trigger. Now builds and publishes directly to PyPI in same workflow (no longer relies on release event cascade). Creates GitHub Release after successful publish.","status":"active","created_at":"2026-03-01T16:08:27.289Z","source":"extraction","content_hash":"ee1a695420e9d07f","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"ft_hEF7dOfax"}
{"_type":"fact","id":"qJGKeaVPyfRy","mind":"default","section":"Patterns & Conventions","content":"styrene-pypi meta-package has sync-version.yml that auto-syncs version and >=X.Y.Z pins from latest stable styrened on PyPI. Now publishes directly to PyPI in the sync workflow (build → publish → release) rather than creating a release to trigger a separate publish.yml.","status":"active","created_at":"2026-03-01T16:08:27.290Z","source":"extraction","content_hash":"52809f7ac0c650fc","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"A-Nf_KEKzUv9"}
{"_type":"fact","id":"CJcRS_-h7T5E","mind":"default","section":"Patterns & Conventions","content":"v0.10.49 released: fix TUI first-run config used internal LAN IP (192.168.0.102) instead of rns.styrene.io for community hub peer","status":"active","created_at":"2026-03-01T16:13:12.911Z","source":"extraction","content_hash":"97d2a6c4546156a5","confidence":1,"last_reinforced":"2026-03-02T21:30:04.888Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MSzmeerFqckw","mind":"default","section":"Patterns & Conventions","content":"TUI settings screen CSS redesigned with semantic widget classes: .setting-input (full-width text), .setting-input-narrow (ports/channels, width:16), .setting-select (dropdowns, width:40), .setting-checkbox (auto-width toggles). Labels fixed at width:22 with right-aligned content.","status":"active","created_at":"2026-03-01T16:14:58.144Z","source":"extraction","content_hash":"6818b6ded9e8535e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"2EN95Q1h4zrf","mind":"default","section":"Patterns & Conventions","content":"TUI settings layout CSS classes (v0.10.50): .setting-input (1fr, full-width text), .setting-input-narrow (width:16, max:20 for ports/numbers), .setting-select (width:40, max:50 for dropdowns), .setting-checkbox (width:auto for toggles). Labels fixed at width:22 with right content-align. This prevents controls from stretching full-width unnecessarily. Announce interval uses Select with human-readable presets (15s through 1d) instead of raw Input.","status":"active","created_at":"2026-03-01T16:18:05.491Z","source":"manual","content_hash":"8cf872f4f4255b3c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"gVZ8sq9tKIC5","mind":"default","section":"Patterns & Conventions","content":"Adversarial CSS audit initiated for entire TUI rendering pipeline — checking all inline CSS across 19+ widget/screen files for missing background inheritance, scrollbar styling gaps, and Textual $variable resolution inconsistencies that could cause color fall-through artifacts","status":"active","created_at":"2026-03-01T16:32:58.195Z","source":"extraction","content_hash":"053f3b1ead26420d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"I7IppBKOB0WM","mind":"default","section":"Patterns & Conventions","content":"Setting description text in TUI now uses $text-muted theme variable (resolves to #9ca3af via STYRENE_DARK['muted_foreground']) instead of hardcoded #7a9a9a; [dim] Rich markup removed from all description strings (v0.10.42+)","status":"active","created_at":"2026-03-01T16:35:01.630Z","source":"extraction","content_hash":"302153a26b5dd726","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"2WmrRSM8ZPk7"}
{"_type":"fact","id":"ctKDYXFlpzRx","mind":"default","section":"Patterns & Conventions","content":"TUI rendering cascade rule: Every scrollable container must explicitly set (1) background: transparent to inherit from parent, (2) scrollbar-background: transparent or theme surface, (3) scrollbar-color: theme border/muted with hover/active states, (4) zero hardcoded hex colors — everything through theme variables. Applies to both Textual and future ratatui port.","status":"active","created_at":"2026-03-01T16:37:28.650Z","source":"manual","content_hash":"a9891cdbed71b479","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"FFSN94jKaLi6","mind":"default","section":"Patterns & Conventions","content":"styrene_brand theme has custom variable $text-muted (maps to muted_foreground #9ca3af dark / #6b7280 light) for description text, column headers, and other secondary text. Added in v0.10.54. Available theme vars: $primary, $secondary, $warning, $error, $success, $accent, $foreground, $background, $surface, $panel, $boost, $border, $text-muted (custom).","status":"active","created_at":"2026-03-01T16:37:33.889Z","source":"manual","content_hash":"8bd1e722e3ad8a4a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"7b3eeqVEF4w6","mind":"default","section":"Patterns & Conventions","content":"imperial_crt.tcss has zero hardcoded hex colors as of v0.10.54. All colors reference theme variables. Hardcoded hex is only allowed in theme definition files (styrene_brand.py, color_cascade.py, imperial_crt.py) which are the single source of truth for color values.","status":"active","created_at":"2026-03-01T16:37:38.628Z","source":"manual","content_hash":"4103f576f9105e65","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"jJW4vW1yXSSd","mind":"default","section":"Patterns & Conventions","content":"v0.10.54 released: fix(tui) comprehensive scrollbar and color cascade audit — global scrollbar styling, transparent background inheritance for Log/RichLog, replaced all 6 hardcoded hex colors in TCSS with theme variables, added $text-muted custom variable","status":"active","created_at":"2026-03-01T16:37:44.940Z","source":"extraction","content_hash":"d02b3c3e6f6f4a04","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"n4fv-g4S2cUt","mind":"default","section":"Patterns & Conventions","content":"TUI CSS audit completed: zero hardcoded hex colors remain in imperial_crt.tcss — all replaced with theme variables ($text-muted, $border, $success, $error). Hardcoded hex values exist only in theme definition files (styrene_brand.py, color_cascade.py, imperial_crt.py) which are the single source of truth.","status":"active","created_at":"2026-03-01T16:37:44.940Z","source":"extraction","content_hash":"b2c1196bffe85446","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":"URi4VHOSpXv_"}
{"_type":"fact","id":"_VJEpgU64JRU","mind":"default","section":"Patterns & Conventions","content":"Settings screen peer rows: on_add_peer must mount new rows into the hp-content Vertical (add_btn_row.parent), NOT into the button's own parent Horizontal. Use: add_btn_row.parent.mount(new_row, before=add_btn_row). Fixed in v0.10.47 after rows were stacking horizontally.","status":"active","created_at":"2026-03-01T16:38:09.614Z","source":"manual","content_hash":"2522aea042ca1c85","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"e8pPG_PfHOCN","mind":"default","section":"Patterns & Conventions","content":"Settings tab keyboard navigation: [ and ] are screen-level bindings that delegate to TabbedContent.action_previous/next_tab. Arrow keys don't work for tab switching because Input widgets inside TabPanes consume them. Added in v0.10.46.","status":"active","created_at":"2026-03-01T16:38:14.884Z","source":"manual","content_hash":"3a0ee2d78119653a","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"nu4jGBll8rnv","mind":"default","section":"Patterns & Conventions","content":"justfile 'release' recipe now auto-publishes: bumps version, commits, tags, pushes to origin, then runs 'just publish'. The 'publish' recipe builds wheel+sdist via python -m build and uploads to PyPI via twine, eliminating the wait for Argo CI pipeline.","status":"active","created_at":"2026-03-01T16:48:45.057Z","source":"extraction","content_hash":"234146c953d441aa","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":9,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"dd6B1-2BChI3","mind":"default","section":"Patterns & Conventions","content":"v0.10.56 released: fix(tui) add missing sys import for restart atexit handler — _restart_tui() referenced sys.argv[0] but sys was never imported in app.py","status":"active","created_at":"2026-03-01T16:48:45.058Z","source":"extraction","content_hash":"129098bf235952e9","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zgFMZapxYFnB","mind":"default","section":"Patterns & Conventions","content":"Dashboard tree device lines use Rich markup with cascade colors: status symbols (● active, ◐ stale, ○ lost), bold name, last_seen with ×N announce count suffix, and optional ✉N unread badge — all using cascade.bright/medium/dim theme tokens. Identity hash removed from tree line in v0.10.60 (visible in detail screen instead).","status":"active","created_at":"2026-03-01T17:27:13.441Z","source":"extraction","content_hash":"049c9f4c81328ac1","confidence":1,"last_reinforced":"2026-03-02T21:30:04.889Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"w6nSgK6PsTyD","mind":"default","section":"Patterns & Conventions","content":"Dashboard tree device lines use Rich markup with cascade colors: status symbols (● active, ◐ stale, ○ lost), bold name, compact last_seen with ×N announce count suffix, and optional ✉N unread badge — all using cascade.bright/medium/dim theme tokens. Identity hash removed from tree labels in v0.10.60 to prevent truncation (visible in device detail screen instead).","status":"active","created_at":"2026-03-01T17:27:13.441Z","source":"extraction","content_hash":"2bccae500e967f22","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"fuS_fgk9hVxY"}
{"_type":"fact","id":"0d-KarMInbp3","mind":"default","section":"Patterns & Conventions","content":"v0.10.59 released: feat(tui) group mesh devices by receiving interface in tree view. v0.10.60 released: fix(tui) tree device labels truncated — dropped identity hash, compact format with ×N announce count and ✉N unread badge.","status":"active","created_at":"2026-03-01T17:28:18.286Z","source":"extraction","content_hash":"881759f69f576e2d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"GvdcVB1BR4Yn"}
{"_type":"fact","id":"NrCXXq8Abwop","mind":"default","section":"Patterns & Conventions","content":"v0.10.63 released: fix(tui) tree refresh not updating display — added self.refresh() after _load_data() since Tree.clear()+root.add() doesn't auto-trigger render. v0.10.64 released: fix(tui) HighlightedPanel missing bottom border + black gaps between panels.","status":"active","created_at":"2026-03-01T17:36:58.545Z","source":"extraction","content_hash":"3b9525edf96b362c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IR2GOfIVIEo3","mind":"default","section":"Patterns & Conventions","content":"v0.10.65 released: fix(tui) tree refresh stale — clear line cache and force layout. v0.10.66 released: fix(tui) hp-content height:1fr broke auto-height panels (upgrade modal).","status":"active","created_at":"2026-03-01T17:49:41.181Z","source":"extraction","content_hash":"42b28e786b8a7bac","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"YYpXHWbGbnt9","mind":"default","section":"Patterns & Conventions","content":"Textual 8.0 key binding pattern: Screen-level bindings that must fire regardless of focused child widget need priority=True on the Binding. Without it, focused widgets (Tree, Input, etc.) consume keys before they bubble to Screen even if the widget has no handler for that key.","status":"active","created_at":"2026-03-01T18:16:43.038Z","source":"extraction","content_hash":"f7fe15d468585867","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Gpy_p2NkGuR3","mind":"default","section":"Patterns & Conventions","content":"Unit test suite has 2145 passed + 5 skipped in ~6s as of v0.10.76. All round-trip config tests pass after accounting for well-known hub merge behavior.","status":"active","created_at":"2026-03-01T18:35:33.231Z","source":"extraction","content_hash":"cb1576c1752eee95","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":10,"decay_rate":0.05,"supersedes":"0OXfYsitt4u5"}
{"_type":"fact","id":"lvGpTGhH1T8t","mind":"default","section":"Patterns & Conventions","content":"`just publish` now publishes to PyPI immediately via twine AND fires a repository_dispatch event (styrened-release) to styrene-lab/styrene-pypi with exact version in client_payload, triggering immediate meta-package sync. Uses gh api to POST dispatches. Non-fatal if dispatch fails.","status":"active","created_at":"2026-03-01T18:53:14.159Z","source":"extraction","content_hash":"6e7582c6a97841c6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"mY_8iSr2J6Ef"}
{"_type":"fact","id":"OVfZ1AdNw5Yn","mind":"default","section":"Patterns & Conventions","content":"`just publish` now fires `repository_dispatch` event (type: `styrened-release`) to `styrene-lab/styrene-pypi` with `client_payload.version` after twine upload. This triggers immediate meta-package sync without waiting for hourly cron or PyPI CDN freshness. Uses `--skip-existing` on twine so dispatch always fires even on retry. [2026-03-01]","status":"active","created_at":"2026-03-01T18:54:52.395Z","source":"manual","content_hash":"a1a4743ed7af2656","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"3-ojjJSyHoQR","mind":"default","section":"Patterns & Conventions","content":"Image format assessment written as docs/image-format-assessment.md in styrene-rs repo with Status/Context/Decision/Comparison/Wire Protocol Design/Dependencies/Open Questions sections — follows architecture decision record pattern.","status":"active","created_at":"2026-03-02T04:14:33.287Z","source":"extraction","content_hash":"519afc3fdfbce2c8","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"eM9yN_8_lTTt","mind":"default","section":"Patterns & Conventions","content":"styrene-rs workspace Cargo.toml uses workspace-level package metadata (edition, rust-version, license, repository) and workspace dependencies for shared crate versions. Profile overrides set opt-level=2 for styrene-rns in dev/test builds. Workspace lints forbid unsafe_code and deny dbg_macro/todo.","status":"active","created_at":"2026-03-02T04:34:19.770Z","source":"extraction","content_hash":"06e3f2211a0069e6","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"AjZdyqo1aJ2c","mind":"default","section":"Patterns & Conventions","content":"v0.10.79 released: feat(tui) double ctrl+c to quit, single ctrl+c pops to dashboard. Mirrors pi agent UX pattern.","status":"active","created_at":"2026-03-02T04:49:35.893Z","source":"extraction","content_hash":"0a5131e75ea324e8","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Aq0oqD-5f4_n","mind":"default","section":"Patterns & Conventions","content":"_count_leaf_nodes(tree) helper function in test_dashboard_tui.py counts Tree leaf nodes with non-None data — the Tree-compatible replacement for DataTable.row_count assertions.","status":"active","created_at":"2026-03-02T05:05:38.494Z","source":"extraction","content_hash":"45991f9ed4674c9d","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uA_T00ahfSgn","mind":"default","section":"Patterns & Conventions","content":"Dashboard chat integration tests rewritten: unread column assertions replaced with tree leaf label string matching (e.g., assert '✉3' in label), _get_row_data DataTable helper replaced with _get_leaf_labels/_count_leaves tree helpers, detail navigation tests navigate down twice (past branch header to leaf) before pressing enter.","status":"active","created_at":"2026-03-02T05:06:54.176Z","source":"extraction","content_hash":"3b01d0d7d68bd08c","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rwhNZVE48qaW","mind":"default","section":"Patterns & Conventions","content":"styrened 0.10.81 released: fix(tui) identity migration from legacy config.yaml to core-config.yaml — _parse_config_dict now reads identity/lxmf sections, _overlay_core_config preserves legacy identity when core-config.yaml has defaults and auto-migrates. 8 new unit tests added. 2153 passed total.","status":"active","created_at":"2026-03-02T06:54:39.864Z","source":"extraction","content_hash":"2774820ca52487aa","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ttB_uIruIxLu","mind":"default","section":"Patterns & Conventions","content":"styrened 0.10.81 released (2026-03-02): fix(tui) identity migration from legacy config.yaml to core-config.yaml. _parse_config_dict now reads identity section, _overlay_core_config migrates legacy identity forward. Published to PyPI.","status":"active","created_at":"2026-03-02T06:54:39.864Z","source":"extraction","content_hash":"d2c53c1e4e2cd555","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"8DKgY4VXWTM5"}
{"_type":"fact","id":"MwggLDpwd3mZ","mind":"default","section":"Patterns & Conventions","content":"Identity migration unit tests at tests/unit/tui/test_identity_migration.py: 5 tests for _parse_config_dict identity parsing, 3 tests for _overlay_core_config migration logic. Patches target styrened.services.config.load_core_config (not tui.services.config) since the import is local inside the function.","status":"active","created_at":"2026-03-02T06:54:39.866Z","source":"extraction","content_hash":"63eecdbd980697ca","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"bN0DgEcY6ROz","mind":"default","section":"Patterns & Conventions","content":"init_db() in models/messages.py accepts str|None for db_path — passing an Engine object causes nested Engine(sqlite:///Engine(...)) URL corruption. Always pass str(db_path) or None, never an Engine object.","status":"active","created_at":"2026-03-02T16:06:13.926Z","source":"extraction","content_hash":"28c645d8aaa35b0b","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":5,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Z__9RqwgJ7sa","mind":"default","section":"Patterns & Conventions","content":"All TUI tests (674) pass including rewritten dashboard chat integration and chat edge cases tests against MeshDeviceTree API. Total: 2153 unit + 674 TUI = 2827 tests green as of 2026-03-02","status":"active","created_at":"2026-03-02T16:14:00.940Z","source":"extraction","content_hash":"b076195aae8c519e","confidence":1,"last_reinforced":"2026-03-02T21:30:04.890Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Fh5zdpIRXHCT","mind":"default","section":"Patterns & Conventions","content":"styrened 0.11.0 stable released (2026-03-02): Network settings overhaul, identity persistence/migration, well-known community hubs, double ctrl+c quit, 2827 tests green. First minor version bump since 0.10.x series (81 patches).","status":"active","created_at":"2026-03-02T16:16:41.907Z","source":"manual","content_hash":"d43eec3b1e109343","confidence":1,"last_reinforced":"2026-03-02T21:30:04.891Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vGcqRn0Coay2","mind":"default","section":"Patterns & Conventions","content":"Textual Input widget override pattern: setting 'border: none' is insufficient — focus state rule '&:focus { border: tall $border }' from DEFAULT_CSS re-applies the border. Must use 'border: none !important' on both the base selector and a separate ':focus' selector to fully suppress Input borders.","status":"active","created_at":"2026-03-02T22:19:52.805Z","source":"extraction","content_hash":"78fc57b80c8d1ea9","confidence":1,"last_reinforced":"2026-03-02T22:24:08.655Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kBrF8_85EF35","mind":"default","section":"Patterns & Conventions","content":"Chat message status icon rendering pattern: read → cascade.bright color, delivered → cascade.dim color, failed → [red bold] icon + [red italic] full line. Applied in three places: _render_message_line(), _update_bubble_status(), _update_optimistic_status().","status":"active","created_at":"2026-03-02T22:24:08.654Z","source":"extraction","content_hash":"41b1a40682594581","confidence":1,"last_reinforced":"2026-03-02T22:26:30.135Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"0AJLYf5OUxmi","mind":"default","section":"Patterns & Conventions","content":"v0.11.2 released: fix(tui) suppress Input widget default border in chat input bar — border:none !important on both normal and :focus states, Input height:1.","status":"active","created_at":"2026-03-02T22:24:08.654Z","source":"extraction","content_hash":"b4e9131882d1fe06","confidence":1,"last_reinforced":"2026-03-02T22:24:08.654Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9-wP0QGxjJzn","mind":"default","section":"Patterns & Conventions","content":"v0.11.3 released: fix(tui) render failed messages in red with bold ✗ icon — visually distinct failure state across all three chat rendering paths.","status":"active","created_at":"2026-03-02T22:24:08.654Z","source":"extraction","content_hash":"ca7bced3c279fa0c","confidence":1,"last_reinforced":"2026-03-02T22:26:30.134Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"xCEjDZJRGh4E","mind":"default","section":"Patterns & Conventions","content":"Relative time formatting pattern: _relative_time(ts: float) returns 'just now' (<60s), 'Xm ago' (<1h), 'Xh ago' (<1d), 'Xd ago' (<7d), or 'Mon DD' strftime for older timestamps. Static method on ContactsScreen.","status":"active","created_at":"2026-03-02T22:28:33.916Z","source":"extraction","content_hash":"39b73a7144f51426","confidence":1,"last_reinforced":"2026-03-02T22:28:33.916Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"VmhGFsPqFHfk","mind":"default","section":"Patterns & Conventions","content":"MenuBarState dataclass uses threading.Lock for safe cross-thread state sharing between rumps NSApplication main thread and asyncio IPC background thread. snapshot() returns a tuple copy under lock; update() takes keyword-only args.","status":"active","created_at":"2026-03-02T22:34:26.751Z","source":"extraction","content_hash":"02fa2245cd497cbf","confidence":1,"last_reinforced":"2026-03-02T22:34:26.751Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"CeFJyJWRvW9h","mind":"default","section":"Patterns & Conventions","content":"styrened 0.11.4 released: feat(tui) enrich contacts screen with presence (●/◐/○) and last message preview with relative time. ContactsScreen DataTable now has ALIAS/STATUS/LAST MESSAGE/PEER HASH columns.","status":"active","created_at":"2026-03-02T22:34:26.751Z","source":"extraction","content_hash":"877a2964eb3f739c","confidence":1,"last_reinforced":"2026-03-02T22:34:26.751Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"KJ7pqxrFKZup","mind":"default","section":"Patterns & Conventions","content":"styrened 0.11.5 released: feat(tui) add macOS menu bar agent for inbox notifications — rumps NSApplication loop, asyncio IPC in background thread, native osascript notifications, MenuBarState with threading.Lock, 'styrened menubar' CLI entry point.","status":"active","created_at":"2026-03-02T22:50:54.652Z","source":"extraction","content_hash":"6f16c2170443fdda","confidence":1,"last_reinforced":"2026-03-02T22:50:54.652Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"S5rpeOQTKGmG","mind":"default","section":"Patterns & Conventions","content":"v0.11.5 released: feat(tui) add macOS menu bar agent for inbox notifications. 2153 unit + 11 menubar tests pass.","status":"active","created_at":"2026-03-02T22:50:54.652Z","source":"extraction","content_hash":"712b3bc9d6aaa108","confidence":1,"last_reinforced":"2026-03-02T22:50:54.652Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"y2bZhLSD5O9H","mind":"default","section":"Patterns & Conventions","content":"Clipboard content priority for attachment staging: (1) NSPasteboardTypePNG image data, (2) NSPasteboardTypeTIFF image data (converted to PNG via Pillow), (3) public.file-url Finder references, (4) text content that looks like a file path. Implemented in styrened.tui.menubar.clipboard.read_clipboard_attachment().","status":"active","created_at":"2026-03-02T22:52:57.809Z","source":"extraction","content_hash":"d11901c3ca37126c","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ZcgAzUmEbOqd","mind":"default","section":"Patterns & Conventions","content":"Clipboard module imports (has_clipboard_image, read_clipboard_attachment) moved to module-level in chat_widget.py with try/except ImportError fallbacks returning False/None — enables unittest.mock.patch to work correctly in tests.","status":"active","created_at":"2026-03-02T22:54:56.413Z","source":"extraction","content_hash":"17b872d4f4793fd8","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"9gYd_JJpjj9V","mind":"default","section":"Patterns & Conventions","content":"TestPasteHandler tests require patching 'styrened.tui.widgets.chat_widget.has_clipboard_image' and 'styrened.tui.widgets.chat_widget.read_clipboard_attachment' (module-level imports) rather than the clipboard module directly, since mock.patch requires the attribute to exist at the target path.","status":"active","created_at":"2026-03-02T22:54:56.413Z","source":"extraction","content_hash":"e3902f8f3941f553","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qQXCqwDQPvoo","mind":"default","section":"Patterns & Conventions","content":"clipboard.py _attachment_from_path() does not use a module-level DEFAULT_MAX_FILE_SIZE constant — size limit logic is embedded in the function body, so tests cannot patch 'styrened.tui.menubar.clipboard.DEFAULT_MAX_FILE_SIZE'.","status":"active","created_at":"2026-03-02T22:54:56.413Z","source":"extraction","content_hash":"42edaed63753ebe9","confidence":1,"last_reinforced":"2026-03-02T22:54:56.413Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"_sqJ6PpDIeFQ","mind":"default","section":"Patterns & Conventions","content":"Clipboard attachment tests at tests/tui/test_clipboard.py cover: _looks_like_path, _generate_screenshot_name, _attachment_from_path (valid/nonexistent/too-large/unknown-mime), _tiff_to_png (valid TIFF via Pillow, invalid data), and ClipboardAttachment dataclass fields. 20+ tests total.","status":"active","created_at":"2026-03-02T22:54:56.414Z","source":"extraction","content_hash":"7b27235ebd4f44d6","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Lls4W9pYTHLS","mind":"default","section":"Patterns & Conventions","content":"v0.11.6 released: feat(tui) clipboard-native attachment system — macOS NSPasteboard integration (PNG/TIFF/file-url/path priority), clipboard.py module, on_paste() clipboard detection, action_attach_file tries clipboard before path prompt. 77 chat+clipboard tests pass.","status":"active","created_at":"2026-03-02T23:38:27.779Z","source":"extraction","content_hash":"09f29e5ed841e5a5","confidence":1,"last_reinforced":"2026-03-02T23:38:27.779Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"lTsTQxkPzUKf","mind":"default","section":"Patterns & Conventions","content":"Ctrl+Y is the primary clipboard-attach binding in ChatWidget (replaces ctrl+p which conflicts with Textual's command palette COMMAND_PALETTE_BINDING). ctrl+o removed entirely. Binding uses priority=True.","status":"active","created_at":"2026-03-03T00:33:18.436Z","source":"extraction","content_hash":"f15bbf3432579af2","confidence":1,"last_reinforced":"2026-03-03T01:44:49.133Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"AkNEEB0nqVUp"}
{"_type":"fact","id":"aByezKQwY0rb","mind":"default","section":"Patterns & Conventions","content":"v0.11.9 released: fix(tui) change clipboard-attach binding from ctrl+p (conflicts with Textual command palette) to ctrl+y — ctrl+y unused by both Textual and Input widgets, evokes 'yank from clipboard'.","status":"active","created_at":"2026-03-03T00:33:18.438Z","source":"extraction","content_hash":"57abeed95e3cac9a","confidence":1,"last_reinforced":"2026-03-03T00:33:18.438Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"us9aSdRHERtT"}
{"_type":"fact","id":"aKGjDOrGn2-C","mind":"default","section":"Patterns & Conventions","content":"on_input_submitted empty-message guard pattern: must exempt staged attachments — 'if not message and not (self._pending_attachment and not self._pending_attachment.get(\"awaiting_path\")): return' to allow image-only sends.","status":"active","created_at":"2026-03-03T00:41:12.673Z","source":"extraction","content_hash":"a7d323afd24adb5f","confidence":1,"last_reinforced":"2026-03-03T00:41:12.673Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"TtSS8NwvCvm5","mind":"default","section":"Patterns & Conventions","content":"v0.11.12 released: fix(tui) allow sending attachment with empty message text + input placeholder shows staged filename for clear user feedback + removed debug notify() calls from v0.11.11.","status":"active","created_at":"2026-03-03T00:41:12.673Z","source":"extraction","content_hash":"17c603fead0f18b1","confidence":1,"last_reinforced":"2026-03-03T00:44:28.859Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"v-ZwNv2rvEGg","mind":"default","section":"Patterns & Conventions","content":"IPC handler validation pattern: use `if req.field is None` not `if not req.field` for string fields that may legitimately be empty (e.g., message content when attachment is present). Guard: `if not req.content and not req.attachment_data_b64: return error`.","status":"active","created_at":"2026-03-03T00:44:28.860Z","source":"extraction","content_hash":"321fe93792b16d33","confidence":1,"last_reinforced":"2026-03-03T00:44:28.860Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"55XQrvab0KOL","mind":"default","section":"Patterns & Conventions","content":"Optimistic message display pattern for attachments: when message text is empty but attachment is staged, display text defaults to '📎 {filename}' for visual feedback in the chat history.","status":"active","created_at":"2026-03-03T00:53:47.493Z","source":"extraction","content_hash":"d6c6ff0099e82f4c","confidence":1,"last_reinforced":"2026-03-03T00:53:47.493Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"FF2bfhvmuPF4","mind":"default","section":"Patterns & Conventions","content":"v0.12.0 released: feat(tui) enrich Node Info panel with COMMS and TRAFFIC sections — COMMS (unread/chats/contacts/auto-reply) in left column after IDENTITY, TRAFFIC (sent/received/pending/links/role) in right column after STYRENE. Data from IPC bridge in daemon mode, local SQLite fallback in standalone mode.","status":"active","created_at":"2026-03-03T01:00:45.612Z","source":"extraction","content_hash":"04c2fd77211ed45f","confidence":1,"last_reinforced":"2026-03-03T01:00:45.612Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MB5OtOa-Lh37","mind":"default","section":"Patterns & Conventions","content":"ChatWidget incremental refresh pattern: compare new_ids set against old_ids_set; on first load do full rebuild with date separators; on subsequent polls only append new messages using msg_id not in old_ids_set; update statuses in-place when IDs unchanged","status":"active","created_at":"2026-03-03T01:29:47.184Z","source":"extraction","content_hash":"e794e54fbb626fc6","confidence":1,"last_reinforced":"2026-03-03T01:46:20.001Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-vqoVx7_ZVy-","mind":"default","section":"Patterns & Conventions","content":"ChatWidget date separator implementation: _date_separator_if_needed() returns (Static|None, str|None) tuple; callers await container.mount(sep) when sep is not None; _last_displayed_date tracks last rendered date for incremental append continuity","status":"active","created_at":"2026-03-03T01:34:46.547Z","source":"extraction","content_hash":"6939042688748604","confidence":1,"last_reinforced":"2026-03-03T01:34:46.547Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ztx9yM-wUj3I","mind":"default","section":"Patterns & Conventions","content":"v0.12.2 released: fix(tui) incremental chat refresh (append-only, no teardown) + date separators (── Today ──, ── Yesterday ──, ── Mon, Mar DD ──) between message groups from different days","status":"active","created_at":"2026-03-03T01:34:46.547Z","source":"extraction","content_hash":"ec14222301c1cf4f","confidence":1,"last_reinforced":"2026-03-03T01:34:46.547Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ad6CE0OXgxHs","mind":"default","section":"Patterns & Conventions","content":"v0.13.0 released: feat(tui) redesign chat conversation UX — messages gravity-pinned to bottom, no 'ME:' prefix on outgoing (position/color distinguish sender), incoming shows sender name on own line above content, conversation title simplified to name-only, input bar reduced to 2 lines with heavy separator border.","status":"active","created_at":"2026-03-03T01:43:46.904Z","source":"extraction","content_hash":"2b0668906abdaf8c","confidence":1,"last_reinforced":"2026-03-03T01:43:46.904Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"qyHAYOU3qaxP","mind":"default","section":"Patterns & Conventions","content":"_do_stage_from_clipboard() must be called synchronously (not via run_worker) to satisfy macOS NSPasteboard main-thread requirement. Pattern: direct call from action handlers and on_paste, never inside run_worker() or asyncio tasks.","status":"active","created_at":"2026-03-03T02:19:10.730Z","source":"extraction","content_hash":"3fce95120cc65b30","confidence":1,"last_reinforced":"2026-03-03T02:19:10.730Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"DdU3eZ0fXs7p","mind":"default","section":"Patterns & Conventions","content":"Dashboard _fetch_daemon_status parallelism pattern: create asyncio.Task for each IPC call (get_conversations, get_contacts, get_auto_reply) before awaiting any, reducing 4 sequential round-trips to ~1 RTT.","status":"active","created_at":"2026-03-03T02:21:07.813Z","source":"extraction","content_hash":"b6c771313f568f49","confidence":1,"last_reinforced":"2026-03-03T02:21:07.813Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zGSNZky_JHYu","mind":"default","section":"Patterns & Conventions","content":"v0.13.1 released: fix(tui) TUI audit rework — NSPasteboard main-thread, dashboard worker races, hops persistence, SQL aggregate queries, parallel IPC calls, incremental chat deletion detection, date separator reset on rebuild, optimistic status by content match.","status":"active","created_at":"2026-03-03T03:06:17.090Z","source":"extraction","content_hash":"e2945a5fac6ff4d0","confidence":1,"last_reinforced":"2026-03-03T03:06:17.090Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"STVJMCkZWWGe","mind":"default","section":"Patterns & Conventions","content":"v0.13.2 released: fix VIA column shows interface + next-hop transport hash suffix for mesh device announce disambiguation.","status":"active","created_at":"2026-03-03T03:11:41.066Z","source":"extraction","content_hash":"60b51fe09b251185","confidence":1,"last_reinforced":"2026-03-03T03:11:41.066Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"uRfNFZcvGEVH","mind":"default","section":"Patterns & Conventions","content":"Screen guard pattern for toggle-style navigation: action_push_screen_X checks isinstance(self.screen, XScreen) before push_screen() to prevent duplicate stacking on repeated key presses.","status":"active","created_at":"2026-03-03T03:16:51.851Z","source":"extraction","content_hash":"5a14922f7ce1a6b6","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"4PN7pxdTeoFW","mind":"default","section":"Patterns & Conventions","content":"v0.13.3 released: feat(tui) add Styrene fleet tab as first tab in Exploration screen — StyreneFleetTable with NAME/VERSION/CAPABILITIES/HOPS/VIA/STATUS/LAST ANNOUNCE columns, deduplication by identity, search/sort/select support.","status":"active","created_at":"2026-03-03T03:16:51.851Z","source":"extraction","content_hash":"c3a5a7a5cc80cb84","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"O-5_bHQKZ2X-","mind":"default","section":"Patterns & Conventions","content":"v0.13.4 released: fix(tui) prevent Settings screen stacking on repeated backtick press — isinstance guard in action_push_screen_settings.","status":"active","created_at":"2026-03-03T03:16:51.851Z","source":"extraction","content_hash":"489ea61b48ed43e5","confidence":1,"last_reinforced":"2026-03-03T03:16:51.851Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"81gsSsRJBqKh","mind":"default","section":"Patterns & Conventions","content":"v0.13.5 released: fix prevent all navigable screens (Inbox/Contacts/Provision/Settings) from stacking on repeated keypress — all four now use isinstance guard before push_screen.","status":"active","created_at":"2026-03-03T19:48:09.514Z","source":"extraction","content_hash":"f6a9d1c43e70792c","confidence":1,"last_reinforced":"2026-03-03T19:48:09.514Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"_bP-w1hHHLGi","mind":"default","section":"Patterns & Conventions","content":"Retry hint pattern for failed chat messages: failed outgoing bubble renders content normally + '↻ press R to retry' hint on second line using cascade.dim color. Retry hint is rendered inline in _render_message_line(), not as a separate widget.","status":"active","created_at":"2026-03-03T19:51:13.591Z","source":"extraction","content_hash":"caab4b352842f7fd","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"I4goPYH8TCOm","mind":"default","section":"Patterns & Conventions","content":"_retry_after_refresh() helper: refreshes message list from DB, then finds the most recent failed bubble with message_id > 0 and matching raw_content, then calls _retry_single(). Used when optimistic bubble (id=0) is selected for retry.","status":"active","created_at":"2026-03-03T19:51:13.591Z","source":"extraction","content_hash":"ad39ffb46be05830","confidence":1,"last_reinforced":"2026-03-03T20:02:49.777Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"cVVz21O_EWQ8","mind":"default","section":"Patterns & Conventions","content":"v0.13.6 released: fix chat retry system rework (R auto-selects last failed, Ctrl+R retries all, per-message worker groups, optimistic bubble refresh on failure, attachment-only optimistic status fix) + fix lxmf_service._resolve_identity Strategy 5 (request_path + 10s poll loop to recover from identity cache eviction).","status":"active","created_at":"2026-03-03T20:02:49.781Z","source":"extraction","content_hash":"98137fda192c5bc6","confidence":1,"last_reinforced":"2026-03-03T20:02:49.781Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"kv-0sgXT2W5K","mind":"default","section":"Patterns & Conventions","content":"IPC handler send pattern: wrap synchronous lxmf_service.send_message() / send_with_retry() calls in `await loop.run_in_executor(None, lambda: ...)` to avoid blocking the asyncio event loop during identity resolution delays.","status":"active","created_at":"2026-03-03T20:06:00.607Z","source":"extraction","content_hash":"3a7eb447c43e1c16","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"RZXZjoF2pG0D","mind":"default","section":"Patterns & Conventions","content":"_SEND_TIMEOUT in chat_widget.py bumped to 30.0s (from 15.0s) to accommodate Strategy 5 identity resolution delay (up to 10s wait) plus actual send time.","status":"active","created_at":"2026-03-03T20:06:00.607Z","source":"extraction","content_hash":"e10104fa5957b096","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"SeOuerSq4x2q","mind":"default","section":"Patterns & Conventions","content":"v0.13.7 released: fix identity resolution Strategy 5 (path request + 10s poll on cache miss), IPC send handlers use run_in_executor, send timeout bumped 15s→30s.","status":"active","created_at":"2026-03-03T20:06:00.607Z","source":"extraction","content_hash":"159a2a33b05e9bbb","confidence":1,"last_reinforced":"2026-03-03T20:06:00.607Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-gZ5Y6xNcbvo","mind":"default","section":"Patterns & Conventions","content":"v0.13.8 released: feat(tui) changelog view in upgrade screen — shows WHAT'S NEW section with commit messages grouped by version, 3-strategy fetch chain (git log → GitHub API → PyPI versions), hides on upgrade start.","status":"active","created_at":"2026-03-03T20:39:15.906Z","source":"extraction","content_hash":"86eb999d4bb21a5e","confidence":1,"last_reinforced":"2026-03-03T20:39:15.906Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"4GYBL88Yxm_t","mind":"default","section":"Patterns & Conventions","content":"v0.13.11 released: feat: auto-generated NomadNet node page + Settings > System > PAGES panel (toggle, node name, Generate Node Page button via IPC CMD_PAGE_REGENERATE). Hardware detection for macOS/Linux SBCs/x86. Docs link uses https://docs.styrene.io, install recommendation uses pipx.","status":"active","created_at":"2026-03-03T21:01:37.454Z","source":"extraction","content_hash":"e50039923da53322","confidence":1,"last_reinforced":"2026-03-03T21:01:37.454Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"BZvDH3-F48_w","mind":"default","section":"Patterns & Conventions","content":"Changelog embedding pattern: run `python scripts/generate_changelog.py` before `just publish` to regenerate src/styrened/_changelog.py from git tags. The `just release` and `just publish` recipes should invoke this script so the bundled data stays current.","status":"active","created_at":"2026-03-03T21:05:40.681Z","source":"extraction","content_hash":"4c46bf88be46499f","confidence":1,"last_reinforced":"2026-03-03T21:09:19.377Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"W4gn_y4O-VtW","mind":"default","section":"Patterns & Conventions","content":"public-hub justfile has 'just push-edge' (main branch → latest+SHA tags) and 'just push-prod' (versioned production image) for ghcr.io/styrene-lab/public-hub releases.","status":"active","created_at":"2026-03-03T21:09:19.377Z","source":"extraction","content_hash":"4cb6bba54723e0f8","confidence":1,"last_reinforced":"2026-03-03T21:09:19.377Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zAuFbFE-9MfE","mind":"default","section":"Patterns & Conventions","content":"v0.13.14 released: feat: NomadNet destination in Styrene 8-field announce wire format — nomadnet_destination_hash stored in NodeStore, _resolve_nomadnet_destination() checks device field first, hub bridge mode reads NomadNet identity file and computes destination hash. Published to PyPI, public-hub bumped to 0.13.14.","status":"active","created_at":"2026-03-03T22:28:27.378Z","source":"extraction","content_hash":"5071844756705aa5","confidence":1,"last_reinforced":"2026-03-03T22:29:43.637Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vsta7WZF3jiq","mind":"default","section":"Patterns & Conventions","content":"v0.13.15 released: fix upgrade changelog by downloading target version's bundled entries from PyPI wheel — new Strategy 2 in fetch_changelog() chain fixes empty changelog for pipx installs.","status":"active","created_at":"2026-03-03T23:04:04.525Z","source":"extraction","content_hash":"d9b258f5c2b8bec4","confidence":1,"last_reinforced":"2026-03-03T23:04:04.525Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ws9iIBXSaf7F","mind":"default","section":"Patterns & Conventions","content":"v0.13.16 released: fix: detect NomadNet identity for announce even without page_server — probes NomadNet identity files directly (multiple paths including /app/.nomadnetwork/storage/identity) regardless of page_server.enabled; also moves caps_str after NomadNet probe so dynamically added capabilities are included.","status":"active","created_at":"2026-03-03T23:34:09.593Z","source":"extraction","content_hash":"c35121bd24ce4bd1","confidence":1,"last_reinforced":"2026-03-03T23:34:09.593Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"k48f-LXoq9Dj","mind":"default","section":"Patterns & Conventions","content":"Micron heading color scheme for render_to_rich(): h1 = '#5af0ce bold', h2 = '#3fbfa0 bold', h3 = '#6a9a8e bold italic' — defined as _HEADING_STYLES dict inside the heading branch of render_to_rich().","status":"active","created_at":"2026-03-03T23:45:53.981Z","source":"extraction","content_hash":"e9155b24305222a9","confidence":1,"last_reinforced":"2026-03-04T00:19:21.939Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XXSmqhUjPJB-","mind":"default","section":"Patterns & Conventions","content":"v0.13.17 released: fix(tui) micron renderer visual quality — h1 bright cyan (#5af0ce), h2 teal (#3fbfa0), h3 dim cyan italic, links with blue (#5ac8fa) underline and ▸ prefix, dividers with themed teal color, literal blocks with │ pipe prefix indent.","status":"active","created_at":"2026-03-04T00:19:21.938Z","source":"extraction","content_hash":"72f7dc90ceca4c0e","confidence":1,"last_reinforced":"2026-03-04T00:19:21.938Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"UvxnQFwPM-6C","mind":"default","section":"Patterns & Conventions","content":"v0.13.18 released: fix(tui) auto-restart TUI after successful upgrade — _finish_success() now calls self.dismiss(True) directly instead of showing a 'Restart TUI' button. Daemon is killed, then TUI restarts automatically.","status":"active","created_at":"2026-03-04T00:19:21.938Z","source":"extraction","content_hash":"d65c89565165bd4c","confidence":1,"last_reinforced":"2026-03-04T00:19:21.938Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"j-LhVuwj11aG","mind":"default","section":"Patterns & Conventions","content":"v0.13.19 released: fix: add diagnostic logging to page browser link establishment — logs path discovery, identity recall, link creation status, and callback events (established/closed with teardown_reason) for NomadNet page browsing diagnostics.","status":"active","created_at":"2026-03-04T00:39:06.215Z","source":"extraction","content_hash":"199c9e5ef4d08f1d","confidence":1,"last_reinforced":"2026-03-04T00:39:06.215Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"XBLMLdx69-eQ","mind":"default","section":"Patterns & Conventions","content":"v0.13.20 released: fix: debounce TCP reconnection storm + preserve active page browser links — _RECONNECT_DEBOUNCE suppresses rapid reconnect events, active links tracked to avoid teardown during page requests.","status":"active","created_at":"2026-03-04T00:50:09.950Z","source":"extraction","content_hash":"12aedc2335673e71","confidence":1,"last_reinforced":"2026-03-04T01:10:57.257Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"GwMpZi8IaV1i","mind":"default","section":"Patterns & Conventions","content":"_start_daemon() in upgrade.py uses shutil.which('styrened') to find the daemon binary and Popen with start_new_session=True + DEVNULL stdio to launch detached. May fail silently if PATH does not include pipx venv bin at the time of the TUI subprocess call.","status":"active","created_at":"2026-03-04T01:10:57.257Z","source":"extraction","content_hash":"b0aeaf93b2af9696","confidence":1,"last_reinforced":"2026-03-04T01:15:01.021Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"57Bk2d-w_4eM","mind":"default","section":"Patterns & Conventions","content":"Hub page content lives on nomadnet-data PVC at /app/.nomadnetwork/storage/pages/. Main index.mu and docs/ persist across pod restarts. Demo pages baked into image at /app/demo-pages/ and copied to pages dir on startup. To update pages on running hub: kubectl cp to pod.","status":"active","created_at":"2026-03-04T01:13:43.703Z","source":"manual","content_hash":"2c703755ccc5ea86","confidence":1,"last_reinforced":"2026-03-04T01:13:43.703Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"-6SUYpXsn-gP","mind":"default","section":"Patterns & Conventions","content":"v0.13.22 released: fix: TUI auto-starts daemon on mount before showing setup screen — _check_daemon() refactored to call _ping_daemon() then _auto_start_daemon() with 8s poll loop. DaemonSetupScreen only appears if auto-start fails.","status":"active","created_at":"2026-03-04T01:15:01.022Z","source":"extraction","content_hash":"459d6d84bdfbc8d7","confidence":1,"last_reinforced":"2026-03-04T01:15:01.022Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Awh6b8I6Sqgd","mind":"default","section":"Patterns & Conventions","content":"v0.13.23 released: feat: NomadNet page caching — write-through SQLite cache, offline fallback with ⚠ banner + age indicator, 'S' save site for periodic crawling, 'C' crawl site BFS, background refresh loop every 5 minutes. PageCacheService at services/page_cache.py, PageCache/SavedSite models in models/messages.py.","status":"active","created_at":"2026-03-04T01:52:16.028Z","source":"extraction","content_hash":"b544c8def9407147","confidence":1,"last_reinforced":"2026-03-04T02:20:04.125Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"v94xlqx2_MTk","mind":"default","section":"Patterns & Conventions","content":"Micron heading color scheme for render_to_rich(): h1='#5af0ce bold', h2='#3fbfa0 bold', h3='#6a9a8e bold italic' — defined as _HEADING_STYLES dict inside the heading branch of render_to_rich().","status":"active","created_at":"2026-03-04T02:20:04.125Z","source":"extraction","content_hash":"a4bfad220a4849c3","confidence":1,"last_reinforced":"2026-03-04T02:20:04.125Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"tlvlYEwaiDef","mind":"default","section":"Patterns & Conventions","content":"v0.13.24 released: fix: page cache assessment fixes — crawl IPC fire-and-forget (asyncio.create_task), _last_reconnect_time init in __init__, exception-path cache fallback in _load_page, deque for BFS queue, stale docstring fix, lint fixes (unused import, f-string, quoted annotation).","status":"active","created_at":"2026-03-04T02:31:08.596Z","source":"extraction","content_hash":"ed915484cc04491e","confidence":1,"last_reinforced":"2026-03-04T02:31:08.596Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"EylFQtkBddBj","mind":"default","section":"Patterns & Conventions","content":"DirectLinkService.establish() takes LXMF destination hash, recalls identity via RNS.Identity.recall(bytes.fromhex(lxmf_dest_hash)), then computes datalink dest hash as RNS.Destination(identity, OUT, SINGLE, 'styrene', 'datalink').hash — deterministic from identity, no announce needed.","status":"active","created_at":"2026-03-04T03:00:16.496Z","source":"extraction","content_hash":"6adffd32bce7f500","confidence":1,"last_reinforced":"2026-03-04T03:06:25.946Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"AxlJgB2Kap2W","mind":"default","section":"Patterns & Conventions","content":"v0.13.26 released: feat: datalink speedtest + LoRa-hardened timeouts — RTT-adaptive payload sizing (TCP: 256KB, fast LoRa 4KB, SF10+ 1KB), per-transfer auto-scaled timeouts, T keybinding in device detail, ASCII bar chart in DeviceStatusWidget, CMD_DATALINK_SPEEDTEST=0x54.","status":"active","created_at":"2026-03-04T03:18:12.183Z","source":"extraction","content_hash":"fbb097dd6a570826","confidence":1,"last_reinforced":"2026-03-04T03:22:27.996Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"lMEqDxTBPhJZ"}
{"_type":"fact","id":"niJLU2q-YcUD","mind":"default","section":"Patterns & Conventions","content":"Stale/lost row dimming pattern in DataTable: active=cascade.bright, stale=cascade.dim (full row dim — all columns), lost=cascade.dim. Status icons: ● active, ◐ stale, ○ lost. Status filter is pre-text-filter so counts reflect type-filtered pool.","status":"active","created_at":"2026-03-04T03:22:27.996Z","source":"extraction","content_hash":"28676f3d10c31da3","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"WLtxJ8r6lTVL","mind":"default","section":"Patterns & Conventions","content":"v0.13.27 released: feat: exploration screen status filtering — LOST hidden by default (h), STALE toggle (shift+H), status bar with per-status counts, dim styling for STALE rows, tab labels show visible/total when filtered. Both ReticumAnnounceTable and StyreneFleetTable support filters.","status":"active","created_at":"2026-03-04T03:26:58.143Z","source":"extraction","content_hash":"1bf20a71a43ff8a5","confidence":1,"last_reinforced":"2026-03-04T03:26:58.143Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Kcb7ggcK_VrH","mind":"default","section":"Patterns & Conventions","content":"_find_device_by_identity(identity) helper on ExplorationScreen looks up MeshDevice from active table's _all_devices list before push_screen — avoids SQLite round-trip in detail screen constructor.","status":"active","created_at":"2026-03-04T03:28:16.294Z","source":"extraction","content_hash":"abfd5be8884ca830","confidence":1,"last_reinforced":"2026-03-04T03:32:31.885Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"b5wEVjp9-Oj5","mind":"default","section":"Patterns & Conventions","content":"Status cache show-stale-then-refresh pattern: on cache hit, show cached data immediately (loading=False) but do NOT set got_status=True, so fresh fetch still proceeds via datalink or LXMF path and overwrites stale data.","status":"active","created_at":"2026-03-04T03:37:22.117Z","source":"extraction","content_hash":"c6d869da0f4b190f","confidence":1,"last_reinforced":"2026-03-04T03:37:22.117Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"QZQvhgp1x2Mz","mind":"default","section":"Patterns & Conventions","content":"asyncio.Lock for single-inflight operations: create lock in start() as self._speedtest_lock, check .locked() for fast-reject before async with, use async with self._speedtest_lock or asyncio.Lock() as fallback.","status":"active","created_at":"2026-03-04T03:37:22.117Z","source":"extraction","content_hash":"cbfb94f4c0b51660","confidence":1,"last_reinforced":"2026-03-04T03:37:22.117Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6WiLE-jwvqVn","mind":"default","section":"Patterns & Conventions","content":"When deduplicating config models, use re-export pattern in the service file: 'from styrened.models.config import XConfig  # noqa: E402' with a 'Re-export from models for backward compatibility' comment.","status":"active","created_at":"2026-03-04T04:14:54.594Z","source":"extraction","content_hash":"42f2c30356b4d395","confidence":1,"last_reinforced":"2026-03-04T04:14:54.594Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6p-E3ooIUqAy","mind":"default","section":"Patterns & Conventions","content":"IPv6 ULA mesh IP derivation: DEFAULT_SUBNET_PREFIX='fd73:7479:7265:6e65' (ASCII 'styrene' hex). Interface ID is first 8 bytes of SHA-256(identity_hash) formatted as 4×16-bit groups. Gateway peers get allowed-ips subnet_prefix::/64; direct peers get /128.","status":"active","created_at":"2026-03-04T04:22:31.592Z","source":"extraction","content_hash":"bdcda6b7740fad45","confidence":1,"last_reinforced":"2026-03-04T04:22:31.592Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"Ls8wOSP635qI","mind":"default","section":"Patterns & Conventions","content":"Mesh VPN handshake protocol: JSON over RNS.Link /vpn/handshake path, HANDSHAKE_VERSION=1. Request fields: version/wg_pubkey/mesh_ip/subnet_prefix/endpoint. Response fields: version/wg_pubkey/mesh_ip/subnet_prefix/endpoint/gateway. Handler returns {error: subnet_prefix_mismatch, our_prefix: ...} if prefixes differ. parse_handshake_request/response raise ValueError on missing fields.","status":"active","created_at":"2026-03-04T04:24:26.855Z","source":"extraction","content_hash":"ecb2d045e567a0c7","confidence":1,"last_reinforced":"2026-03-04T05:07:53.942Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":"s8UtlwaHiF0v"}
{"_type":"fact","id":"zekPJxrUloxc","mind":"default","section":"Patterns & Conventions","content":"mesh_vpn.py subnet prefix extraction pattern: use ':'.join(ip.split(':')[:4]) NOT ip.rsplit(':', 4)[0] — the rsplit approach breaks on compressed IPv6 notation.","status":"active","created_at":"2026-03-04T04:32:22.084Z","source":"extraction","content_hash":"aec095807e535b56","confidence":1,"last_reinforced":"2026-03-04T04:32:22.084Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"El-jmOmpI0dA","mind":"default","section":"Patterns & Conventions","content":"Use `rich.cells.cell_len()` (not `wcwidth.wcswidth()` or `len()`) for visible character width in Textual/Rich contexts — it matches the rendering engine's behavior for East Asian Width, emoji, and zero-width chars.","status":"active","created_at":"2026-03-04T04:33:47.114Z","source":"extraction","content_hash":"81ddd275c763425e","confidence":1,"last_reinforced":"2026-03-04T04:38:52.705Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"M0Co-RGTT77U","mind":"default","section":"Patterns & Conventions","content":"Bug regression test pattern: when fixing subtle logic bugs, replicate the exact logic inline in a helper function and test the helper directly — avoids Textual async app instantiation overhead while still catching regressions if source diverges.","status":"active","created_at":"2026-03-04T04:38:52.704Z","source":"extraction","content_hash":"04982573df5a7764","confidence":1,"last_reinforced":"2026-03-04T04:38:52.704Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"KbeDgXZyqlTo","mind":"default","section":"Patterns & Conventions","content":"v0.13.30 released: feat: mesh VPN service (services/mesh_vpn.py) + fix TUI announce spacing (' ×N' with space) + fix node_info_panel two-column alignment via rich.cells.cell_len + fix highlighted_panel on_resize width guard.","status":"active","created_at":"2026-03-04T04:38:52.704Z","source":"extraction","content_hash":"28f594d00f75547c","confidence":1,"last_reinforced":"2026-03-04T04:43:27.123Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"zpOlRUudVTjm","mind":"default","section":"Patterns & Conventions","content":"NodeInfoPanel test pattern: _render_panel(panel) helper captures Rich Table renderable via Console(file=StringIO, width=140).print() — required when render() returns RenderableType not str. See tests/tui/widgets/test_node_info_panel.py.","status":"active","created_at":"2026-03-04T04:48:20.047Z","source":"extraction","content_hash":"619c81762f664477","confidence":1,"last_reinforced":"2026-03-04T04:48:20.047Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"MnPSwKLPV8HI","mind":"default","section":"Patterns & Conventions","content":"NodeInfoPanel test pattern: _render_panel(panel) helper in test file captures Rich Table renderable via Console(file=buf, highlight=False, markup=False, width=140).print() and returns plain text — required when render() returns RenderableType not str. Added io, Console imports at top of test file.","status":"active","created_at":"2026-03-04T04:48:20.047Z","source":"extraction","content_hash":"1b1c89afcba8d852","confidence":1,"last_reinforced":"2026-03-04T04:48:20.047Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"0EzAVqN-sil5"}
{"_type":"fact","id":"TRehrIo5b0Dt","mind":"default","section":"Patterns & Conventions","content":"HighlightedPanel border rendering pattern: use call_after_refresh() in on_mount (avoids width=0 race), extend on_resize guard to catch 0→nonzero width transition for first valid layout.","status":"active","created_at":"2026-03-04T04:50:42.634Z","source":"extraction","content_hash":"ce5ea55a33dc8303","confidence":1,"last_reinforced":"2026-03-04T04:50:42.634Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"LFw8n1yD2ex_","mind":"default","section":"Patterns & Conventions","content":"v0.13.31 released: fix HighlightedPanel border rendering (call_after_refresh on mount, 0→nonzero width guard), fix NodeInfoPanel Rich Table layout (see v0.13.30), add 'c' key no-selection notification on dashboard.","status":"active","created_at":"2026-03-04T04:50:42.634Z","source":"extraction","content_hash":"762077c04a502fe5","confidence":1,"last_reinforced":"2026-03-04T04:50:42.634Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"8ht2CjrVroSP","mind":"default","section":"Patterns & Conventions","content":"VXLAN-over-WG per-peer bridge pattern: gateway creates 'vx-{hash[:10]}' VXLAN interface per peer (local=self.mesh_ip, remote=peer.mesh_ip, dstport=4789, dev=wg-styrene), brings it up, adds to bat0 via batctl, sets 100Mbit throughput_override. Teardown removes from bat0 then deletes interface. See src/styrened/services/mesh_vpn.py.","status":"active","created_at":"2026-03-04T05:16:01.972Z","source":"extraction","content_hash":"59a976763b209391","confidence":1,"last_reinforced":"2026-03-04T05:16:01.972Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"xxoEwhwppHg5","mind":"default","section":"Patterns & Conventions","content":"Screen deduplication helper _screen_in_stack(T): any(isinstance(s, T) for s in self.screen_stack) — use instead of isinstance(self.screen, T) to guard against double-push during Textual screen transitions.","status":"active","created_at":"2026-03-04T05:26:28.106Z","source":"extraction","content_hash":"fdff496ee4888bd9","confidence":1,"last_reinforced":"2026-03-04T05:26:28.106Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"IzU82CNObVvH","mind":"default","section":"Patterns & Conventions","content":"v0.13.32 released: fix screen stack deduplication (_screen_in_stack helper) + r-key triggers update check via _check_for_updates(). Pre-existing mesh_vpn/direct_link/daemon/test changes also included.","status":"active","created_at":"2026-03-04T05:26:28.106Z","source":"extraction","content_hash":"0434cd5b18f10225","confidence":1,"last_reinforced":"2026-03-04T05:26:28.106Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vof37ty0UVw8","mind":"default","section":"Specs","content":"Changes DO NOT EXIST unless they are part of an upstream tagged release. Always commit, push, tag, and push the tag before considering any change complete.","status":"active","created_at":"2026-03-02T20:25:00.000Z","source":"manual","content_hash":"a1b2c3d4e5f6","confidence":1,"last_reinforced":"2026-03-02T22:19:52.804Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"0SSOL-9_2jnm","mind":"default","section":"Specs","content":"After tagging a release, immediately run `just publish` to push to PyPI. Do not wait for the Argo pipeline. The user expects immediate feedback.","status":"active","created_at":"2026-03-02T20:30:00.000Z","source":"manual","content_hash":"f1e2d3c4b5a6","confidence":1,"last_reinforced":"2026-03-04T04:12:37.252Z","reinforcement_count":16,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"fmLb0PZG21eM","mind":"default","section":"Specs","content":"styrene (meta-package) and styrened versions are UNIFIED. They are not separate version tracks. When bumping styrened, the styrene-pypi version is automatically synced to match. Treat them as one version.","status":"active","created_at":"2026-03-02T20:35:00.000Z","source":"manual","content_hash":"a7b8c9d0e1f2","confidence":1,"last_reinforced":"2026-03-02T22:19:52.805Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"l1EL0koJJCe-","mind":"default","section":"Specs","content":"Auto-config NomadNet page feature planned: generate a Micron-formatted page showing node build version, platform specs, and hardware info, written to ~/.nomadnetwork/storage/pages/index.mu. Operator-accessible from TUI.","status":"active","created_at":"2026-03-03T20:52:21.736Z","source":"extraction","content_hash":"50886acfcabada8f","confidence":1,"last_reinforced":"2026-03-03T20:57:57.904Z","reinforcement_count":3,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"rKerpWTNnYf4","mind":"default","section":"Specs","content":"NomadNet page caching implementation in progress: PageCache/SavedSite SQLAlchemy models added to messages.py, PageCacheService written to services/page_cache.py with write-through caching, saved sites, BFS crawler, and background refresh loop.","status":"active","created_at":"2026-03-04T01:46:27.807Z","source":"extraction","content_hash":"1c2865692de4a0a3","confidence":1,"last_reinforced":"2026-03-04T01:52:16.027Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"UQNnXwjvkHd7","mind":"default","section":"Specs","content":"NomadNet page caching system implemented: SQLite page_cache and saved_sites tables in models/messages.py. PageCacheService at services/page_cache.py with cache_page(), get_cached_page(), save_site/remove_site/list_saved_sites(), crawl_site() BFS, background _crawl_loop(). Integrated into daemon._start_page_browser() via init_db() engine. IPC handler has write-through caching on success + cache fallback on failure/exception. New IPC commands: CMD_PAGE_SAVE_SITE(0x45), CMD_PAGE_REMOVE_SITE(0x46), CMD_PAGE_LIST_SITES(0x47), CMD_PAGE_CRAWL_SITE(0x48), CMD_PAGE_GET_CACHED(0x49). Message classes: CmdPageSaveSiteRequest, CmdPageRemoveSiteRequest, CmdPageListSitesRequest, CmdPageCrawlSiteRequest, CmdPageGetCachedRequest added to ipc/messages.py.","status":"active","created_at":"2026-03-04T01:48:00.318Z","source":"extraction","content_hash":"f7266edd500bbf1f","confidence":1,"last_reinforced":"2026-03-04T02:20:04.124Z","reinforcement_count":4,"decay_rate":0.05,"supersedes":"tnzfCO1T_SgI"}
{"_type":"fact","id":"0kIK_ZZbgL3p","mind":"default","section":"Specs","content":"Next ladder step toward unified L2 mesh: implement a TAP interface per remote peer, add it as a BATMAN-ADV hard-if, and tunnel raw Ethernet frames bidirectionally over an RNS.Link (DirectLinkService channel). Each remote Styrene node that establishes a datalink gets a TAP on both ends, extending bat0 across internet/LoRa transports. See styrene-edge/sbc/common/batman-mesh.nix for existing BATMAN config patterns.","status":"active","created_at":"2026-03-04T04:00:05.657Z","source":"extraction","content_hash":"cc9438d543c5804f","confidence":1,"last_reinforced":"2026-03-04T04:00:05.657Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"vK5nVtnnPeOO","mind":"default","section":"Specs","content":"L2 tunnel over RNS.Link design: each remote peer gets a TAP interface (tap-<peer_hash_prefix>), added to bat0 via batctl/netlink. Frames flow TAP↔RNS.Link. Requires CAP_NET_ADMIN. BATMAN-ADV sees TAP as neighbor, runs OGMs, builds routing. bat0 MTU must be clamped for LoRa links. RTT/speedtest data from DirectLinkService feeds throughput metric.","status":"active","created_at":"2026-03-04T04:02:43.624Z","source":"extraction","content_hash":"57154b49dbb80e46","confidence":1,"last_reinforced":"2026-03-04T04:02:43.624Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"ZMJ8EEqjHI09","mind":"default","section":"Specs","content":"Next implementation: RNS-bootstrapped WireGuard mesh VPN — use DirectLinkService (/vpn/handshake) to exchange WireGuard keys and endpoints over encrypted RNS.Link, then WireGuard handles IP traffic. Gateway nodes bridge WG interface into bat0. New service: services/mesh_vpn.py. Platform-aware: Linux kernel WG vs macOS userspace wireguard-go.","status":"active","created_at":"2026-03-04T04:09:41.628Z","source":"extraction","content_hash":"b887d7e21496cb20","confidence":1,"last_reinforced":"2026-03-04T04:12:37.252Z","reinforcement_count":2,"decay_rate":0.05,"supersedes":null}
{"_type":"fact","id":"6FlAdenNBiJW","mind":"default","section":"Specs","content":"Current version: v0.13.31 on main and PyPI. 2245 unit tests pass. Hub still running v0.13.20.","status":"active","created_at":"2026-03-04T05:13:16.305Z","source":"extraction","content_hash":"17fe4bfc67a27729","confidence":1,"last_reinforced":"2026-03-04T05:13:16.305Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"8LehLRfp83MD"}
{"_type":"fact","id":"jGx_dPLF7ekV","mind":"default","section":"Specs","content":"Current version: v0.13.32 on main and PyPI. 2243 unit tests pass. Hub still running v0.13.20.","status":"active","created_at":"2026-03-04T05:26:28.104Z","source":"extraction","content_hash":"65d4140a7519d105","confidence":1,"last_reinforced":"2026-03-04T05:26:28.104Z","reinforcement_count":1,"decay_rate":0.05,"supersedes":"pNOI3f7WhawX"}
{"_type":"episode","id":"-RwkAXiD9Y1C","mind":"default","title":"Fixed critical IPC enum collision, audit issues, added 28 tests","narrative":"The goal was to resolve a critical IPC command value collision where datalink commands (0x60-range intended) overlapped with terminal commands (0x50-range), causing Python's IntEnum to silently alias duplicates and misroute messages. The collision was fixed by relocating datalink commands to 0x60-0x64, and a regression test was added asserting all IPCMessageType values are unique. Alongside the critical fix, a full audit sweep addressed eight additional issues: DirectLinkService.stop() added to daemon shutdown, bridge variable initialized before try/except to prevent UnboundLocalError, _resolve() race condition patched with try/except, status cache bounded to 64 entries with LRU eviction, speedtest guarded with asyncio.Lock, bar chart scaling made relative, and dead variables removed. Twenty-eight new unit tests were written covering RTT-adaptive payload selection, cache TTL and eviction, service lifecycle, exploration table state, and the IPC collision regression. The release was tagged v0.13.29 and published to PyPI; a Dependabot high-severity alert on the repo remains open.","date":"2026-03-04","session_id":null,"created_at":"2026-03-04T03:42:41.402Z","fact_ids":["1LWHCrJiqGY0","57Bk2d-w_4eM","b-56pPnuzUqf","d9JM2CIWX2lo","dE_W-1wRQYQM","fs1LXKatIzRd","nA052JS-GyfU","nGrtwQxmgf1n"]}
{"_type":"episode","id":"3LIPghrR2hgJ","mind":"default","title":"TUI screen stack dedup and r-key update check fix","narrative":"The goal was to fix two TUI bugs: screen overlays stacking duplicates (double-ESC behavior) and the 'r' refresh key never triggering the version update check. The `action_open_*` guards used `isinstance(self.screen, T)` which only checks the top of the stack, so a new `_screen_in_stack(T)` helper was introduced to scan the full stack. `action_refresh()` was updated to also call `_check_for_updates()`, which is safe due to its `@work(exclusive=True)` decorator. Pre-existing uncommitted changes from a prior session (WireGuard handshake improvements, direct_link teardown hardening, daemon lifecycle, and 20 new mesh VPN tests) were batched into the same commit. All changes were released as v0.13.32 to both PyPI and GHCR.","date":"2026-03-04","session_id":null,"created_at":"2026-03-04T05:26:35.730Z","fact_ids":[]}
{"_type":"episode","id":"klZDBpNEcVhp","mind":"default","title":"TUI Chat Widget and Dashboard Deep Audit","narrative":"The session was a detailed code review of a styrened TUI patch targeting chat widget improvements, clipboard handling, dashboard data display, and mesh hop tracking. The reviewer systematically traced data flows through RNS transport internals, IPC handlers, Textual widget lifecycle, and CSS layout to identify bugs. The audit found six critical issues including an incomplete NSPasteboard threading fix still present in paste paths, non-existent dict keys causing permanently blank TRAFFIC stats, a missing NodeStore method silently zeroing contact counts, incremental refresh not handling message deletion, and multiple concurrent dashboard worker races. Twelve additional warnings and omissions were documented, including no persistence for the new hops field and no tests for any new UI behaviors. The verdict was NEEDS REWORK.","date":"2026-03-03","session_id":null,"created_at":"2026-03-03T01:53:25.726Z","fact_ids":["qP4sunc6x81P"]}
