fix(kanban): coordinate notifier ownership per profile - #72277
fix(kanban): coordinate notifier ownership per profile#72277DeliciousHouse wants to merge 106 commits into
Conversation
…taller The macOS launcher fast path gates on hermes_is_installed(), which needs .hermes-bootstrap-complete next to a built desktop app. Nothing in the Rust bootstrap pipeline ever wrote that marker -- only install.ps1 did -- so every reopen of /Applications/Hermes.app re-ran setup instead of launching. Publish the marker atomically (temp sibling + fsync + rename) because hermes_is_installed() only checks existence: a torn direct write would arm the fast path against a half-installed tree. A marker write failure emits BootstrapEvent::Failed so the installer UI leaves the progress state. Co-authored-by: giggling-ginger <giggling-ginger@users.noreply.github.com>
install.ps1 wrote the marker on Windows and the Rust installer now writes it, but install.sh -- the path every Mac and Linux CLI install takes -- never did. A machine set up with install.sh therefore looked uninstalled to the desktop app, which re-ran first-run bootstrap on every launch. Stamp the same schema-v1 payload install.ps1 writes, from both the staged `complete` stage and monolithic main(). An unresolvable HEAD skips the marker rather than writing one the desktop validator rejects: absent reads as a clean "bootstrap needed", malformed reads as a confusing half-state.
Marker presence was the launch gate, but the marker is provenance about who ran the install -- not proof the runtime works. A CLI-installed repo+venv, or a healthy install whose marker a repair deleted, both read as "never installed" and dropped the user into first-run bootstrap on every launch. Split the two questions: classifyActiveRuntime() reports marker validity and runtime usability separately, and the resolver launches whenever the runtime is usable, logging when it proceeds without a marker. An unusable runtime still falls through to bootstrap even with a valid marker, so an interrupted install can't spawn a dead backend. Drops isBootstrapComplete(), which had no callers left once the gate moved. Co-authored-by: iveywest <iveywest@users.noreply.github.com> Co-authored-by: lihengming <lihengming@users.noreply.github.com>
Repair signalled "reinstall me" by deleting the bootstrap marker. That was already destructive -- repair is reachable from a transient backend error on a fully working install -- and it stranded users in first-run setup with no way back short of hand-writing the marker file. Carry the intent in an explicit flag instead. Repair forces the next resolve through the installer and clears itself once the reinstall starts, so a forced reinstall still works without destroying provenance about how the install was created. Closes NousResearch#72166
Adds an `idle-cost` scenario for the symptom Brooklyn reported: with a thread spinning, resizing the sidebar feels slow. It holds N tiles busy, pushes NO tokens, and measures the renderer's self-inflicted commit rate plus fps while dragging the splitter and while typing. It reproduces immediately. Five busy tiles, nothing streaming: idle commits 17.7/sec (should be 0 — nothing is happening) drag 1.4 fps p95 812ms, worst frame 1.9s typing 61 fps (fine — this is specific to resize) Attributing the drag window showed 105,385 TooltipProvider renders and ~15s of component time across a 60-frame gesture. Cause: `Tip` mounts a full Radix provider + Tooltip per call site, and there are ~107 of them. Radix's Tooltip holds real state and Popper subscribes to layout, so an unrelated interaction re-rendered all of them. Mounts the machinery lazily instead, on first hover/focus. Tooltip churn drops ~4x (105k -> 26k) and drag doubles to 3fps. Note `defaultOpen` on the armed Tooltip is load-bearing: the pointerenter that armed it has already fired, so Radix never sees it and the tip mounts silently closed. A test caught exactly that, and now guards it. 3fps is still bad — the remaining cost is the whole transcript re-rendering per resize frame (MessagePrimitive.Parts 12,600 renders / 10.5s, Block/Ct 24,300 each, all 100% wasted). Separate fix.
The synthetic gesture oscillated +/-3px, which nets to zero displacement and can clamp to a no-op — so it reported a confident fps number for a drag that never moved the sash. Sweeps monotonically now, dispatches pointer events React's synthetic system accepts (isPrimary/button/buttons), and records dragTarget + dragMoved so a drag that silently did nothing is visible in the output rather than passing as a measurement. Verified: dragMoved now reports 60px where it previously reported 0.
TreeGroup called useStore($layoutTree) to build its right-click menu's move/split directions. That subscribes every zone — and therefore every mounted pane and its entire transcript — to the whole layout tree. A sash drag rewrites the tree once per frame, so dragging the sidebar re-rendered all five tiles' message lists on every pointermove, for a context menu nobody had open. The directions are only read when the menu renders, so read the tree there with .get() instead. Same lazy shape the neighbouring `closable` prop already uses. Measured over one 60px sash drag with five busy tiles: commits 83 -> 12 ChatView 150 -> 10 (4465ms -> 353ms) AuiProvider 9450 -> 630 (9868ms -> 774ms) TreeGroup 180 -> 12 TreeSplit 90 -> 6 Also fixes an observer effect in the harness: idle-cost recorded render attribution *during* the timed gesture, and the counter walks the fiber tree on every commit. That was large enough to hide this 15x reduction behind an unchanged fps, so timing and attribution are separate passes now and `record` defaults off. Adds scripts/diag-drag-churn.mjs — the probe that found this. It reports the transcript chain (who above the messages re-rendered) plus every atom that notified, which is what named TreeGroup instead of leaving it to be guessed at. Notably the atom list came back EMPTY: this was never store churn, so the render-attribution path was the only thing that could have found it.
parseMarkdownIntoBlocksCached bypassed its cache for text under 1024 chars, on the theory that re-lexing a short message is cheap. The lex is cheap; what it returns is not free. `parseMarkdownIntoBlocks` builds a fresh array every call (verified in streamdown's dist: `let r=[]` ... `return r`), and Streamdown mirrors the block list into useState — so a new array identity for UNCHANGED text re-renders Streamdown and every Block beneath it. Most messages are short, so most of the transcript was on the uncached path. Caching every length cuts the idle cost of five mounted tiles: Streamdown 5.2ms -> 2.6ms Block 128ms -> 85ms Ct 122ms -> 81ms Cache bumped 64 -> 256 entries to cover the now-larger key space. This does NOT reduce Streamdown's 105 idle self-renders — array identity turned out not to be what drives those, and I verified the cache returns a stable identity, so that root is still open. This is a cost win, not the churn fix.
Decouple notification polling from dispatch ownership, route subscriptions through their stamped profile adapters, and coordinate duplicate gateways with profile-scoped locks. Preserve item-safe retries, API-server wake ordering, and complete human review briefs.
Related: #72241 and #56802. This patch adds per-profile advisory ownership locks and notifier independence from dispatch ownership; #72241 salvages profile routing. The overlapping notifier policies need maintainer consolidation rather than a duplicate verdict. |
…marker-triage fix: make the bootstrap-complete marker consistent across every install path
…pace `plainTextInRange` serialized the caret's preceding content through a bare <div>, but `composerPlainText` appends "\n" to any block element that isn't the editor slot. So `beforeText` always looked like it ended in whitespace and the separating space was never inserted — dragging a file in after a word produced `review@file:...` glued together. Marking the scratch container with RICH_INPUT_SLOT makes it serialize in the same coordinates as the editor. Same fix lands in the new `caretOffsetInEditor`, which measures caret offsets the same way.
Reverts the tooltip half of 4798994; keeps the idle-cost scenario. Lazily mounting Radix on first hover measured well (105k -> 26k TooltipProvider renders per drag) but broke 18 tests across 12 files. Those tests are not incidental: the repo has an established convention of asserting `[data-slot="tooltip-trigger"]` at mount to prove a control carries a tooltip, and deferring the mount invalidates all of them at once. There is also a real behavior risk the convention was protecting — `asChild` puts the slot on the button element itself, so arming REPLACES the node, which is exactly the kind of identity change that breaks focus restoration and ref-holding call sites. A 4x cut in tooltip churn is not worth reworking every tooltip assertion in the app plus taking that risk, on a component with ~107 call sites. If it's worth revisiting, the right shape is probably making TooltipProvider itself cheap (one app-level provider) rather than deferring the mount per call site — that preserves the DOM contract these tests encode. The genuine win in this branch stands on its own: the $layoutTree subscription fix (commits 83 -> 12 on a sash drag) is unaffected.
with NousResearch#60769 Both the wake chat-scope salvage (NousResearch#72191, merged) and the DM-topic metadata salvage added HERMES_SESSION_CHAT_TYPE plumbing; the rebase auto-merge kept both copies. Dedupe the ContextVar declaration, _VAR_MAP entry, set_session_vars parameter/token, and the run.py call-site kwarg, and prefer the persisted chat_type column with delivery_metadata as the legacy fallback in the notifier wake path.
The kanban notifier _collect() loop iterates subscriptions without per-subscription error handling. When claim_unseen_events_for_sub raises for one subscription (e.g. DB corruption, lock contention), the entire tick aborts — silently blocking delivery for ALL other subscriptions. Wrap the per-subscription logic in try/except so one bad subscription logs a warning and continues to the next, instead of jamming the entire notifier. Closes NousResearch#59269
- honor SendResult(success=False) instead of discarding it, so an adapter that REPORTS (not raises) a soft send failure — e.g. the Telegram adapter's "Not connected" mid-reconnect — no longer advances the cursor past an undelivered event and silently loses the notification. Addresses the notifier half of NousResearch#31901. - add block_loop_detected to the notifier's TERMINAL_KINDS so a task routed to triage for a human decision (re-blocked past the recurrence limit) actually pings its subscribers instead of stalling silently. - raise MAX_SEND_FAILURES 3 -> 12 (~60s at the 5s tick) so a transient Telegram/API outage does not permanently unsubscribe a live channel now that reported soft-failures also reach this counter. - route active-profile-stamped subscriptions via the primary adapter on a single-profile gateway (self.adapters[platform] when the stamped notifier_profile equals the active profile). Related to NousResearch#56802. Adds test_kanban_notifier_rewinds_claim_on_reported_send_failure asserting a reported send failure leaves the event unseen (rewound) rather than consumed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Salvaged from PR NousResearch#63001 (reduced scope): probe each board with the new read-only kanban_db.count_notify_subs() before the writable connect(), so boards with zero subscriptions are never opened writable on the 5s notifier tick (no schema migration, no WAL/-shm sidecar churn, no checkpoints). The PR's machine-global .notifier.lock singleton gate was deliberately NOT salvaged: a lock-winning default-profile gateway cannot deliver a secondary profile's subscriptions in standalone-profile deployments (profile routing fails closed in _authorization_adapter), so the lock could suppress delivery entirely. The probe captures the per-tick cost win without that regression.
…cted e2e coverage Follow-ups from review of salvaged PRs NousResearch#59278 and NousResearch#62712: * test_kanban_notifier_isolates_per_subscription_failure previously created the good subscription first; list_notify_subs() has no ORDER BY, so the good delivery happened before the bad claim raised and the test passed even without the isolation fix. The bad task is now created first AND a deterministic-order shim forces the failing subscription to be iterated first, so the test fails on the old whole-tick-abort behavior. * New test_notifier_delivers_block_loop_detected_triage_ping: drives a block_loop_detected event through one notifier tick end-to-end, asserting the triage ping reaches the adapter and the cursor advances (the sweeper review of NousResearch#62712 flagged that only DB-level emission was tested).
Parity with Claude Desktop quick-entry-window / ChatGPT Quick Chat.
…o allowlist proposals
…hidden-console design Two halves close the 'legacy pythonw gateways survive updates forever' gap: 1. hermes update now regenerates the installed Scheduled Task / Startup launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume phase. They are persistence artifacts written once at install time; updates never touched them, so pre-aa2ae36c3f installs kept launching the gateway through pythonw.exe forever — every descendant spawn flashed a conhost (NousResearch#54220/NousResearch#56747) and, since NousResearch#70344, the console-less gateway died at startup with RuntimeError: sys.stderr is None (NousResearch#71671). The task /TR points at a stable script path, so rewriting the files retargets it with no schtasks call and no UAC. No-op for modern installs; best-effort so a failed refresh never fails the update. 2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter to its sibling console python.exe when it exists, so the update pause/resume argv-replay path (and any other caller handed a legacy command line) respawns on the current design instead of faithfully resurrecting the old one. Keeps pythonw when no sibling exists — a failed respawn is worse than a console-less gateway.
The status bar shipped every affordance it had, so approvals, the terminal toggle, agents, cron and webhooks sat there permanently for users who never touched them. Those five now start hidden and the bar owns a context menu that turns them back on, persisted per install. Items opt in by naming themselves with `toggleLabel`, so a plugin contribution that doesn't opt in always shows; the system icon and the version/update pills are listed but locked on, since hiding the way back into settings strands the user. Preferences store the hidden set rather than the visible one, so an item added to the bar in a later version appears for existing users instead of staying silently off.
A CDP trace of one sash drag settled what the render counters could not. I had assumed the remaining cost was layout/paint; it was not: script 6770ms | style 1866ms | layout 71ms Layout was never the problem. The top attributable callsite in our own code was use-resize-observer.ts at 977ms. Counting the callbacks named the mechanism exactly: 8,620 ResizeObserver instances constructed, and during a 40-move drag 2,600 callbacks each carrying exactly ONE entry — 65 separate callbacks per pointermove. Every consumer owned a private observer, so N elements resizing under a common ancestor meant N trips through the observer machinery instead of one batched delivery. With five mounted tiles that is ~100 user bubbles, each with its own observer, all woken by a width change. One shared observer with a WeakMap of target -> handlers. Callers keep their exact contract: a handler observing several elements is still invoked once with all of its entries, and unobserve happens when the last handler for an element goes away. Verified by trace, before -> after: use-resize-observer 977ms -> 42.5ms (-96%) style recalc 1866ms -> 1145ms (-39%) total script 6770ms -> 3929ms (-42%) Callback count 2,600 -> 43: one delivery per frame instead of 65. Adds the two probes that found it. diag-drag-trace.mjs takes a real timeline trace and prints the style/layout/script split plus the top script callsites — that split is what disproved the layout theory. diag-ro-storm.mjs counts RO callbacks vs entries, which is what distinguished 'a few expensive calls' from 'very many cheap ones'.
Every `Tip` carried its own `TooltipProvider`, and there are ~107 call sites. Each is a subtree that re-renders when anything above it does, so they dominated unrelated interactions: 52,784 TooltipProvider renders and 18.3s of component time in a single sash drag. Radix's provider holds only refs and stable callbacks (no reactive state) — hoisting one to the app root is what it is designed for. `Tooltip` still reads delayDuration/disableHoverableContent from context, and the per-Tip overrides are preserved. `Tip` keeps a local provider as a FALLBACK, chosen by context: a component rendered in isolation has no root provider and Radix throws "`Tooltip` must be used within `TooltipProvider`". Without this, 20 unit tests that render a single control fail. Inside the app the flag is always true, so the common path is a bare Tooltip. This is the shape the earlier lazy-mount attempt should have taken. That one deferred the Radix subtree until hover, which moved data-slot="tooltip-trigger" off the mounted DOM and broke 18 tests encoding that contract. Hoisting keeps the contract intact — every one of those tests passes unchanged. Measured on the same drag: TooltipProvider 52,784 renders / 18.3s -> gone from the table Primitive.div 40.5s -> 13.4s Popper 10.5s -> 2.7s Tooltip 15.7s -> 4.4s
withFrames ran its own requestAnimationFrame ticker while the gesture body independently awaited rAF per step. Two rAF consumers, so the observer's deltas counted the driver's frames as well as the app's — it reported ~3fps for a drag that a single-clock probe measures at ~23fps, and it never moved no matter what got fixed underneath. Timing now comes from the same callbacks the body drives (__MARK__). This also fixes a silent false-negative on the typing pass: it paced on setTimeout, so the independent ticker was mostly sampling idle waits between keystrokes and reported a flat 61fps. On the driving clock the same interaction reports ~30fps with 27 of 40 frames over 33ms — which matches the 'typing feels slow' symptom I previously could not reproduce. TYPE now records __TYPE_TARGET__ and the runner throws when no composer is found, so a pass that measures nothing fails loudly instead of scoring a perfect 0 deficit — same guard DRAG already had.
…rt-only
Captures medians of 5 runs for multitab and render-churn so tonight's
wins can't silently regress.
idle-cost is deliberately NOT gated. Its render attribution and idle
commit rate are trustworthy and are what the scenario exists for, but the
drag fps it reports (~0.6fps, p95 814ms) contradicts a direct
single-clock probe of the same gesture on the same build (57fps). I ruled
out sash selection, tile setup, render-counter residue, and a 20s soak,
and could not explain the gap — so the metric ships as a report, not a
gate. Gating CI on a number I can't defend would either fire on a phantom
or mask a real stall.
tier: 'report' is outside GATED ('ci','cold'), so the scenario still runs
and prints but neither compares nor writes a baseline.
After N consecutive guardian denials in a session the deny message escalates to a hard-stop instruction. Inspired by ChatGPT Work auto-review circuit breaker.
…goes through OpenRouter The Nous Portal docs claimed routing 'happens through OpenRouter under the hood' with OpenRouter-equivalent failover, and that the catalog 'mirrors OpenRouter's model list'. That is not the Portal's contract: some models route through OpenRouter, others through proprietary or secondary providers, and per-model routing can change over time. The stale wording licensed users to expect OpenRouter-proprietary request extensions (top-level cache_control, session_id sticky routing, provider preferences) to work through the Portal, producing misfiled bug reports like NousResearch#71576. Reworded both pages (en + zh-Hans) and added an explicit note that OpenRouter-specific extensions are not part of the Portal API contract.
A dedicated /context (alias /ctx) gateway slash command that gives a full context-window view with: - Usage gauge: visual bar + fraction + percentage + headroom - Auto-compression threshold and how far away it is - Compression count and how much the last one freed - Cumulative session throughput (explicitly labelled as throughput, NOT context size — each call re-sends the window) - Cascading fallback: running agent → cached agent → SessionStore metadata → rough transcript estimate Not included (per current-main design): - Cache reporting removed: commit 446b8e2 intentionally removed cache reporting from user-facing surfaces because providers that omit cached-token details produce misleading values - Sync DB calls replaced with async_session_store (current main requires AsyncSessionStore with await) Also rewords the /status tokens line from 'Cumulative API tokens (re-sent each call)' to 'Lifetime tokens billed: ... (not your current context size; use /context)' to reduce the recurring confusion that the cumulative figure is the current context window. Fixes salvation of PR NousResearch#52184 (salvage commit replaces a 12K-commit-behind fork branch with a fresh implementation against current main, incorporating reviewer feedback from @whoislikemiha and the hermes-sweeper).
`hermes prompt-size` reported skills as one <available_skills> block total and tools as one json-bytes total, so there was no way to see which installed skill or toolset actually dominates the fixed prompt budget. Add two additive breakdowns to compute_prompt_breakdown (hermes_cli/ prompt_size.py): - toolsets_breakdown: each resolved tool is attributed to its single canonical registry toolset (registry.get_tool_to_toolset_map), summed by group. Fully attributable — the grand total equals the existing tools.json_bytes minus JSON array framing (2*count bytes). - skills_breakdown: parsed from the rendered <available_skills> block, one entry per skill with two honest, distinct numbers — index_line_bytes (the always-on cost of listing the skill) and skill_md_bytes (on-disk SKILL.md size, the real read cost paid only on skill_view). Sorted largest-first by read cost. render_breakdown prints both as sorted "Toolsets by size" / "Skills by size" tables (skills capped at 20; --json carries them all). All existing keys and output are unchanged. Runs fully offline (dummy credentials, no network). Tests cover shapes, largest-first ordering, per-tool attribution reconciling to the total, namespaced-name parsing, and unmapped-skill handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the cherry-picked /context command (PR NousResearch#52184) and prompt-size attribution helpers (PR NousResearch#66656) into one visual context view across surfaces, and absorbs the per-component budget-visibility goal of the /tokens proposal (PR NousResearch#48470): - agent/context_breakdown.py: pure renderers over the existing payload — a 5x20 glyph block grid (1 cell ~= 1% of the model window), an 'Estimated usage by category' table with free space, and expanded per-skill / per-toolset listings via compute_context_details(), which reuses the prompt-size attribution mechanism (skills index-line bytes + registry tool->toolset map) converted to the same chars/4 heuristic. - cli.py: /context [all] renders grid + category table (+ expanded listings) from the live agent and in-memory conversation history. - gateway/slash_commands.py: /context appends the plain-text category table (no grid — monospace not guaranteed on messaging platforms); /context all adds the expanded listings. Fail-open: breakdown errors never break the gauge. - hermes_cli/commands.py: /context gains the 'all' subcommand; /version demoted to /hermes version on Slack to keep the 50-slash cap. - tests: renderer unit tests against synthetic payloads, registry test, gateway /context + /context all + failure-degradation handler tests. - docs: slash-commands reference + CLI guide entries. Read-only and locally computed: no provider calls, no prompt-cache impact. Co-authored-by: RemyFevry <29257684+RemyFevry@users.noreply.github.com> Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com> Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
…ser-row Preserve the original prompt when a mid-turn redirect corrects a turn
…prefs Quieter status bar and sidebar counts
… status indicator Display-only port of Claude Code /focus; composes with existing /verbose tool-progress modes.
…rf-finish perf(desktop): drag at 60fps with five streaming tabs
Shows staged and unstaged changes in the current working directory. /diff shows stat summary + full diff, /diff --stat shows summary only. Uses git diff directly — no checkpoint system required. Works in any git repository. Closes NousResearch#4250
Widen the cherry-picked /diff base (NousResearch#4839 by @SHL0MS) into one cross-surface implementation, folding in the review feedback and the best ideas from the two sibling PRs (NousResearch#22703, NousResearch#53527): - tools/working_diff.py: shared git collection layer — unstaged (default), staged, and all (vs HEAD) modes; untracked files folded in via `git diff --no-index` so new files appear as additions (Codex /diff parity); shlex-split arguments preserve quoted paths. - CLI: handler moved to hermes_cli/cli_commands_mixin.py per the current god-file decomposition (dispatch stays in cli.py), renders through the rich console with a 400-line terminal-flood guard. - Gateway: _handle_diff_command in gateway/slash_commands.py + dispatch in gateway/run.py; fenced ```diff output truncated to 60 lines / 3000 chars before the platform senders apply their own per-platform message clamps (tool-progress-style layered truncation). Localized strings in all 17 locale catalogs. - /diff session (from NousResearch#53527): cumulative checkpoint-baseline diff of everything Hermes changed, via new CheckpointManager.session_diff(); docstring records the retained-baseline approximation caveat from review. Works on both surfaces; degrades with an actionable message when checkpoints are off. - Slack: /diff routed via /hermes diff (50-slash cap; keeps telegram-parity test green and /version native). - Registry: cross-surface CommandDef with staged|all|session subcommands; docs: slash-commands reference (CLI + gateway tables + both-surfaces list) and hermes-agent skill reference. - Tests: tests/tools/test_working_diff.py (real git repos), tests/hermes_cli/test_diff_command.py (real git + stubbed checkpoint manager), tests/gateway/test_diff_command.py (end-to-end handler, real checkpoint store), TestSessionDiff in tests/tools/test_checkpoint_manager.py. Salvaged from the /diff PR cluster NousResearch#4839 + NousResearch#22703 + NousResearch#53527. Co-authored-by: Ninso112 <ninso112@proton.me> Co-authored-by: Harshkamdar67 <harshkamdar67@gmail.com>
Decouple notification polling from dispatch ownership, route subscriptions through their stamped profile adapters, and coordinate duplicate gateways with profile-scoped locks. Preserve item-safe retries, API-server wake ordering, and complete human review briefs.
|
Correction verification for exact head
No Hermes install/runtime, gateway/config, dispatcher, Telegram, live application board, Jira, production, deploy, ready-for-review, or merge mutation was performed. |
|
Blocking re-review of corrected head
Verification on this exact head: This PR remains draft and was not marked ready or merged. The one engineering correction cycle has already been used, so I am routing the remaining decision to dev-lead rather than opening a second correction bounce. |
|
Closing after the single correction cycle remained below the merge contract. No risk waiver or same-branch exception is authorized; a clean-upstream replacement will use an explicit at-least-once delivery contract that prefers no lost human-floor notifications while bounding duplicate risk. |
Summary
SendResult(success=False), and preserve complete blocked/scheduled human-review briefs within platform limitsTest plan
uv lock --checkpy_compileon every changed Python filegit diff --checkBroader-suite environment notes
devenvironment reached 6.7% before collection errors from the optionalagent-client-protocolextra, unrelated to this Python-only notifier diffnpm run checkwas attempted; unchanged Desktop/TUI workspaces fail on this Windows host becausebippyis absent and POSIX path/editor assumptions fail, while the web and tests-js workspaces passedSafety and compatibility
defaultdispatch_in_gateway: false