Six fixes for the wave-7 picker/input cluster:
DeepSeek -> "Deepseek") and left the gemini- branch's words lowercase
("Gemini 2.5 pro"). Vendor casing + parameter counts now applied after
title-case (GLM, DeepSeek, MiniMax, OpenAI, ERNIE, MiMo, BGE, VL, IT,
FP8, AI; 8b -> 8B, a3b -> A3B), and the gemini branch title-cases like
every other branch.
and was unreachable by keyboard (rows are highlighted, never DOM-focused,
so Radix's own ArrowRight never fires). The chevron is now visible on
every model row and ArrowRight (caret parked at query end) hands focus
to the highlighted trigger and opens its sub; ArrowLeft returns focus
to the search field. Consolidates #86968 + #104532.
-fast/-thinking/-preview ids to the base label. The tag now rides the
display name on every surface, and formatModelPillLabel no longer
doubles Fast for a -fast variant id.
all MoA presets were disabled: manual picks are sticky by design
(d595e636c8), but the virtual moa provider's catalog row disappears
entirely once no preset is enabled, so that one absence is
authoritative (moaPickRemoved) and the pick reseeds from the profile
default. Narrow moa-only exception — no general catalog diff.
token (nimb -> nimb*); none of the CJK routes can honour it (bigram and
trigram routes quote tokens so the star matches literally; LIKE has no
star wildcard at all), so CJK searches returned zero results. The star
is now stripped per token on the CJK path only.
measure() effect deps (stale measurements after toggling Inbox style)
and the card estimate undershot the four-line/wrapped-title worst case
(74px), painting rows over their neighbours on cold start. Card
estimate raised to the worst-case-covering 96px and the deps fixed.
Linux uses apple-touch-icon.png as the window icon and Nix requires it to
match the full-bleed launcher icon, so keep it full-bleed and point the
dev-only app.dock.setIcon at assets/icon-mac.png instead.
Dev runs replace the Dock icon with public/apple-touch-icon.png, which the
generator rendered full-bleed, so it drew ~24% larger than its Dock
neighbors. Render it from the mac-grid master like the icns targets.
On the 824 grid the plate matches peers, but the girl inside a white tile
with a ring read small; scale her 1.12x about the plate center for every
mac target.
A pending clarify card waits on its gateway clarify.request. When that
frame is lost the card sat as a disabled preview until the 300s timeout
with no hint of what went wrong.
After a 4s grace the card now asks the owner socket for
session.events.since, which re-delivers the session's open requests, so a
request the backend still holds parks and the card goes live. If nothing
turns up, single and batch cards show an inline notice (all locales)
pointing at Stop, and drop the dead Skip/Continue actions.
Since the questions[]-only schema (#95907) every single question is a
one-entry batch on both the tool args and the gateway wire, yet no test
exercised that shape (called out in #98645). Lock the behavior down at
both layers:
- unit: a one-entry batch renders the batch card (not a blank/spinner
single card) both with the wire already parked and when the request
lands after the tool row, and answers with the qid-keyed lock
- e2e: a SINGLE_BATCH trigger drives the real chain (composer -> gateway
-> agent -> clarify tool -> clarify.request -> renderer) through mount,
pick, confirm, and settle for questions.length === 1
The e2e mock's trigger routing also learns to scope its has-tool-result
guard to the answering turn's own question text: the existing any-tool-
result check would false-positive once a second scripted clarify shares
the conversation history.
Adapted to main: the mock server now lives in tests-js/scripts, and a batch
confirm answers with clarify.lock.
Electron booted from active-profile.json and ignored argv. hermes
desktop and hermes -p <name> desktop never appended --profile, so
the packaged app kept the stored profile.
Parse both --profile spellings before startHermes, persist that
name, and append the same flag on the packaged launch. A missing
flag does not change the stored profile.
Desktop paste/file attachments land in Hermes-managed staging dirs on the
GATEWAY (composer-pastes/ for large text pastes, attachments/ for dropped
files), but on the Remote SSH topology the workspace root (TERMINAL_CWD) is a
path on the SSH HOST - the two filesystems are fully disjoint, as the issue
thread confirms. Two gaps combined to reject every staged attachment with
"path is outside the allowed workspace":
- _resolve_path admitted only allowed_root + composer-paste roots, so a
gateway-staged attachments/ path was refused outright. Admit the
_CACHE_DIRS staging roots (attachments/, images/, cache/*, composer-pastes/)
via a helper that asks get_cache_directory_mounts - the gateway's OWN
payload is never a workspace escape, and the path-traversal and
credential-deny guards in _ensure_reference_path_allowed still run after.
Anything else outside the workspace stays blocked.
- composer-pastes/ was missing from _CACHE_DIRS, so its bytes never reached
the remote: ssh/daytona/vercel_sandbox sync via iter_sync_files ->
iter_cache_files, and to_agent_visible_cache_path only translates mounted
dirs - a paste attached on a fresh session dangled on the remote host.
Tests cover the disjoint-filesystem SSH topology end-to-end (text inlines,
binary renders the synced ~/.hermes path), the still-refused stranger path,
local-backend unchanged, and the composer-pastes mount+sync enumeration.
Consolidates PR #110387 by Finn763 (the _agent_staged_path guard widening and
the SSH-topology tests, adapted to the current _ensure_reference_path_allowed
ordering) with PR #103412 by ericmaddox (whose mapping insight is subsumed by
the _CACHE_DIRS entry, which fixes both the sync and the translation).
Co-authored-by: ericmaddox <ericmaddox@users.noreply.github.com>
The narrow-viewport overlay for a collapsed zone is `absolute inset-y-0` —
it starts at the viewport's top edge with its tab strip (SESSIONS | BOTS) as
the first child, so on macOS the strip slides under the traffic lights.
Docked zones already reserve that band (TreeGroup: useWindowControlsOverlap
-> paddingTop plus an absolute [-webkit-app-region:drag] spacer; top-edge
zones use usePanelTitlebar), but the overlay used neither.
Reserve it the same way: measure the native controls rect against the
overlay element and pad the strip below it, keeping the band a window-drag
target via the drag-region spacer. The overlay's data attribute now carries
the revealed pane id (it was a constant empty string), which the regression
test uses as its selector.
Salvaged from PR #110034 by Muhammed Emin Boydak (the overlap hookup, the
paddingTop + drag spacer, and the 34px-reservation test), adapted to the
current file (NO_PANE_GROUP import, per-pane data attribute).
Fixes#110033.
Model output can arrive carrying Gemini-style grounding citation markers:
private-use delimiters U+E200/U+E201 wrap a `citeturn<n>search<m>` id list
(U+E202 separates ids) - e.g. `\uE200citeturn0search11\uE202turn2search0\uE201`.
Desktop had no rule for that shape (CITATION_MARKER_RE only strips bare
numeric `[n]` markers), so the private-use code points painted as replacement
glyphs - the reported "triple bars" - and the `turn…search…` ids rendered as
literal prose, wrapping across table cells and obscuring the answer.
Add CITATION_TRANSPORT_MARKER_RE and strip it in rewriteProseSegment, the
same shielded path the numeric marker rule rides: inline code and math spans
split out first, so `$\sqrt[3]{8}$` and quoted marker text are untouched, and
the bare no-delimiter alternative only fires on the `cite` head so plain prose
and stray private-use characters (icon fonts) are left alone. A marker that
cannot be resolved to a source is dropped, never invented into a link -
matching the reporter's own expectation. Mid-stream flushes (closing U+E201
not yet arrived) are covered by the optional-tail shape.
Backend-side emission (which search provider leaks the markers into model
text) remains unisolated, as the report itself notes; the display-layer strip
is justified regardless.
Fixes#120587. No external PR existed (the catalog's linked PR #120592 is a
dead reference).
MarkdownLink nulled fallbackLabel whenever the link text matched the target
URL — exactly the bare-autolink case — so PrettyLink fell through to
urlSlugTitleLabel and rendered a host-only label (`ncpssd.org` for
https://www.ncpssd.org/), with the address readable only via hover/inspect.
The user could not read an address sent in chat.
The link's own text is always a legitimate fallback label; pass it through.
Labeled links are unchanged: their authored label already wins display, and
that shape (a label hiding the address) is PrettyLink's documented contract,
not a bug.
Salvaged from PR #38213 by Phantomthedog (the fallbackLabel change and the
localhost/example.com render tests), reworked to keep the change scoped to
the bare-autolink shape and reshaped into an end-to-end
MarkdownTextContent test alongside the existing session/filelinks suites.
A popped-out Browser window is a fresh renderer: no in-memory atoms cross
the window boundary, and no session ever pushes a rail scope into it (the
controller skips session/preview watching there), so its scoped previewTabs
view started empty and PreviewTilePane rendered null — the shell spawned but
never painted, matching the reported black window. Root cause is the
per-profile rail scoping from c996d1c088, not the GHSA window-open policy
the reporter suspected: the window/IPC handoff itself is fine.
Three coordinated moves in the store:
- adoptPersistedBrowserTab now reads every profile bucket (plus the
pre-scoping single-array shape; the old decode returned [] for bucketed
storage, so even the sibling URL sync was dead) and, when the tab is not
in this renderer's view, re-homes the view onto the OWNING bucket instead
of splicing the tab into 'default' — a splice would duplicate the popped
tab into the primary profile's rail. When the tab IS present (the docked
mirror on pop-out close), it adopts the newer URL/label as before.
- PreviewTilePane calls that adoption from a browser window when its tab is
missing from the view.
- The persist subscriber no longer echoes module-init emissions back over
storage: nanostores fires subscribe immediately, so every renderer used to
clobber its un-adopted record (a legacy single-array store was wiped
before pendingLegacyTabs could adopt it; a 'default'-only bucket store was
removed the same way), and the view is now seeded from the renderer's own
bucket — which also restores the primary profile's rail at boot, since
setPreviewScope('default') early-returns on the initial viewKey.
Salvaged from PR #120110 by finn763 (diagnosis + adoption mechanism +
creation-emission guard, re-homed onto the owner bucket to avoid the
duplicate-bucket write.
EOF
)
The unknown-id shortcut gave any unsaved draft a private OAuth jar, so a
pre-save cloud sign-in wrote persist:hermes-remote-oauth-conn-<id> while the
saved cloud connection reads the shared persist:hermes-remote-oauth. Send the
draft's kind/authMode with the request and only grant a private jar to remote
OAuth drafts; everything else falls back to the legacy jar.
The #99989 salvage threads {connectionId, label} through
oauthLoginConnectionConfig; the pre-existing #89529 test still asserted
the single-argument call shape. Update it to the two-argument contract.
Co-authored-by: Together <BorgWrightpcalac@outlook.com>
The cherry-picked test expected 'conn:<id>' but main's partition prefix is
'persist:hermes-remote-oauth-conn-<id>' (PR #99992 was written against an
older naming). Align the assertion with the shipped prefix.
Co-authored-by: Together <BorgWrightpcalac@outlook.com>
Settings → Connections lets a draft remote gateway be signed in BEFORE it
is saved. The login IPC carried only the URL, so resolveOauthPartition()
matched against the on-disk registry, found no entry, and fell back to the
legacy shared jar: the fresh session was invisible to the saved connection
(which reads its own per-connection jar, #92183), and in the same-host
setup it also evicted the other gateway's cookie in the shared jar.
The login IPC now carries the draft's identity. An explicit connectionId
wins; otherwise the main process mints the id the save will use
(connectionIdForPendingLogin, same derivation as normalizeConnectionInput)
and returns it so the editor pins the id into the draft. The resolver
resolves a named connection by identity: a known entry follows the
existing rules; an unknown id is a pending non-primary oauth remote —
editor saves never promote a fresh entry to primary — and gets its own
partition up front.
Fixes#99989
(cherry picked from commit 054a7af5e49403660c29c152440c0d3bb46d3871)
A republished connection reply carried only the cached backend descriptor,
whose getWindowState() spread was baked in at process cold start
(startHermes caches the backend for its lifetime). A window that entered
fullscreen after the first dial got a stale isFullscreen: false on every
republish — reconnect, sleep/wake, gateway switch, backend restart, ⌘R
reload — so the renderer's live fullscreen flag was overwritten and the
titlebar's traffic-light inset reverted to the windowed offset while still
fullscreen. Toggling fullscreen fired a live push again and "fixed" it,
which is exactly the intermittent behavior reported (#102451).
Read the calling window's state when the reply is built, via the new
connection-window-state helpers (liveWindowState/overlayWindowState), so
every shape connectDesktopProfileRoute returns — registry-scoped,
primary-resolved and bare — carries live values consistent with the
hermes:window-state-changed live-push path. Both connection IPC handlers
now pass their sender through; without it the lookup silently falls back
to mainWindow, so a secondary window would be told the primary's chrome —
the same defect for secondary windows that the primary had for stale
snapshots.
The secondary-window fullscreen leak (bindWindowChromeEvents) is already
fixed on main (window-chrome-events.ts threads the bound window through
sendWindowStateChanged), so this is the remaining surface.
Tests: connection-window-state.test.ts covers sender-window state reads,
primary fallback, destroyed/no-window degradation, and the overlay
behavior on all three reply shapes (DI over BrowserWindow.fromWebContents
and getWindowState — no source-text assertions).
Fixes#102451
Co-authored-by: ryrenz <163799701+ryrenz@users.noreply.github.com>
session.info heartbeats mirror the runtime's resolved model/provider (e.g.
the generic `custom` billing class a named provider resolves to) into the
view through setCurrentModel/setCurrentProvider, which also persist to the
composer's sticky localStorage selection. Every heartbeat therefore
overwrote the user's actual pick, so a later new chat followed the
last-seen runtime class instead of the selection or the Settings default.
Add setCurrentModelTransient/setCurrentProviderTransient (mirroring the
existing setCurrentCwdTransient pattern) and use them in
syncRuntimeMetadataToView, which is the only caller reached from periodic
heartbeats rather than an explicit user pick.
Fixes#102793
(cherry picked from commit d8049b4ba40a59becac59a8f8ccbddcf8b9d58cc)
The failed/cancelled state of the first-launch install overlay only
offered "Copy output" and "Reload and retry" -- retry clears the
latched failure and reloads, re-entering bootstrap. There was no way
to back out of a deliberately cancelled install short of quitting.
Add a Close button (and Escape) that dismisses the overlay locally,
without touching main's bootstrap state or reloading, so the app
falls through to whatever view sits behind it.
(cherry picked from commit 68dffe3db385735cedc26559f0bac695b20bf42a)
Use Electron 40's event-object console-message contract, retain one-argument listener arity, and report malformed signature drift once so renderer logging cannot fail silently.
Co-authored-by: CupaJ12 <108900676+CupaJ12@users.noreply.github.com>
(cherry picked from commit 378701b2b7af1e0a3accfaec7b8daa46c30832d6)
Desktop rebuilds the AIAgent per turn (idle reap -> next message re-mint),
and agent_init.py gives every rebuilt agent a brand-new, empty _credits_latch
via new_credits_latch(). seed_credits_at_session_start() -> _hydrate_seed_state()
then unconditionally primes latch["seen_below_90"] on that fresh latch and
evaluates once -- correct for a genuinely new session opening mid-band, but on
a reap/resume rebuild it makes evaluate_credits_notices() see
shown_band=None vs. current_band=<the same band as before>, so it re-fires
"You've used $X of your $Y cap" on every message even though the user already
saw that exact notice moments ago on the previous incarnation of the same
session (#101578).
agent.session_id is stable across these rebuilds even though the agent object
and its latch are not, so add a small process-lifetime cache
(_seen_usage_bands, bounded to 500 entries, MRU eviction) keyed by session_id
that remembers the last usage_band actually shown. _hydrate_seed_state()
restores it into the fresh latch before evaluating, so a rebuild with unchanged
usage stays quiet, while a rebuild after a genuine crossing (recorded via the
same warm-path write in rate_limit_credits._emit_credits_notices, the single
chokepoint both the seed and live-header paths share) still fires normally.
Deliberately not persisted anywhere durable -- a real process restart is a real
"session open" and should still warn immediately, matching the existing
cold-start seed behavior for a session that opens already in a band. An agent
with no session_id (plain CLI, never rebuilt) falls back to the pre-fix
behavior unchanged.
Tests (tests/agent/test_credits_cold_start.py): a rebuild with the same
session_id and unchanged usage does not re-fire; a rebuild after a genuine
band change still fires (and clears the old key); a different session_id is
never suppressed by another session's history; an agent without a session_id
degrades to the old always-prime behavior without raising.
Fixes#101578
Co-authored-by: Edizzier <umit.ediz@hotmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
A Desktop Docker session that registers an absolute host directory
such as /mnt/... or /srv/... as its cwd skipped the /Users|/home/
drive-letter heuristic, so the mount check never ran and commands
were wrapped with cd to that host path. Classify the mounted host
directory as unusable before that heuristic, and remap it to
/workspace in the live-env write and the per-command resolver.
The scoped clear deleted only the root's own key from each cache, but this
PR's listing pass now keys gitRootCache on every listed directory and
nestedRepoCache on every entry the ignore rules probe — all strict
descendants of the root, never the root key itself. The refresh paths in
use-project-tree therefore served stale nested-repo and git-root answers:
git init inside a parent-ignored directory stayed hidden until a full
no-arg clear (connection change or relaunch).
Evict every key at or under the root, for all three caches, matched on a
path boundary via isUnderPath so /repo does not evict /repo2, and only
within the current connection's cache keys. Regression test covers the
refresh-makes-it-visible case and the sibling-root boundary.
The drain-exhaustion drop treated 'hint lookup returned undefined' as
'the session is gone', but getSessionOwnerHint also returns undefined for
TWO OR MORE routes (cloud gateway plus local backend) — a positive
liveness signal misread as absence. Use the plural getSessionOwnerHints
(≥1 route keeps the entry), and refuse to drop while the loaded list page
cannot prove absence either ($sessionsLoadError, or any profile in
$sessionProfilesTruncated — the sidebar list is one page, so a session
below the fold is unknown, not deleted). 'Maybe' never deletes; the
queued prompt stays for a manual send.
A prompt left in the composer queue when the app exits is drained after
relaunch against a session runtime that no longer exists. The submit
path already resumes by stored id (#106986 gated the drain on session
discovery for exactly this), but when the stored resume itself refuses
(deleted from another surface, backend restart rejecting), the drain
retried MAX_AUTO_DRAIN_ATTEMPTS times, showed the queueStuck ERROR
banner ('message not sent' — reading as data loss), and because the
failure counter was process-local while the queue persists in
localStorage, every subsequent launch replayed the whole cycle.
- Persist the per-entry drain-failure budget with the queue
(QueuedPromptEntry.drainFailures): the retry ladder is unchanged
within a process, but an exhausted entry is skipped after restart —
no attempts, no banner. Queueing a fresh prompt lifts the budget off
the session's entries, and a manual send from the queue panel clears
it (clearQueuedPromptDrainFailures), mirroring the composer's
in-process counter reset on the same gestures.
- At exhaustion, distinguish gone from known (discovery has settled,
so the loaded list plus owner hints are authoritative): a session no
row or hint answers to can never accept the queued prompt — drop the
entry and notify quietly (info, new queueDropped strings). A session
that still exists keeps its entry (real data, manually sendable) and
gets the queueStuck notice downgraded from error to info.
Regression tests: gone-session drop with a quiet notice, known-session
retention, and a restored-after-exhaustion queue that neither attempts
nor notifies. i18n: queueDroppedTitle/Body added to all nine locales.
Closes#98015.
Implementation from the issue reporter's live-verified v3 patch
(MichaelZelbel), posted on the tracker after their first patch (and PR
#101115) proved unshippable: the readDir bridge strips `.git` entries
(FS_READDIR_HIDDEN in electron/fs-read-dir.ts), so any implementation
that probes directory listings for a `.git` entry passes mocked tests
and does nothing in production.
Two changes to filterIgnored:
- Detect nested repo/worktree roots via the existing gitRoot bridge
(git rev-parse --show-toplevel): a directory is its own repo's root
when gitRoot(dir) resolves to dir itself. Covers worktrees (a .git
file) for free, works even though listings never show .git, and only
probes entries the parent's ignore rules would actually hide.
- Re-anchor the ignore-rule chain at the nearest git root of the LISTED
directory (falling back to the project root): git applies ignore
rules only inside their own repository, so a parent's `dev/*` must
not empty a nested repo one level down — only that repo's own
.gitignore chain governs.
Probe results are cached like the gitignore reads and cleared with the
same cache-invalidation path. Coexists with the per-project
showIgnoredFiles opt-in (checked first — it skips the whole pass).
Tests: nested clone, worktree-style .git file, browsing inside a
nested repo, plus the existing suite.
Closes#99861.
Salvage of PR #99467 by Jean-Dominique Vidjanagni (DomVidja, the issue
reporter), rebased onto current main.
After auto-compression rotates a session's stored id, tiles stayed
keyed on the pre-rotation segment while the rest of the app (selection,
route, composer) moved to the new tip — the same conversation rendered
as two tabs (identical or differently titled, since each tip is titled
independently), while state.db holds one row per lineage.
- rekeySessionTile(previousId, nextId, runtimeId?): re-home any open
tile keyed on the pre-rotation id to the new tip, preserving dir/
anchor/before placement so the pane mirror re-docks in the same slot.
Main-vs-tile reconciliation is scoped to the same Sessions workspace/
owner; Bot tiles and distinct-backend-route tiles stay independent
surfaces. When the new tip is already the main selection the stale
tile is dropped (main OR tile — never both). A rotation for a
background runtime re-keys the persisted profile bucket owning it
without replacing the active profile's atom.
- Called from handleTransition AND the cache-path rotation in
useSessionStateCache (ungated on the active runtime — a background
tile's conversation rotates too).
Rebase notes: main's stricter #97511 owner-provenance semantics
(sessionOwnerByRuntimeId as the LAST rung of knownOwnerForSession,
secondaryProfileOwnerForEvent fail-closed) are kept; the PR's runtime-
owner-first chain was dropped in favor of it. Regression coverage
covers the background-runtime re-home, the cross-profile persisted
bucket, main-selection drop, detach preservation, and cache-vs-
route-follow ordering.
Closes#98622.
- Gate `$sessionProfilesTruncated` subscription behind the Sessions tab
with `useStoreSelector` and a stable empty object, matching the
existing re-render discipline for `$sessions` / `$pinnedSessionIds`.
- Show the real page size in `SidebarLoadMoreRow` so the label reads
"load 50 more", consistent with the sidebar.
- Hide the load-more affordance when `onLoadMoreSessions` is not wired.
The Command Center Sessions tab renders the shared $sessions store, which
is paginated by the sidebar with a default window of 50 rows. The sidebar
exposes a 'load more' control (SidebarLoadMoreRow + bumpSessionsLimit) to
grow that window, but the Command Center has no equivalent, so sessions
beyond the first page are unreachable from it.
Reuse the existing sidebar affordance: accept an optional onLoadMoreSessions
prop (wired to loadMoreSessions at the call site), subscribe to
$sessionProfilesTruncated, and render SidebarLoadMoreRow in a fixed bar
below the scrollable Sessions list (bottom-right, outside the scroll
container so it stays put while scrolling). Clicking it bumps the shared
limit and refetches, so both surfaces stay in sync.
Salvage of PR #99679 by 686f6c61, rebased onto current main.
/new and idle/daily resets keep parent_session_id for durable lineage,
and the sidebar's flattenSessionsWithBranches nested on that field
alone — so a platform's chats collapsed into one growing nested chain
of branches even though none of them were /branch forks. The backend
records the distinction on disk already (model_config._reset_from vs
_branched_from, gateway/session_recovery.py:433) but list payloads
strip model_config before any UI can read it.
- forkParentId(): nest only genuine forks — _branched_from wins, a
_reset_from parent means top-level sibling, legacy/optimistic rows
with only parent_session_id keep nesting.
- _session_row_dict lifts _reset_from/_branched_from out of
model_config so compact list rows (which strip that heavy field)
carry the distinction; tui_gateway project-tree rows project the two
markers too.
- Optimistic desktop /branch rows stamp _branched_from so the flat
render stays correct before the authoritative row arrives.
Disk lineage is unchanged. Tests: a _reset_from chain renders flat,
a genuine fork still nests beside a reset sharing the same parent,
and list_sessions_rich promotes both markers (also under
compact_rows=True).
Closes#99648.
Salvage of PR #98475 by kokhlo, rebased onto current main (deps array
re-merged after display.resume_last_session / disk-plugins landed).
The cleanup effect in useDesktopIntegrations drops the remembered
navigation once a bounded resume retry exhausts, but the persistence
effect re-runs on ordinary session-list refreshes with no idea the
session is stranded — it writes the dead routed id straight back into
hermes.desktop.lastSessionId/lastRoute, so every relaunch reopens the
resume-error screen until the user manually hits New chat.
Treat resumeExhaustedSessionId as a write barrier in the persistence
effect: skip the setRememberedSessionId/setRememberedRoute writes while
the routed id is the exhausted one, and add the latch to the effect's
deps. Regression test covers the routed-at-the-dead-session shape plus
a later session-list refresh that re-runs only the persistence effect.
Closes#98467.
Salvage of PR #102346 by teamster22, rebased onto current main.
The sidebar fetch splits its rows into three mutually exclusive
source-scoped slices ($sessions recents / $cronSessions /
$messagingSessions), and recents excludes every messaging and cron
source. All three tab-title resolution sites (tileStoredRow,
syncWorkspaceTitle, workspaceDragPayload) searched recents only, so a
telegram/matrix/discord-origin session tab read "New session" forever
even while its sidebar row showed the real title — a partition miss,
not a timing miss, so no activity could ever fix it.
Route the three resolvers through ownerLookupSessionRows() (the
three-slice concat added for this bug class in #95633, which preserves
$sessions' array identity when the other slices are empty) and add
$cronSessions / $messagingSessions to watchSessionTiles' `also` array
and syncWorkspaceTitle's listeners so the late-arriving slices re-sync
the label.
Closes#95096.
Checkpoint refresh treated a failed start-time check as a collected exit
and queued a completion. A live PID whose start time matches, or whose
start time cannot be read, stays running. A reused PID is closed without
being signalled. A gone PID is pruned. A completion is emitted only when
an exit status was collected.
Review of the run-history fallback:
- Merge agent sessions with script-only output docs per execution instead of
branching on 'any sessions exist': a job converted to no_agent keeps its id
(update_job supports it), so a surviving historical agent session must not
hide newer script-only fires. Same-execution rows dedupe by session span.
- Decode output-doc filenames and read the execution ledger inside the owner
profile's home scope: hermes_time resolves the configured zone through the
current HERMES_HOME, so a cross-profile request decoded with the
dashboard's zone and read the dashboard's executions.db.
- Attach per-run status from the execution ledger (one row per fire) matched
by claim window instead of last_run_at proximity, and surface terminal
attempts with no surviving doc as their own rows — the latest failed run
no longer disappears behind (or relabels) an older successful document.
- Read output docs with encoding='utf-8-sig' (Windows footgun: PowerShell and
some editors BOM files; plain utf-8 breaks on BOM-prefixed docs).
Three regressions cover the converted-job merge, the per-profile timezone
decode, and the missing-newest-doc case.
The cron detail rendered only the prompt, so script-only (no_agent) jobs —
which have prompt: "" — showed a blank description; the script field, the one
meaningful detail, was never displayed. The detail now shows a muted 'script'
badge next to the state pill and renders the script under a Script label when
the prompt is empty. Output-doc run rows (source='cron_output') have no backing
session, so they render as plain rows without a chat-navigation affordance in
both the detail run history and the sidebar quick-peek.
Fixes#42433 (blank-detail and script-badge facets)
Salvaged from #77382 by @andrexibiza (jobDescription model + detail badge/label
and tests), adapted onto current main and extended to the sidebar peek.
Co-authored-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Script-only (no_agent) jobs deliberately skip SessionDB, so their run history
was permanently empty — the desktop showed "No runs" next to hundreds of
completed fires. When no session rows exist, the runs endpoint now falls back to
the job's output docs under cron/output/<job_id>/, one row per fire, with the
latest status prefixed; with no surviving docs but a recorded last_run_at, a
single metadata row is surfaced instead. Rows mirror /api/sessions shape with
source='cron_output' and a cron_output: id prefix that cannot collide with
SessionDB cron_{job_id}_* session ids.
Filename timestamps are read back with hermes_time.get_timezone() — the same
configured zone save_job_output writes them with — not a fixed-offset snapshot
of today's local offset (wrong by hours when HERMES_TIMEZONE differs from the
server zone, and by an hour across DST).
Fixes#42433 (run-history facet)
Salvaged from #61403 by @LeonSGP43 (fallback design, metadata-only row, tests),
reworked per its review: timezone handling uses the configured zone and the
tests pin started_at under a configured-zone mismatch.
Co-authored-by: LeonSGP43 <cine.dreamer.one@gmail.com>
Skill slashes that miss the 4018 gate print the loading banner and park
the prompt on unread _pending_input. Refuse before process_command, and
return command.dispatch's skill payload or a hard error when the skill
scan raises so fail-open cannot drop the turn.