Skip to content

fix(sync): resolve 8 Nous upstream conflicts blocking the nightly (513 commits) - #16

Closed
vashkartik wants to merge 2909 commits into
ace/patchesfrom
fix/nous-sync-conflicts-20260726
Closed

fix(sync): resolve 8 Nous upstream conflicts blocking the nightly (513 commits)#16
vashkartik wants to merge 2909 commits into
ace/patchesfrom
fix/nous-sync-conflicts-20260726

Conversation

@vashkartik

Copy link
Copy Markdown
Owner

Unblocks the nightly Nous upstream sync, which has been failing since 2026-07-25 04:00 (merge conflicts unresolved) and again on 07-26. Merges 513 upstream commits into ace/patches.

Why it was stuck

git rerere had only preimages recorded (the nightly aborts before resolving), so every 4 AM run re-hit the same conflicts and failed. It fails safe — the live runtime was never touched — but the fork has been frozen at the Jul 23 baseline.

Conflicts resolved (8 files)

File Resolution
tui_gateway/server.py (6 hunks) Kept Ace's update-guard turn lease (_claim_update_turn / _release_update_turn_if_idle) — the nightly apply controller depends on it — and took upstream's retained-error snapshot, empty-truncation refusal (confirm_empty_truncate), cancellation emit, and turn-marker retirement. Both sides' helper sets are disjoint, so hunk 1 keeps both.
use-session-actions/index.ts Ace seeds an in-flight turn from the server resumed.inflight; upstream recovers from a client-side journal. Complementary, not redundant — kept both, gated so the journal wins and the turn can't render twice. Fail-latch keeps upstream's pre-recovery check but does not bail when the server handed us a live turn.
types/hermes.ts Kept Ace's named SessionInflightSnapshot and absorbed upstream's new auto_continue field plus error/recoverable/status.
markdown-text.tsx Ace's local-HTML preview and upstream's session-ref links are disjoint predicates — both chained.
electron/main.ts Kept Ace's microphone import + upstream's expanded native-auth-decisions imports.
use-composer-metrics.ts Kept Ace's poppedOutRef (tracks the full gate incl. mobile/coarse-pointer embeds; upstream's inline check covers only secondary windows). Dropped the now-unused root.
gateway/platforms/base.py Pure upstream addition (prompt_response) — took upstream.
tests/test_tui_gateway_server.py See below.

Three fork-aware test adaptations

  1. Dropped upstream's test_clarify_timeout_seconds_maps_non_positive_to_unlimited — the fork deliberately removed _clarify_timeout_seconds (protected patch: desktop clarify has no wall-clock timeout).
  2. assert replaced == [...]replaced[0] — Ace's _persist_session_history durably writes the completed turn at turn end, so the truncation write is the first of two. Upstream has no turn-end persist.
  3. _CompressingAgent.run_conversation now absorbs **_kwargs — upstream's fix(desktop): persist @image: refs instead of vision-enrichment text so attachments survive session switch and restart NousResearch/hermes-agent#70720 passes a new persist_user_message kwarg that the rigid fake signature rejected.

Verification (the nightly's own gates)

  • capella-patch-guard.sh23/23 protected patches present
  • pytest (14 focused files) — 790 passed, 0 failed
  • tsc -bexit 0, no stray .js emitted
  • vitest — 7 files, 58 passed

Why this must land (rather than rely on rerere)

Resolutions are now recorded — the 07-26 run already auto-replayed 6 of them. But adaptations 2 and 3 sit outside any conflict hunk, so rerere can never carry them; the nightly re-derives an un-adapted tree and the gates fail. Landing this puts them in the base permanently.

🤖 Generated with Claude Code

Frowtek and others added 30 commits July 24, 2026 19:10
…eletion

Addresses @egilewski's review: the parent-directory check still deleted
checkpoint history for the most common unmount layout.

Detaching storage removes the parent outright in some layouts
(`/Volumes/Ext/proj` on macOS, `/media/<user>/<label>/proj`), which the first
commit handles. But in the classic static layout — `/mnt/volume/proj`, an
fstab entry, a container bind-mount — unmounting removes the contents and
leaves the mount point behind as an empty directory. `parent.is_dir()` is then
true, the project is absent, and the startup sweep deletes its ref, index and
metadata: exactly the case this PR set out to protect.

Reproduced against the real predicate before this commit:

    mount root vanished (macOS)   -> False   ok
    empty surviving mount point   -> True    <-- history deleted
    really deleted (siblings)     -> True    ok

An empty parent carries no information: it looks identical whether the volume
was detached or the project was deleted. So require the parent to actually say
something — it holds some other entry (we observed a populated directory that
does not contain the project), or it is itself a live mount point (the volume
is attached right now and demonstrably does not hold the project).

The cost is that a project deleted out of an otherwise-empty parent is no
longer reclaimed by the orphan rule. It is not leaked: the retention rule
reads `last_touch` rather than probing the filesystem and still collects it,
so reclamation is deferred, not lost. That is the right direction for a
predicate whose false positive destroys a user's restore points unattended.

`_dir_has_any_entry` stops at the first entry via `os.scandir` instead of
materializing a listing, since a project root can hold a large tree.

tests/tools/test_checkpoint_manager.py: `test_surviving_empty_mountpoint_
keeps_its_checkpoints` pins the reviewed case, and `test_empty_parent_project_
is_still_reclaimed_by_retention` pins the deferral above so the safety valve
cannot silently regress into a leak. Both fail on the previous commit. The
real-orphan control now seeds a sibling so it exercises a populated parent
rather than the ambiguous empty one. 80 passed in the checkpoint suite; the 2
remaining failures (`TestGitEnvIsolation`, `TestClearFunctions`) fail
identically on clean main.
…orphan classification

Follow-up to the cherry-picked NousResearch#69063: egilewski's review found that the
_dir_has_any_entry(parent) guard treats ANY entry in the mount point's
parent as proof the volume is attached — but unmounting exposes the
UNDERLAY directory's own files (e.g. a .keep placeholder), so a populated
underlying mount-point dir still classified the project as an orphan and
deleted its ref/index/metadata. Reproduced on both main and the PR head.

Attachment evidence is now positive instead of circumstantial:

* _volume_evidence() records the parent directory's (st_dev, st_ino)
  identity in the project's metadata while the workdir is observably
  live (at _register_project/_touch_project time). A mount point
  resolves to the mounted filesystem's root while attached and to the
  underlay directory after detach — same path, different directory,
  different identity.
* _workdir_is_observably_gone() now requires the parent visible at
  prune time to match that recorded identity before the populated-parent
  check can classify an orphan. A mismatch means a different directory
  (the underlay) is showing through — a detached volume, not an
  observed deletion.
* Metadata without a recorded identity (written by older versions) is
  never orphan-classified — unsure never deletes; the retention/stale
  rule still reclaims genuinely abandoned projects off last_touch.
* The frozen pre-v2 layout has no metadata channel for the identity, so
  it keeps the structural checks only (require_parent_identity=False).
* A failed evidence probe on re-registration preserves the previously
  recorded identity — stale evidence can only make pruning MORE
  conservative.

Windows: st_dev/st_ino of 0 (filesystems without file IDs, some network
shares) is treated as "no evidence recorded", which falls into the
conservative never-orphan path. os.path.ismount and Path.stat are
cross-platform; no POSIX-only calls added.

tests/tools/test_checkpoint_manager.py: adds egilewski's exact
regression (checkpoint history for mnt/volume/project, detach exposes
mnt/volume/.keep, prune with orphan deletion enabled → NOT deleted;
fails on the bare cherry-pick, passes with this fix), plus
no-recorded-identity conservatism and probe-failure identity
preservation. His absent-parent/empty-parent/retention/genuine-deletion/
live-project controls all still pass.

Reported-by: egilewski (review on NousResearch#69063)
Use wall deadlines for deleteWebhook and start_polling, then fail cold startup unless getUpdates proves progress. This lets the gateway discard partial PTB state and retry with a fresh adapter.\n\nRefs NousResearch#67498
Give Telegram a 180s default outer connect budget so cold polling can prove getUpdates readiness. Preserve the 30s default for other platforms and all explicit config/env overrides.\n\nRefs NousResearch#67498
…neration

Follow-up hardening for the salvaged NousResearch#69240 readiness gate (NousResearch#67498):

- _start_polling_once now returns its (generation, progress_event) pair
  so the strict cold-start gate binds to exactly the generation it
  started, instead of re-reading self._polling_progress_event which a
  concurrent recovery task may have replaced with a newer generation's
  event (the G1/G2 race flagged in the NousResearch#69240 review).
- Strict cold start no longer schedules background polling recovery: a
  polling error during the readiness wait is captured by a strict
  callback and fails the connect attempt immediately with a loud
  OSError, so GatewayRunner disposes the partial adapter and retries
  with a fresh one — no more waiting out the full readiness deadline on
  a generation that already errored, and no G2-on-partial-app healing.
- After readiness is proven the strict callback delegates every later
  polling error to the real background-recovery callback, preserving
  the existing degraded/reconnect semantics for the polling lifetime.
- The readiness-timeout error message now states the deadline and that
  the gateway will retry with a fresh adapter (loud failure, not a
  silent wait).
- Regression tests: current-generation progress connects; a polling
  error during strict cold start fails fast without scheduling
  background recovery (the NousResearch#67498 idle-threads shape); stale-generation
  progress is rejected.

Progresses NousResearch#67498
The strict cold-start readiness gate (NousResearch#67498) means adapter.connect() no
longer returns True until the mocked start_polling records a successful
getUpdates round trip for its generation. Update the conflict-suite
Application mocks accordingly:

- fake_start_polling side effects call
  adapter._record_polling_progress(adapter._polling_generation) on the
  initial connect (retry generations intentionally do NOT auto-progress
  where a test asserts the conflict count survives an unproven retry).
- _build_polling_app takes the adapter so its start_polling mock can
  record progress.

Without this, the cold connects in these tests wait out the full 60s
readiness deadline and fail — which is exactly the fail-closed behavior
the gate is supposed to provide when polling shows no progress.
Both the Desktop panel and the CLI setup flow need somewhere in .env to put
a custom endpoint's API key. Deriving the name from the endpoint's hostname
collapses two servers on one machine onto a single slot, and every IP-based
local endpoint slugs to a digit-leading name that save_env_value rejects
outright. Key off the endpoint's own identity and keep a fixed prefix.

Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
…as an update (NousResearch#69179)

The desktop self-update chain (Desktop -> hermes-setup --update ->
hermes update -> hermes desktop --build-only -> relaunch) rebuilds
Hermes.exe on the user's machine and declared success on bare file
EXISTENCE. A truncated PE (corrupt cached Electron zip / interrupted
extraction or rcedit rewrite / full disk) or a wrong-architecture
unpacked tree therefore shipped as the 'updated' app, which Windows
refuses to load with 'This app can't run on your computer'
(此应用无法在你的电脑上运行) — and the previous working build had
already been wiped by before-pack.mjs, leaving nothing to fall back to.

Fix, in three parts:

- hermes_cli/main.py: post-build integrity gate on Windows
  (_ensure_desktop_exe_launchable). Parses the PE header of the freshly
  built Hermes.exe — MZ/PE magic, section-table completeness vs file
  size (catches truncation), and COFF machine vs the host arch (catches
  arm64/x64 mixups). On failure it purges the (likely corrupt) cached
  Electron zip, invalidates the content-hash build stamp so the
  updater's retry-once genuinely re-downloads and rebuilds, restores
  the previous build from the .bak tree when one exists (keeping the
  corrupt tree as .corrupt for diagnostics), tells the user the update
  was aborted and their old version kept, and exits nonzero.
  _desktop_packaged_executable also now prefers a host-loadable PE over
  pure newest-mtime when multiple win-*-unpacked trees coexist.

- apps/desktop/scripts/before-pack.mjs: on win32, the previous unpacked
  tree is preserved as <appOutDir>.bak (only when it holds the product
  exe — partial/corrupt trees still get the plain wipe) instead of
  being destroyed, providing the rollback material for the gate above.
  Non-Windows behavior is unchanged.

- Behavior-contract tests: tests/hermes_cli/test_desktop_exe_integrity.py
  (23 tests — synthetic PE fixtures for truncation/non-PE/arch-mismatch,
  rollback semantics, and the build-only exit contract) and 6 new vitest
  cases in before-pack.test.mjs for the .bak preservation rules.

Progresses NousResearch#69179
…endpoint

Test enumerates a custom provider's catalogue and the panel holds the result
in discoveredModels, but the save payload never carried it, so only the one
model the user hand-typed reached providers.<id>.models. Every downstream
picker reads that map straight from config.yaml with no live probe, which is
why a proxy serving 18 models offered exactly one.

Send the discovered list and merge it onto the entry, so models already
known keep their context lengths.

Fixes NousResearch#69988

Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com>
…yaml

The Custom Endpoints panel wrote the raw key to providers.<id>.api_key, so
the credential sat in plaintext in a file users routinely share and commit.
The input is masked, so nothing warned them.

Write the key to .env and reference it via key_env, the same indirection
built-in providers use and that runtime_provider already resolves. The read
side has to move with it: reporting has_api_key from api_key alone would
show "no API key" for every migrated endpoint, and activate copying only
api_key would drop the credential entirely. Delete now clears the .env slot
too, and an entry still carrying a pre-fix plaintext key is migrated on its
next save so existing users get cleaned up without re-entering anything —
unless the key is a hand-written ${VAR} template, which is already safe and
must not be duplicated into a second env var.

Fixes NousResearch#69449

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: asorry75 <33794789+asorry75@users.noreply.github.com>
hermes model's custom-endpoint flow is the other write path that produced a
plaintext key, on both the model block and the custom_providers entry. Route
it through the same .env indirection as the Desktop panel, and swap an
existing entry's inline key for the reference when the URL is re-saved.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Bug-class coverage for both fixes: the full catalogue survives Save, context
lengths are preserved, the key never lands in config.yaml on either write
path, blank clears it, a pre-fix plaintext key migrates while a ${VAR}
template is left alone, two endpoints on one host keep separate credentials,
and an IP-derived name is still a valid POSIX env var.

The two delete tests asserted on the plaintext mirror; they now assert the
same invariants against the credential reference.
The desktop gateway passed the vision-enriched, model-only message text
(carrying an `image_url:<path>` hint) straight into run_conversation as
the persisted user turn. The renderer only parses `@image:<path>`, so it
could not rebuild the attachment from history: after a restart the image
was gone and only the caption survived, and on a live session switch the
warm cache disagreed with the authoritative text and the frontend
"rescued" the image by appending it after the caption.

run_conversation already supports persist_user_message for exactly this
"what the model sees" vs "what gets stored" split; it was simply never
wired up for the attachment path.
Persisted history carries no attachment metadata for non-image refs, so
resume reconciliation dropped `@file:` chips off a user turn whose text
matched. Carry the warm cache's refs forward when the resumed message has
none of its own, never replacing refs that are already present.

(cherry picked from commit eac5b0a)
The unquoted alternative in the directive pattern is `\S+`, so a ref built
by string interpolation truncates at the first space and strands the tail
as loose text next to a broken thumbnail. Composer images live in the app's
userData dir, which on macOS is `~/Library/Application Support/<App>/` — so
every pasted or dropped image hit this.

Adds format_reference_value next to REFERENCE_PATTERN, mirroring
formatRefValue in the desktop's directive-text.tsx, and covers the
round-trip through the parser.
…s too

A turn routed to a model that takes pixels directly sends `content` as a
parts list, and the session store deliberately ignores a plain-string
persist override for a list payload — a text override must not erase a
turn's image summary. So the override was dropped for every user on a
vision-capable main model, and the durable row kept only the caption plus a
literal `[Image attached at: ...]` / `[screenshot]`, which the renderer
cannot turn back into an image. Only vision-preprocessed (text-mode) turns
were actually fixed.

Mirror the shape instead: swap the text part for the `@image:` ref form and
keep the image parts, so the model still has the pixels for the rest of the
session, and drop the `[screenshot]` stand-in on the way into the bubble
when a ref was lifted from the same message.
Matches the two derived values above it and fixes the indentation.
Session previews are the first 60 characters of the first user message, so
persisting the @image: directives ahead of the caption labelled the session
with a truncated file path in the sidebar, session switcher, and command
palette. Clients lift the refs out of the body line by line, so moving them
after the caption changes nothing about how the turn renders.
The unit tests cover each layer in isolation, but nothing exercised the whole
chain the bug lived in: the real gateway persisting an attachment, SessionDB
holding it after the process exits, and the renderer rebuilding a thumbnail
from the stored turn.

Seeds a session through the real gateway with an image attached, then launches
desktop against it — so the first render is already the relaunch case. Pins
native image routing (the majority path, and the one where a text-only persist
override is dropped) and stages the file behind directory and file names with
spaces, mirroring the macOS composer's Application Support path.
Map picker-prefixed custom provider selections back to their configured model IDs before validation, persistence, and API requests.

Fixes NousResearch#68347
Attribution check needs a mapping for the cherry-picked commit's author so
release notes credit them correctly.
…el-id-resolution

fix(model_switch): don't send a picker prefix as the custom provider model id
…point-keys-and-models

fix: custom endpoint keys go to .env, and Save keeps the whole model list
…age-persist

fix(desktop): keep attached images renderable across session switches and restarts
… commands (NousResearch#71048)

A real APPLICATION_COMMAND interaction forwarded over the relay arrived
slash-less: _discord_interaction_to_event set text = data['name'] ("new",
not "/new"), MessageType.TEXT, and dropped options entirely — so a
registered /new dispatched as plain chat instead of a command
(MessageEvent.is_command() is text.startswith("/")).

Port the connector's Slack slash-command precedent (normalizeSlackCommand
builds `${command} ${args}`.trim() with a leading slash and explicit
command type): for type-2 interactions build "/" + name, append rendered
options space-separated (scalar options contribute their value, matching
the native adapter's f"/model {name}" shape; SUB_COMMAND/
SUB_COMMAND_GROUP contribute their name then recurse into nested
options), and set MessageType.COMMAND. Type-3 (custom_id) and other
interaction types are unchanged. This implements the interaction->command
sub-design previously flagged as deferred in the _on_passthrough
docstring.

Companion connector fix in gateway-gateway: fix(relay): strip own-mention
prefix so addressed slash commands dispatch.
Resolve @session:<profile>/<id> reference values to the session's title:
the in-memory sidebar list answers most lookups, and an unknown id falls
back to GET /api/sessions/{id}. Cache, in-flight dedupe, and subscriber
fan-out mirror the external-link title resolver.

An untitled row resolves to empty rather than "Untitled session" so the
caller's short-id fallback stays the chip label.
Route session refs in the transcript through the title resolver so a
dropped session reads as its title instead of a truncated id, and use
Tabler's funnel for the session chip icon.
Assistant text goes through the markdown renderer, not DirectiveContent,
so a session reference an agent wrote came out as literal text. Rewrite
bare refs into `#session/<value>` links during markdown preprocessing and
dispatch that href to the shared chip in MarkdownLink, alongside the
existing media and preview hrefs.

Preprocessing already skips code fences and inline code, so a ref being
discussed in code stays literal. The pure parsing/href helpers move to
session-refs.ts to keep the resolver's React and API imports out of the
per-flush preprocess path.
Follow-up cleanup for PR NousResearch#71123:
- Extract getattr(args, 'lineage', 'single') == 'logical' to a local
  (appeared 3x in the export block)
- Document that the double _collect_delegate_child_ids traversal in
  delete_session is an intentional TOCTOU guard inside the write txn
OutThisLife and others added 27 commits July 26, 2026 03:26
…ck-scope

Scope measured-height vars to each chat surface
CuaDriverBackend caches a long-lived cua-driver subprocess for the life of
the Hermes process, and stop() was never called from anywhere — the driver
outlived the session that spawned it. NousResearch#69903 stopped the orphan from pegging
a core by disabling the cursor overlay, but left the process behind; this is
item 3 of NousResearch#28152 ("Hermes does not keep the driver alive after tool
completion").

Register an atexit hook, mirroring browser_tool's
atexit.register(_emergency_cleanup_all_sessions). atexit only, no signal
handlers, for the prompt_toolkit reason documented there. reset_backend_for_tests
now reuses the same teardown instead of repeating it.
…e-leak

fix(sessions): stop a /skill's own text becoming the session title
…eground-leak

Render the workspace pane from its own session slice
…tory-cost

perf(desktop): make streaming cost independent of transcript length
The sidebar "+" now stacks a tab instead of replacing the surface, so the
prior session stays mounted and several chat surfaces can be on the page at
once. Helpers that waited for the old transcript to disappear from the page
timed out, and `.first()` locators / bare `document.querySelector` calls
started resolving against the wrong session (CI's "resolved to 2 elements"
strict-mode violation).

Target the most recently mounted `[data-composer-target]` surface instead,
and assert the NEW surface is empty rather than waiting for the old text to
vanish.
An agent told to work in a fresh git worktree does exactly that — creates
it, cds in, and runs every later command there — but the session stayed
pinned to the checkout it started in. The desktop kept labelling the chat
with the primary branch while all the work landed somewhere else.

The desktop half already existed: session.info carrying a moved cwd runs
followActiveSessionCwd, which refreshes the project tree and scopes the
sidebar into the new project. The backend just never reported the move.

Reconcile the session's cwd against terminal_tool's per-session record at
the end of a turn, when the agent has stopped moving and its recorded cwd
is a stable answer. A plain cd stays what it always was — not a workspace
move — so the reconcile only fires when the recorded cwd sits in a
different git working tree than the session's workspace.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…-atexit

fix(computer_use): stop the cua-driver child on exit
Content links read as a small primary-tinted chip instead of an
underline, dropping the trailing external-link arrow. The tint is
currentColor-relative, so one class carries text and fill in the same
hue across every theme, and `box-decoration-break: clone` gives a
wrapped link a chip per line fragment.
The arc reduced to a few faint dots on session rows. Two causes: the ring
was outset by 2px into a scroller that clips horizontally, losing its
left and right runs; and its tail color defaults to the chrome
background, which is invisible against the sidebar, leaving only the
bright stop of each gradient pass.

An `arc-row` variant sits flush and ties the tail back to the ring
color. The ring's radius is now derived from its standoff (r_host + gap)
rather than inherited, which keeps any outset host concentric instead of
pinched.
The strip's rule is an inset shadow painted in the container's last pixel
row, and full-height tabs covered it — so each tab read as overhanging
the bar by 1px. Inactive tabs compensated with their own border, stacking
a second translucent line that darkened the seam.

Inactive tabs now stop 1px short and draw no bottom border, leaving the
container as the sole owner of one continuous rule; the active tab keeps
full height so it alone cuts through. Hover also darkens rather than
lightens, since lightening moved a hovered tab toward the active
surface's look.
Technical mode rendered the raw payload two different ways — a bare
block for most rows, a native `<details>` for file edits, whose
browser-drawn marker matches nothing else in the app. Both are now one
collapsed chevron disclosure at a smaller type size, with even padding
against the row body.
…ollow

fix(gateway): follow a session into the worktree it settled in
…-polish

Desktop UI polish: link chips, sidebar arc, tab strip rule
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and
printed "⚡ loading skill: work" there, then fired the skill's kickoff
prompt as a user message into whatever conversation was on screen.

The dispatcher resolves its target once, through resolveTargetSessionId,
and every other consumer of that answer already honors it: the output
writer binds to the target's stored id, and the busy gate reads the
target's own state. The send did not — `submitPromptText(message)` passed
no target at all, so submit fell back to `activeSessionIdRef`, which
names the foreground chat. NousResearch#71805 fixed the two sibling leaks in this
same function; this is the third and the one that actually moved the
user's prompt.

Forward the resolved pair instead. Every target the dispatcher serves —
a tile, a background queue drain, a session this very call created —
was hitting the same fallback, so the fix covers the class rather than
the tab case that surfaced it.
…target

fix(desktop): send a skill's kickoff into the tab that invoked it
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…us-tab

Open a tab from the sidebar "+" when a chat is already loaded
… the chat

While the workspace pane shows a full page (Artifacts, Skills, Messaging, a
plugin route), a sidebar click on the ACTIVE session did nothing: onResumeSession
took focusOpenSession's `true` for the main-session branch as "already on
screen" and skipped the navigate, but fronting the workspace tab doesn't put the
chat back — the page is still routed. The user had to click some other session
and then the active one to get back.

focusOpenSession now reports WHICH surface it fronted ('main' | 'tile' | null),
and focusedSessionNeedsRoute decides: a tile never needs a route (its pane
renders the chat regardless), a main hit does while a page covers the workspace.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ick-active

fix(desktop): clicking the active session from a full page returns to the chat
…t-fix

# Conflicts:
#	apps/desktop/electron/main.ts
#	apps/desktop/src/app/chat/composer/hooks/use-composer-metrics.ts
#	apps/desktop/src/app/session/hooks/use-session-actions/index.ts
#	apps/desktop/src/components/assistant-ui/markdown-text.tsx
#	apps/desktop/src/types/hermes.ts
#	gateway/platforms/base.py
#	tests/test_tui_gateway_server.py
#	tui_gateway/server.py
@vashkartik
vashkartik requested a review from vectorcmd as a code owner July 27, 2026 01:10
@vashkartik

Copy link
Copy Markdown
Owner Author

Closing as superseded by the merged rebaseline/sync chain (#21 and #36) and the later upstream-sync PR #39. The eight conflict-resolution classes documented here were checked against current ace/patches: the update-turn lease, empty-truncation guard, prompt-response support, and related upstream/fork behaviors are present in the current tree. This historical head remains preserved at 4d47163; no branch/worktree was deleted.

@vashkartik vashkartik closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.