feat(photon): rebaseline fork on v2026.8.3 - #15
Merged
Merged
Conversation
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…-egilewski-myk0la chore: contributor email mappings for egilewski and myk0la-b
…-ckaznocha chore: AUTHOR_MAP for ckaznocha
…search#76870) Deferred model switches append a marker and bump history_version at turn start; the dispatcher was snapshotting history before that mutation, so the version-mismatch guard rejected the turn's own result as a stale/concurrent write. Move the snapshot to after _apply_pending_model_switch/_sync_agent_model_with_config, under history_lock, so the turn's own preparatory mutation is included in its baseline while the anti-stale guard still catches real external writes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
teknium1 flagged ISSUE_76870_RELATORIO_CAUSA_RAIZ.md as containing stale metadata (references an unrelated local branch) and asked to drop the standalone report, keeping only the focused server.py fix and regression test.
…#33208) strip_think_blocks passed the same response-scrubbing strings through re's pattern dispatcher on every response. Skills Guard repeated the same work for 121 patterns against every scanned line. Compile the existing expressions once and reuse Pattern.sub/search. Keep each generic tool-call tag in its own paired expression so mismatched openers retain their payload while existing stray-closer cleanup remains unchanged. Part of NousResearch#33208 Salvaged from NousResearch#32713 by @ErnestHysa. Co-authored-by: ErnestHysa <takis312@hotmail.com>
Simplify-pass follow-up on the NousResearch#69653 salvage: the original code built these patterns from a name loop; hand-expanding them into 10 literals lost that single source. Tag-name tuples restore it (adding a 6th reasoning tag is now a one-place change), and the gnarly named-function pattern regained a pointer to its step-1c rationale. Byte-equivalence of every rebuilt pattern verified programmatically (alternation-order neutrality probed: the \b and > anchors make order irrelevant).
at828@proton.me -> ATran28 (GH id=1445620) Needed for PR NousResearch#77270 whatsapp bridge reconnect fix.
…-map-atran28 chore: add ATran28 to AUTHOR_MAP
…search#77184) request_restart was calling stop() immediately, so the requesting turn stayed in the drain wait set and got force-killed at restart_drain_timeout. Wait for active work to reach zero first, then stop against an idle gateway.
… compute-host shutdown ComputeHost.shutdown() called flush_all_sessions() before its own in-flight turn drain loop. server._finalize_session latches on session["_finalized"] and every later call returns immediately, so that one flush was spent while turns were still producing output: the unflushed tail was never persisted, commit_memory_session wrote long-term memory from a truncated transcript, the session's DB row was marked ended while it was live, on_session_end fired with completed=False/interrupted=True against a running session, and the active-session lease was released out from under a turn. The drain loop exists precisely so that mid-turn work survives a teardown; finalizing first defeated it. Reachable from all three teardown paths: the parent/orphan guard (which os._exit(0)s immediately after), the SIGTERM/SIGINT handler, and stdin close. Drain first, then flush. A slice of the caller's budget (_FLUSH_RESERVE_SECS, never more than half of it so a short explicit wait still gets a real drain) is withheld from the drain so the flush still runs when turns outlast the window: HostSupervisor SIGKILLs the host _SHUTDOWN_TIMEOUT_SECS after SIGTERM — 10.0s, the same value as shutdown()'s default wait — so a drain allowed to consume the whole budget would leave the durability write racing that kill. `wait` itself is unchanged, so total shutdown latency and the SIGTERM->SIGKILL margin are unchanged.
The drain loop slept a flat 0.05s per tick, so it could overshoot its deadline by up to one tick and spend part of the reserve withheld for flush_all_sessions(). For a small `wait` the reserve is itself half the budget, so a single overshoot can consume all of it: at wait=0.34 the drain budget is 0.17s but the loop requested 4 x 0.05 = 0.20s of sleep. Clamp each tick to the remaining time. The new test asserts on the summed *requested* sleep rather than wall-clock, which is deterministic: every sleep is bounded by the strictly-decreasing remainder, so the total can never exceed the drain budget regardless of how the scheduler interleaves.
…n deadline expires The drain reserves a slice of the shutdown budget so flush_all_sessions still runs when in-flight turns outlast the window. But that flush was unconditional: a session whose turn was still running got its one-shot _finalize_session spent mid-turn, and the executor.shutdown(wait=False, cancel_futures=True) immediately after does not join the turn. The session was then permanently un-finalizable and its active-session lease had been released out from under live work — the same persistence and lifecycle race the drain exists to close, just relocated past the deadline instead of removed. Give _turn_futures a session association (Future -> sid, the same key space as server._sessions) at both submit sites, and on deadline expiry exclude the sids whose futures are still running from the flush. Those sessions are retained unfinalized and therefore recoverable; sessions with no live turn finalize exactly as before. The done-callback now pops under the lock, since a bare dict.pop is not the drop-in set.discard was. wait semantics, the reserve math and the bounded per-tick sleep are unchanged, so this adds no shutdown latency. All three shutdown callers (orphan, sigterm, and the tight stdin_closed wait=2.0 path) funnel through this one function and are covered.
The PR's skip-live-sessions optimization is partially defeated by
server._shutdown_sessions() registered via atexit (server.py:1172),
which runs on SystemExit after shutdown() returns for the SIGTERM and
stdin_closed paths. The orphan path (os._exit(0)) bypasses atexit.
This is a pre-existing issue — the old finalize-first order had the
same atexit interaction. The comment documents the gap and suggests a
follow-up: gate _shutdown_sessions on not session.get('running').
…croll jitter (fixes NousResearch#73629) Fix direction inspired by PR NousResearch#73674 by @drbronson with added Vitest component unit tests.
Review follow-up on the NousResearch#75714 salvage: perfectionist/sort-imports would fail lint CI; autofixed.
`StatusRulePane` renders `StatusRule` outside the `!isBlocked` guard in appLayout.tsx, so the status rule stays mounted underneath approval, model-picker, pager, sessions and every other blocking overlay. Its three timer-driven components keep firing the whole time: `FaceTicker` (glyph, 1s clock, verb rotation), `SessionDuration` (1s) and `IdleSince` (1s). Every tick re-renders a rule nobody can see, and in an Ink TUI that churn reads to the user as the dialog flickering. Gate all three components' interval creation on the existing `$isBlocked` computed store, so nothing is armed while an overlay covers the rule. The pause alone would leave the elapsed read-outs frozen at the moment the overlay opened, so each effect re-seeds `now` from the wall clock when it re-arms. `SessionDuration` and `IdleSince` already did this; `FaceTicker` gains the same re-sync. Closing a five-minute overlay now resumes at the true elapsed time instead of the pre-overlay value. No new store is introduced — `$isBlocked` already exists and already ORs the current OverlayState field set.
`$isBlocked` answers "is text input suspended", not "is the status rule covered". appLayout uses it only to hide the input rows (appLayout.tsx:384); `StatusRulePane` renders outside that guard, at :365 for `at="top"` and :449 for `at="bottom"`. So the previous revision paused FaceTicker / SessionDuration / IdleSince under prompts that leave the rule fully on screen — approval, billing, subscription, confirm, clarify, sudo and secret all render through PromptZone in NORMAL FLOW above ComposerPane (appOverlays.tsx:58-162, appLayout.tsx:553-568). They push the rule down; they do not cover it. Freezing a visible clock is a worse bug than the churn being removed. Replace it with `$isStatusRuleOccluded`, a narrow derived store over overlay + ui state covering only what actually paints over the rule: - `widget` — the modal widget slot renders at viewport level (ActiveWidgetSlot, sdk/host.tsx:209) so it can anchor the full-screen absolute `Overlay` against the whole terminal. - the FloatingOverlays set (modelPicker, pager, petPicker, sessions, skillsHub, pluginsHub) — but only when `ui.statusBar === 'top'`. That panel is `position="absolute" bottom="100%"` inside ComposerPane's relative Box (appOverlays.tsx:387), so it grows UPWARD over the top rule and can never reach the bottom one. Deliberately excluded: the PromptZone flow states above; `agents` and `journey`, which unmount the entire ComposerPane subtree (appLayout.tsx:553) so React's effect cleanup already clears the intervals; `ambient`, an in-flow dock; and composer completions, which share the floating grid but are a render prop that changes per keystroke — re-arming a 1s interval on every character would restart the countdown each time and starve the tick. `statusBar: 'off'` needs no branch: StatusRulePane returns null for both slots, so the timers never mount. Tests: the store-level cases are re-split into occluding and non-occluding sets, and an AppLayout-level `describe` mounts the real layout so the rule sits in its true position — asserting that under approval and sudo the rule is still rendered AND its clock advances (1m 0s to 1m 30s), that a floating model picker suppresses the clocks with the rule at the top, and that the same picker leaves them armed with the rule at the bottom.
Review follow-up: $isStatusRuleOccluded and FloatingOverlays each enumerated the same six overlay kinds — adding a 7th floating panel required updating both or the timer gate silently missed it. Extracted hasFloatingPanel as the shared predicate (completions stays local to FloatingOverlays; it deliberately never occludes the status rule). Full ui-tui suite green (1487 tests).
… LCM recovery (issue 441) Root fix (Option A) for design-session "Context compression exhausted" crashes. The three compression-retry handlers after an API overflow/413/long-context error (conversation_loop.py ~4229/4488/4747) passed the tool-BLIND messages-only estimate (approx_tokens) to _compress_context, so hermes-lcm's forced-overflow recovery armed on the message count and missed overflows driven by tool-schema/system overhead. Now they pass estimate_request_tokens_rough(api_messages, tools=...) — the same overhead-aware estimator already used at :4580 — so recovery arms on the TRUE request size; LCM's _overflow_recovery_assembly_cap self-subtracts the overhead so the full request fits. Empirically validated on real failed session 6dddf1a67b76 (LCM engine, floor=24000/cap=248000): observed 256,359 >= 248,000 -> arms; recovery 231,313->208,559 msg-tokens -> full request 233,605 < 272,000 FITS. Prior messages-only path did NOT arm (231K < 248K) and crashed. Durable copy: ~/.hermes/local-patches/optionA-overflow-overhead-aware.patch (survives hermes update reset). Upstream PR pending. classify_api_error call at :3667 intentionally unchanged (not recovery). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… add recovery-path tests Post-tool compression path passed context_compressor.last_prompt_tokens (0 in the no-usage fallback) to _compress_context instead of the overhead-aware _real_tokens computed just above — same tool-blind bug as the overflow handlers (upstream PR NousResearch#77169 review, teknium1). Also adds production-path regression tests asserting the 413, context-overflow (two wordings), and Anthropic long-context recovery handlers pass estimate_request_tokens_rough(..., tools=...) (sentinel-patched) to _compress_context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on (NousResearch#71067) _CryptoStateStore.get_encryption_info only consulted mautrix's in-memory MemoryStateStore, which has no record of m.room.encryption for rooms the bot joined in the past (the raw-sync path never feeds those state events through set_encryption_info). On a fresh crypto store this returns None for all previously-joined rooms, so OlmMachine reports them as unencrypted, never tracks peer devices, and silently drops all inbound messages. Fix: pass the mautrix Client into _CryptoStateStore so get_encryption_info can fall back to a live GET /_matrix/client/v3/rooms/{room_id}/state/ m.room.encryption query when the in-memory store returns None. The result is cached back via set_encryption_info so subsequent lookups (and OlmMachine device tracking) hit the fast path.
… changes The crypto store is keyed by Matrix user ID, not device ID, so swapping in a new access token (which mints a new device_id) silently inherits the previous device's Olm account. That account's identity keys can never be published under the new device ID, and the pickle key embeds the old device ID anyway — the result is stale-key mismatches and cross-signing signatures the homeserver refuses to replace, degrading E2EE in ways that are hard to diagnose (peers silently withhold room keys). _reset_crypto_store_if_device_changed() compares the store's persisted device ID against the live one at connect time and wipes the store on mismatch, so a fresh Olm account is generated for the new device instead of reusing stale key material. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…E_ID connect() resolved client.device_id as `self._device_id or resolved_device_id`, so a configured MATRIX_DEVICE_ID masked the live whoami() device. With persisted A, configured A and a rotated token reporting B, the reset compared A to A and never fired — exactly the token-rotation case this PR claims to handle. An access token is bound to one device and the homeserver only accepts key uploads for that device, so a configured value naming a different one cannot work. The live whoami() device now wins on conflict and logs an error naming both. The configured value is still preferred when whoami() reports no device. Adds a connect()-level regression for persisted A + configured A + whoami B, and corrects test_connect_uses_configured_device_id_over_whoami, whose stated premise this inverts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-derivation of NousResearch#23254 (@devsart95) on today's flush loop. The turn flush in _flush_messages_to_session_db wrote one BEGIN IMMEDIATE transaction per message row; a typical agent turn (user + assistant + tool results) paid 3-8 transactions -- and, off WAL (the default on macOS while the WAL-reset guard is active), 3-8 fsyncs -- per turn. Adds SessionDB.append_messages_batch: same row shape as append_message (shared _prepare_message_row serializer + _MESSAGE_INSERT_SQL column list, so the two writers cannot drift), same compression-lock and compression-closed guards, one aggregated session-counter UPDATE, one transaction for the whole batch. Row serialization stays outside the write lock. The flush loop now collects the turn's new rows and writes them in one call. All-or-nothing pairs exactly with the persisted-marker stamping: on failure no rows landed and no markers were stamped, so the next flush re-writes the whole tail (same recovery contract as before, minus the partial-prefix case that could double-count). Measured (same harness, 5-message turn, journal_mode=DELETE, synchronous=FULL): 2.32ms -> 0.83ms median per turn flush (64% faster, 5 fsyncs -> 1). On WAL the win is smaller but the atomicity fix holds.
Sibling sites of the per-message flush pattern: both branch-seed paths (session.branch in methods_session.py and the lazy seed persist in server.py) copied the parent history row-by-row -- one transaction per row, and a branch seed can be hundreds of rows. Route both through SessionDB.append_messages_batch. The server.py path also gains real atomicity: _branch_seed_persisted assumed every row landed, which the per-row loop could not guarantee.
…rites The flush now goes through append_messages_batch; MagicMock-based assertions and barrier fakes that hooked append_message observed nothing (the flush's try/except swallowed the AttributeError). Assert on the batch payload instead.
… share guards, chunk seeds Simplify-pass folds on the NousResearch#23254 salvage: - REUSE (HIGH): append_messages_batch now delegates row serialization to the pre-existing _insert_message_rows helper (already shared by replace_messages / archive_and_compact / portability import) instead of adding a third serialization path (_prepare_message_row + _MESSAGE_INSERT_SQL are gone). One row-writer for every multi-row path; the row-ID return was consumed by no production caller, so the batch returns the inserted count. - QUALITY (HIGH): the compression-lock + compression-closed admission guards are extracted into _check_transcript_write_guards, shared by append_message and append_messages_batch (previously duplicated 23 lines that had already needed targeted fixes, NousResearch#74478). The role-gated reasoning filtering is no longer duplicated in run_agent.py — it lives at its one site inside _insert_message_rows. - EFFICIENCY (MEDIUM, measured): unbounded seed copies hold one BEGIN IMMEDIATE for seconds (10k rows ~= 2.4s; FTS triggers dominate) and monopolize the in-process write lock. append_messages_batch grows a chunk_rows param; all seed/copy call sites use chunk_rows=500. Same recovery semantics as the old per-row loops, bounded lock holds. - REUSE (MEDIUM): the two remaining per-row branch-copy loops found by the pass (gateway/slash_commands.py /branch, hermes_cli cli_commands_mixin.py branch) are converted to chunked batches too (AsyncSessionDB's generic to_thread forwarder covers the async site). Turn-flush benchmark unchanged after the refactor: 2.43 -> 0.87 ms median per 5-message flush (64% faster).
…pend_messages_batch CI-caught: test_verification_stop_caching and test_tui_gateway_server::test_native_vision_turn_persists_a_renderable_image_ref both assert on append_message.call_args, but the flush loop now calls append_messages_batch. Same class of test-fake fallout fixed in 5 other files — these two were missed.
…#38491) Re-derivation of NousResearch#38491 by @stremtec onto current main (the original is 10,119 commits behind; the hook moved into ui-tui/src/app/). The hook returned a fresh object literal every render, defeating memoization in useMainApp's consumers; useMemo over the (all-useCallback-stable) handles makes the return referentially stable. Dep array covers ALL nine returned handles incl. trimTail (the re-derivation initially omitted it - stale-closure class).
…ection get_messages_as_conversation, get_resume_conversations, and get_ancestor_display_prefix still took self._lock — the same global choke point the read-path split (WAL per-thread read-only connections) was meant to remove from every recall/browse read. These three are the hottest reads in the file: every session resume across the gateway, CLI, and ACP adapter goes through one of them, so a resume racing a burst of concurrent-session writer flushes still convoys behind them exactly like the fixed paths used to. _session_lineage_root_to_tip (the lineage walk shared by all three, plus get_conversation_root) had its own independent self._lock use and needed the same conversion — without it the outer functions still blocked on the very first line. Verified empirically: a reader thread calling all three functions while another thread holds self._lock blocked for the writer's full hold duration before the fix, and returned immediately after (SQLite 3.50.4 in this dev venv falls back to journal_mode=DELETE per the WAL-reset-bug guard, so the requires_wal-marked regression test is exercised via a local WAL-forced script instead; it still runs and passes on any runtime where WAL is actually active).
…-email chore: add contributor email mapping for ArcherQAQ
…probe sites fetch_endpoint_model_metadata's generic (non-LM-Studio) /models fetch and its llama.cpp /v1/props context-length follow-up built request URLs straight from the unrewritten candidate, unlike every other local-probe site. Both retained the multi-second dual-stack IPv6 connect penalty that _localhost_to_ipv4() exists to skip (measured on macOS: localhost 32.9ms vs 127.0.0.1 0.1ms on a dead port; ~2s on Windows). normalized stays the cache key so caching behavior is unchanged; only the outbound request target is rewritten. Re-derived from PR NousResearch#61528 onto current main (original no longer applied cleanly).
CI slice 3/7 failures: run_conversation tests pass MagicMock base_urls through the metadata probe path; re.sub raised TypeError where the old code let non-strings flow through. Preserve that contract.
…search#60800) Three fixes for the Desktop/TUI cold-start stall where the event loop is blocked for ~14s between HERMES_BACKEND_READY and the first prompt (NousResearch#60800): 1. copilot_auth: skip subprocess fallback when any Copilot env var is explicitly set (even if invalid). The user expressed token intent via env var; silently substituting a CLI token is surprising and the subprocess adds up to 5s on Windows. 2. tui_gateway/ws: run resolve_skin() via asyncio.to_thread so config loading + skin engine init do not block the WS read loop during the cold-start RPC burst. 3. web_server: extend _warm_gateway_module to pre-import the heavy module chains (auth, copilot_auth, runtime_provider, skin_engine, inventory, model_switch) that the first WS connection + RPC burst would otherwise import on the loop thread. These trigger .pyc compilation and Defender scans on Windows (15-30s per the existing comment) and were not covered by the original gateway-only warm. Tests: 5 new tests in test_cold_start_gil_stall.py + 2 new tests in test_copilot_auth.py. All 36 copilot_auth tests + 16 ws/web_server tests pass.
Review folds on the NousResearch#60807 salvage: - resolve_skin tests are behavioral (thread-ident probe + ready-frame wiring check) instead of pure source inspection, per the NousResearch#72720 pattern; a source assertion remains as belt-and-braces. - The warm-list test does REAL imports and checks sys.modules — _warm_gateway_module swallows ImportError by design, so the PR's tracking-stub test would pass even with a typo'd module name. - resolve_copilot_token logs a debug line when the env-var short-circuit skips the gh-CLI fallback (behavioral change made observable).
…ency Salvage of NousResearch#26860 (hunk 2, ported \u2014 the PR's base predates the current gateway layout by ~11.9K commits). Messaging platforms can set gateway.platforms.<key>.skip_context_files: true to skip the filesystem-heavy context-file discovery (SOUL.md, AGENTS.md, .cursorrules walks) during AIAgent construction \u2014 10-100x slower stat()/walk costs on Windows made this a real per-turn tax. Soul identity is still loaded (single small file), so the persona survives. The flag participates in _agent_config_signature so toggling it rebuilds the cached agent instead of silently reusing a prompt built under the other setting (prompt-cache correctness). The PR's hunk 1 (mtime-caching the per-turn dotenv reload) was dropped: df51ad7 mtime-cached load_config/read_raw_config and c2eda92 removed the per-turn deepcopies, capturing most of that win; the function has since gained a multiplex early-return and managed-scope overlay that the original whole-function skip would have bypassed.
… parent channel (NousResearch#77830) When a Discord channel message initiates a relay auto-thread, the thread does not exist at ingest (source.thread_id is None) — the connector creates it on its FIRST send and auto-threads any outbound carrying the reply anchor. The final reply carries that anchor, so it lands in the thread. But the tool-progress / status bubbles (the "Searching the web for..." updates and the streaming preamble) were sent with _progress_metadata=None and _progress_reply_to=None: _resolve_progress_thread_id returns None for Discord (only slack/mattermost get a synthetic thread), so the progress send had no anchor and the connector posted it FLAT in the parent channel. Result: the search-status updates leaked outside the thread while the answer threaded (staging repro 2026-08-02). The connector now stamps prospective_thread_id on the inbound (the anchor message id == the id of the thread it will create). Reuse it: when a relay-delivered Discord channel-initiate carries prospective_thread_id and has no real thread yet, carry the reply anchor (event_message_id) on both the progress metadata (reply_to_message_id) and the progress reply_to, so the connector routes the progress bubble into the SAME auto-thread as the final reply. Applied to both the tool-progress path (_progress_metadata / _progress_reply_to) and the status/interim callback path (_status_thread_metadata). Events already arriving in a real thread, DMs, and non-relay sources are untouched (guarded on delivered_via_upstream_relay + prospective_thread_id + not thread_id). Tests: two new cases in test_run_progress_topics.py — a relay Discord channel-initiate asserts every progress send carries the anchor (reply_to + metadata.reply_to_message_id + non_conversational), and an event already in a real thread asserts the synthetic-anchor path does NOT engage. Full gateway progress + relay + session suites green (228 passed).
Salvage of NousResearch#71282 (Fixes NousResearch#71281): a routable-but-dead endpoint (corp LAN address while off-VPN) blackholes TCP SYNs, so every probe in the model-metadata waterfall waits out its full connect timeout — 20+ seconds of stall per startup across detect_local_server_type, fetch_endpoint_model_metadata, and the per-model probes. A module-level blackhole cache keyed on host:port is populated when any probe observes a ConnectTimeout (httpx or requests; read timeouts deliberately excluded — an accepted connection is not a blackhole) and consulted at the top of each guarded function. 30s TTL: long enough to collapse one startup burst, short enough that VPN recovery is picked up without a restart. Guard ordering: blackhole check -> disk L2 -> HTTP waterfall, and a blackholed leg aborts the remaining legs instead of letting each stall in turn. Squash of the PR's two real commits (the branch's merge commits made it un-rebase-merge-able; content verified identical via merge-tree).
The Herald Release — voice (streaming TTS, barge-in, wake words), A2A v1.0, outbound webhooks, grounded citations, desktop platform wave. ~3,650 commits, ~1,400 PRs, ~1,200 issues closed, 650+ contributors since v0.19.0. Also: contributor audit additions (18 email mappings, bot-filter widening).
ragnos-dev
marked this pull request as ready for review
August 4, 2026 03:33
ragnos-dev
added a commit
that referenced
this pull request
Aug 13, 2026
The v2026-8-3 rebaseline (#15) dropped the heartbeat-file stamping that 639d041 added, while scripts/launchd/photon-sidecar.sh (exports PHOTON_SIDECAR_HEARTBEAT_PATH) and keez-reaper.sh (kickstarts the sidecar + inbound adapter pair when the file goes stale >90s) still assume it. Result: as soon as both processes are up, the reaper kickstarts the pair every tick forever, and Keez inbound can never hold a connection. Ports the original stamping back onto current main: stamp on /inbound consumer connect and on every 25s keepalive tick. Co-authored-by: Dr. RAGnos <doctor@ragnos.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Retained delta
The candidate changes 26 files relative to official v2026.8.3. The only non-RAGnos functional delta is the narrow Photon secure attachment patch under plugins/platforms/photon and its tests. Buzz, local voice, Kanban mirror behavior, and A2A are unchanged.
Evidence
Production boundary
This PR does not update the workspace pin, restart launchd, send a device message, or deploy. Production activation remains gated on the real-device inbound and outbound round trip plus workspace ship gates.
Rollback
The prior live fork commit is 490ac0a.