Commit Graph

28243 Commits

Author SHA1 Message Date
Teknium 21d3e63702 fix(compression): watermark commit — appends flow freely, concurrent tail survives compaction
Redesign of the #75316 class (supersedes the approach in PR #87307).

Root cause family: the compression lock fenced ORDINARY transcript appends
for the whole slow provider-summary call. Turns died as
session_persistence_failed whenever a message overlapped a compression
(#74568, #77386, #75083), stale dead-PID locks blocked writes for the full
TTL, and the busy-wait mitigation (#75264) was an order of magnitude shorter
than real summaries. Separately, the commit archived from a pre-call
snapshot, so rows appended mid-compression were swept into the archive.

Design: the commit transaction is already exclusive — no lock phases needed.

1. Appends never check compression_locks. The lock's only job is stopping
   two compressions colliding; it keeps that job. The whole stale-lock /
   busy-wait symptom family dies as a class.
2. Watermark captured in the DB at compression start
   (get_active_message_watermark = MAX(id) of active rows) — not from
   in-memory message dicts, which carry no row ids in production.
3. archive_and_compact(watermark=, lock_holder=): one transaction verifies
   the holder still owns an unexpired lease (a reclaimed lease cannot
   publish a stale compaction), archives the snapshot, inserts the compacted
   set, and re-sequences the concurrent tail (id > watermark) via a
   pure-SQL column clone — every column except id survives byte-exact
   (api_content, platform_message_id, reasoning sidecars, token counts),
   FTS triggers index the clones naturally, originals stay archived and
   recoverable. watermark=None preserves the historical behavior.

Removed: the append-side compression fence in _check_transcript_write_guards
(with rationale note), making the _COMPRESSION_BUSY_WAIT_S retry lane
unreachable from append paths (kept for other callers).

Tests: 12 new (watermark contract, column-exact clone, commit fence incl.
lease-lost/expired/rollback failure injection, append-vs-commit race);
busy-retry suite flipped to pin the new contract; sabotage-verified (5 fail
with the watermark disabled, 12 pass restored); E2E through the real
compress_context seam with a mid-summary append landing and surviving.
2026-08-15 23:37:22 -07:00
Teknium 128fb74d98 chore: map contributor email for 5Hyeons 2026-08-15 23:37:15 -07:00
5Hyeons 9975180a59 fix(mcp): handle DCR clients with secrets across all OAuth paths
Some MCP OAuth providers (notably Supabase) return a client_secret from
dynamic client registration but omit token_endpoint_auth_method. The MCP
SDK defaults the missing method to "none", so the token exchange omits
client_secret and the server rejects it (HTTP 422 "Required parameter:
client_secret"), looping the browser consent page.

This resolves the whole class, not just one provider:

- Storage layer (HermesTokenStorage): coerce secret-bearing client info
  with missing/none auth method to client_secret_post on both read and
  write, persisting the corrected shape.
- Both live provider paths (tools/mcp_oauth.py HermesOAuthClientProvider
  and tools/mcp_oauth_manager.py HermesMCPOAuthProvider): coerce
  in-memory client info immediately before token exchange and refresh.
- Accept the full 2xx range on token and refresh responses (Supabase
  returns 201 Created), instead of the SDK's exact-200 check.
- Redact token response bodies from error messages and logs on
  malformed responses.

The Figma-specific request-time default (apply_oauth_provider_defaults)
remains; this generalizes the same bug class for every DCR provider.

Fixes #29680. Supersedes #34274 and #35700 (201-only variants).
2026-08-15 23:37:15 -07:00
Teknium 411903b6fa chore: contributor mapping for NikolaRHristov 2026-08-15 23:03:52 -07:00
Teknium 20fcc11a0c refactor(hooks): share fail-closed approve logic; update sibling tests
Follow-up to the #28953 salvage:

- Extract _resolve_block_from_details() so resolve_pre_tool_block and
  _dispatch_pre_tool_call_hooks share ONE fail-closed approval-gate
  implementation. This also gives the new dispatcher the observability
  context wrapping around request_tool_approval that the original PR's
  inlined copy lacked.
- Update sibling tests that patched resolve_pre_tool_block at the three
  migrated dispatch sites to patch _dispatch_pre_tool_call_hooks with the
  (block_message, modified_args) tuple contract.

Verified: 448 targeted tests green; E2E with a real shell hook in an
isolated HERMES_HOME rewrote a live write_file call (path + content)
through handle_function_call, with block and negative paths intact.
2026-08-15 23:03:52 -07:00
Nikola Hristov d083b85591 feat(hooks): pre_tool_call content transformation via modify directive
Adds a `modify` response type to pre_tool_call hooks so a hook can
transform tool arguments before the tool executes, instead of repairing
results afterwards via post_tool_call.

- hermes_cli/plugins.py: _dispatch_pre_tool_call_hooks() fires hooks once
  and returns (block_message, modified_args); modify directives
  shallow-merge into an accumulated dict built from the original args.
- agent/shell_hooks.py: _parse_response() accepts both the canonical
  {"action": "modify", "args": {...}} and Claude Code-compatible
  {"decision": "modify", "tool_input": {...}} wire formats.
- model_tools.py, agent/tool_executor.py, agent/agent_runtime_helpers.py:
  dispatch sites migrated; modified args applied before execution.
- Docs + 10 new tests (merge semantics, precedence, block interplay).

Salvaged from PR #28953. Best fix for #18988.
2026-08-15 23:03:52 -07:00
Teknium f3bf718a62 feat(desktop): drag-resizable panes on the Capabilities Skills tab
The Skills tab's three panes are now all drag-resizable:

- List/detail column seam: MasterDetail grows an optional resizeId that
  turns the seam between the rail and the detail pane into a vertical
  drag sash (same visual language as DetailPane's top-edge sash). The
  rail width persists in the shared pane store under that id;
  double-click resets to the default 0.75fr track. Skills and Tools
  tabs share one id so the split stays consistent across tabs.
- Skills Hub section: the embedded hub picker's top edge is now a drag
  sash — pull the hub pane up to grow it (the skills list above
  absorbs the change). Height persists through the same pane store,
  double-click resets, and the cross-origin iframe gets
  pointer-events:none during the gesture so it can't swallow the drag.
  Replaces the old native CSS corner-resize handle.
- The skill editor bottom pane already resized via DetailPane's sash.

MASTER_DETAIL_WIDE_COLS keeps its exported shape (MCP tab reads it) —
the --md-split var falls back to the declared track when unset, so
grids without a sash render exactly as before.
2026-08-15 22:58:30 -07:00
Teknium 9b9dd6eeab fix(computer-use): zero-rect AX bounds serialize as unknown, not a position
KDE/Qt apps report [0,0,0,0] bounds for elements that are perfectly
clickable by index (live QA: all 50 of kcalc's zero-rect elements,
including every radio button). Serializing that as a plausible rect
invites a model to derive coordinate=[0,0] and click the screen corner.

- _element_to_dict: zero rect -> bounds: null
- _format_elements: '@ bounds-unknown (click by element index)' instead
  of the fake rect in the summary line
- malformed bounds fail open (unchanged serialization)

Live-proven on real kcalc (cua-driver 0.20.0): 50 elements now null, 0
zero-rect leftovers, summary annotated, real rects preserved, and a
null-bounds radio button still clicks fine by index.
2026-08-15 22:36:52 -07:00
Teknium 1d5fc2bc0b feat(config): wire compression.tail_mode + docs (en/zh)
#87326 shipped the lean-compaction capability on the compressor; this adds
the config.yaml surface (compression.tail_mode: legacy|lean, default
legacy), DEFAULT_CONFIG entry, and docs on both the dev-guide compression
page and the user-guide configuration page, with zh-Hans parity.
2026-08-15 22:36:31 -07:00
Teknium 4a9411cf7f Merge pull request #87326 from NousResearch/feat/compaction-eval-and-tail-policy
feat(compression): lean tail mode + compaction recall eval harness
2026-08-15 22:22:39 -07:00
Teknium b7f6280259 fix(computer-use): refuse wrong-window input on app= mismatch; hint near-miss actions
Live complex-action QA on a real KDE desktop (kcalc + kate multi-app
flows) found two dispatch gaps:

1. Wrong-window input reported as success. Input actions deliver to the
   backend's sticky target (last capture/focus_app); the app= argument
   models routinely pass on the input call itself was silently dropped.
   Proven live: with kcalc sticky, type(text='777', app='kate') returned
   ok:true and typed 777 INTO KCALC. New guard: provable mismatch
   (both names known, neither substring of the other — list_windows
   names are localized/variant) refuses with input_target_mismatch and
   a one-call fix instruction. Unknown current target fails open so
   legacy no-app flows are untouched.

2. Near-miss unknown actions were dead ends. A model emitting 'hotkey'
   got a bare unknown-action error. Suggestion map now names the real
   action ('did you mean key?') without aliasing — we never repair bad
   model output, we just point at the schema.

Also documents the verified-lost-keystroke rung in the computer-use
skill: KTextEditor (Kate/KWrite) discards synthetic X keystrokes at the
toolkit level — foreground type reports ok but AX shows nothing arrived,
and a raw XTest control fails identically outside our stack. Guidance:
after one verified-lost round trip, switch to file/DBus I/O instead of
looping the ladder.

Live proof on the fixed build: mismatch refused, kcalc display clean,
same call after capture(app=kate) succeeds, 'hotkey' suggests 'key'.
11 new tests; 158 sibling tests green.
2026-08-15 22:18:09 -07:00
Teknium b7790d91a6 merge: refresh onto current main (take main's superior sequential-timeout de-flake) 2026-08-15 19:11:57 -07:00
Teknium 0ee5ae61e2 docs(evals): real Codex CLI head-to-head arm + results
scripts/codex_arm.py drives OpenAI Codex CLI end-to-end on the same
transcripts: chunk-file reads until its REAL auto-compaction fires (verified
via compacted events in the rollout jsonl; peak 455-483K vs its 258K
window), then quizzes post-compaction with the identical question banks and
judge. Results (results/codex-arm-2026-08-15/): codex 36.7% avg vs lean
closed-book 40.0% vs lean+recovery 68.3%. Codex has no runtime re-access
over its rollout history — the session_search differentiator, measured.
2026-08-15 18:41:24 -07:00
Teknium 460d345642 fix(computer-use): flag macOS zero-display capture in doctor + discovery reason
A headless Mac or asleep built-in panel leaves ScreenCaptureKit with 0
shareable displays while TCC grants pass — health_report stays ok and
every capture silently returns 0x0 (#67165). Guard at the report seam
(_apply_display_count_guard, both real and fallback paths): flips the
screen_capture_capability check to fail with recovery actions (wake
display / HDMI dummy / virtual display) and downgrades ok -> degraded.
The empty-discovery reason ladder gains the matching darwin rung.

Composed from #52949 (sujeet111) and #67259 (webtecnica); both PRs
predate the doctor rewrite and the envelope normalization on main, so
this reimplements their shared intent at the current seams.

Co-authored-by: Sujeet <64351924+sujeet111@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-08-15 18:20:55 -07:00
0xGr1mm d303e18d09 fix(computer_use): ignore placeholder pid/window_id ids
capture() read any non-None pid/window_id as a request for exact-window
targeting. Several providers emit every declared schema property on every
tool call, zero-filling unused optional integers, so those calls arrive as
pid=0, window_id=0. The exact-target branch was then entered, the caller's
app= was discarded, _positive_int(0) returned None for both ids, and the
capture failed with a message pointing at pid/window_id. For that class of
model capture(app=...) and frontmost capture never worked at all.

Normalize non-positive ids to None before the branch decision so dispatch
falls through to app/frontmost discovery. Malformed non-numeric ids are
deliberately not treated as placeholders: they still reach the existing
validation error instead of being silently ignored.

Fixes #81333
2026-08-15 18:20:55 -07:00
Teknium 61f2738205 test(tool-executor): close the second race — deadline vs middleware preamble
The first hardening (deterministic worker start) still went red on its
own PR's CI: the 0.1s deadline can ALSO expire between worker start and
handle_function_call — argument parsing, hooks, and cold imports run in
that window, and the timeout interrupt then returns the middleware
without dispatching (first_started unset; observed 0.15s tool error vs
0.1s budget in the slice-4 log).

Deadline raised to a 1.0s floor: still fast, ~7x the worst observed
preamble, and the submit() sync from the first commit keeps the
countdown anchored to worker start. The clarify human-wait test's sleep
rises to 1.3s so it still outlasts the deadline (the relationship the
test exists to pin). Sabotage v2 injects BOTH races (0.3s thread start
+ 0.5s preamble): hardened passes, un-hardened reproduces the exact CI
failure.
2026-08-15 18:15:57 -07:00
Teknium d24d294421 test(tool-executor): deterministic worker start kills sequential-timeout flake
test_sequential_tool_timeout_emits_result_and_continues failed twice in
two days on unrelated computer_use PRs (slice 4, xdist): assert
first_started.is_set() -> False. Root cause: the sequential timeout path
computes deadline = now + timeout_s right after executor.submit(); with
the test's 0.1s deadline, a loaded CI worker can take longer than the
whole deadline just to START the pool thread, so the future is cancelled
before the tool ever dispatches.

Fix: an autouse fixture subclasses DaemonThreadPoolExecutor so submit()
blocks (bounded 10s) until the worker callable has begun — the deadline
now races the tool, not the thread scheduler, which is what these tests
mean to pin. Timeouts stay tight (0.05s), so the suite stays fast.

Sabotage-proven: a 0.3s injected thread-start delay reproduces the exact
CI failure without the fixture and passes with it.
2026-08-15 18:15:57 -07:00
Teknium 933ef69470 feat: session picker lifecycle status + delete 2026-08-15 18:15:22 -07:00
Teknium 99500aca11 fix: guard breadcrumb writes for bare test fakes; fold session category into general
- cli.py breadcrumb call sites use getattr so object.__new__/SimpleNamespace
  test fakes without the mixin method don't AttributeError (pitfall 17)
- web_server: fold one-field 'session' category into 'general' per the
  single-field-category test contract
2026-08-15 18:15:18 -07:00
Teknium d6f02e3496 feat: per-terminal --continue via terminal breadcrumbs 2026-08-15 18:15:18 -07:00
Teknium 04c61f2949 feat: import and resume Claude Code / Codex CLI sessions 2026-08-15 18:15:13 -07:00
Teknium e2990428e7 docs(evals): ship transcript-building scripts + full eval detail in-repo
- scripts/reconstruct_lineage.py: rebuild full uncompacted lineage
  transcripts from a state.db COPY (descendant-tree walk, content-hash
  dedupe, synthetic-artifact strip, system_prompts hash resolution)
- scripts/replay_lineage.py + scripts/build_html_report.py: replay a 500K
  prefix through any checkout's compressor and render before/after
  side-by-side with compaction artifacts color-coded
- README: transcript-building workflow, scoping-tripwire section
- results/SCORECARD-2026-08-15.md: full per-transcript scorecards, all 4
  exam question banks, survival analysis, methodology + caveats
2026-08-15 17:40:43 -07:00
Teknium a2cc867870 fix(tests): de-flake sequential-tool-timeout race on loaded runners
test_sequential_tool_timeout_emits_result_and_continues asserted
first_started.is_set() immediately after the executor returned — but on a
starved CI runner the 0.05s tool timeout can expire before the hung call's
worker THREAD is even scheduled, and the timeout path's future.cancel()
then legitimately prevents that dispatch entirely. That is the exact
behavior under test (executor refuses to wait), not a failure.

The test now asserts the real contract: executor returns fast, the NEXT
tool actually dispatched, and the message stream carries both call ids in
order with the timeout result first. Verified both ways with a
thread-start-delay sabotage run: old assert fails under delay, new
assertions pass under delay and still fail if the executor stops
continuing past a hung call.
2026-08-15 17:24:18 -07:00
Teknium d5773bfc3a feat(desktop): Skills tab hub browser + full-skill detail pane; drop Browse Hub tab; SkillsView SDK export
Consolidates the Capabilities view around the Skills tab and opens the
whole surface to plugins:

- Skills tab: the embedded Skills Hub picker now renders BELOW the
  installed-skills list, expanded by default, with the update-all action
  in its header. "+ Add to this Agent" picks are refused with a toast
  when the skill is already installed in the scoped profile (name and
  identifier both checked against the unfiltered list).
- Detail pane: shows the ENTIRE skill for any provenance — frontmatter
  metadata rendered as key/value rows plus the full SKILL.md body —
  via the existing GET /api/skills/content (new getSkillContent fetcher,
  profile-scoped, cached per skill+scope).
- Browse Hub top-level tab removed (hub.tsx deleted, tabHub i18n keys
  dropped); the hub lives inside Skills now. Legacy ?tab=hub URLs fall
  back to Skills via useRouteEnumParam. Hub actions/store and all hub
  REST fetchers are unchanged.
- SkillsView gains `embedded` (tab state local to the component instead
  of the route ?tab= param) and `fixedProfile` (pins every tab to one
  profile; the scope selector hides and the profiles roster fetch is
  skipped) and is exported from @hermes/plugin-sdk — so Hermes-Bot-Mode
  can render the real Capabilities surface inside its create/edit agent
  Advanced section pinned to a bot.
- i18n: hub.alreadyInstalled (en + zh; others fall back).

Tests: index.test.tsx 8/8 (new: full-skill detail pane renders
frontmatter+body; picker refuses already-installed picks and is expanded
by default); toolset-config-panel 28/28; hermes-parity 11/11. Typecheck
(3 tsconfigs) + eslint clean.
2026-08-15 17:15:44 -07:00
Teknium 95aa709606 fix(computer-use): diagnose empty window discovery; fail fast on dead-daemon CLI fallback
Two live-QA findings from a locked KDE desktop (real cua-driver 0.20.0):

1. capture() with zero discovered windows returned a bare
   'capture mode=ax 0x0' — no hint that the desktop session was LOCKED,
   which freezes renderers and hides windows. New
   _empty_discovery_reason() names the dominant causes in order: locked
   session (loginctl LockedHint probe, fail-safe), missing DISPLAY,
   else a pointer at hermes computer-use doctor. Surfaced through the
   existing window_title -> summary path, so the model and the user see
   it inline.

2. _call_tool_via_cli retried 'daemon is not running' 4x with ~3.5s of
   backoff sleeps — a permanent condition for that invocation (the CLI
   transport needs the machine-wide daemon; Hermes' MCP runtime does
   not). Now fails fast on the first attempt with a message naming the
   split. Transient empty output (EAGAIN congestion) keeps the retry
   loop — pinned by test.

Live-verified on the locked desktop: capture now reports the lock and
the unlock action; CLI fallback errors immediately with the transport
explanation. 7 new tests; 191 sibling tests green.
2026-08-15 17:11:53 -07:00
Teknium 9146f4c851 fix(evals): explicit encoding on all file I/O (ruff PLW1514) 2026-08-15 17:07:46 -07:00
Teknium 44536bd41b docs(evals): 4-transcript compaction scorecard
lean+recovery 68.3% avg recall @ 49K retained vs current 45.8% @ 162K —
+22.5pts at 0.30x tokens. Anchor index moved GUI needle-fact recall
23.3->60.0 closed-book, 46.7->80.0 with recovery.
2026-08-15 17:01:22 -07:00
Teknium 31ca1200ef feat(compression): field-proven summarizer prompt upgrades
- anti-injection rule in preamble (gemini-cli state_snapshot pattern)
- verbatim security-constraint preservation in Constraints & Preferences
  (claude-code rule)
- Errors & Fixes section with user-correction quoting (claude-code sections
  4/  + CompInt user-feedback emphasis)
2026-08-15 17:01:22 -07:00
Teknium c4bbb14e52 feat(compression): mechanical anchor index + region-scoping tripwire
- _build_anchor_index(): regex-harvests PR/issue numbers, SHAs, branches,
  file paths, error strings, handles, URLs from the compacted region into a
  bounded indexed summary section. LLM-free, so needle identifiers cannot be
  paraphrased away (the GUI-lineage failure class: 10/15 verbatim-or-nothing
  golds). Doubles as session_search query-anchor map.
- evals/compaction/test_region_scoping.py: sentinel tripwire proving the
  summarizer input carries ONLY the compacted region (head/tail sentinels
  never reach the serialized turns body) in both legacy and lean modes.
2026-08-15 17:01:22 -07:00
Teknium 7a82457ede feat(compression): digest noise filter + FTS5 recovery sim + digest-aware query hints
- _digest_worthy() drops no-signal tool rows before chunking (GUI-lineage
  digests were starving on tool-noise)
- eval recovery sim now uses in-memory SQLite FTS5 + BM25 (production
  session_search engine) instead of term-frequency scoring
- recovery query writer sees the digest section (front of context) so it can
  mine anchor identifiers
2026-08-15 17:01:22 -07:00
Teknium 8fe9025abd feat(compression): lean tail mode + recovery-aware eval arm
Lean mode (tail_mode='lean', default stays 'legacy'):
- tail budget = clamp(2.5% of window, 10K, 25K) instead of 0.20*window
- stale tail tool results demoted to session_search recovery stubs
- chunked identifier-preserving digests of the compacted region (map-reduce,
  pristine pre-prune tool contents)
- verbatim user messages embedded in summary (codex retention-by-role rule)
- deterministic session_search recovery footer

Eval: policies matrix gains lean + a '+recovery' arm giving the answerer one
simulated session_search round-trip against the archived region.
2026-08-15 17:01:22 -07:00
Teknium 33242d5ee0 feat(evals): compaction recall eval harness
Measures recall accuracy vs tokens retained across compaction policies.
Real transcripts in, LLM-generated recall exam from the summarized region,
per-policy answer+judge passes, scorecard out.
2026-08-15 17:01:21 -07:00
Teknium 9c58a78a7d feat(desktop): Capabilities-wide profile scoping + one-click hub installs on the Skills tab
Extends the Capabilities "Configuring:" profile selector (#86548) from
Tools/MCP to the WHOLE view — Skills, Tools, MCP, and Browse Hub now all
read and write the same selected profile — and brings Bot Mode's
one-click Skills Hub picker into the main Capabilities -> Skills tab.

Scope widening:
- skills/index.tsx: the selector renders once above whichever tab is
  active. Skills list, toggles, bulk ops, editor, and archive are scoped
  via the trailing-profile pattern; the skills RQ key gains the scope key.
  Toolsets analytics (usage badges) load per scope. SkillsHub and the
  hub picker are keyed/remounted per scope. Scope changes drop the open
  editor/archive dialog and in-flight analytics (same hazards as an
  app-wide profile switch); an app profile switch clears the override.
- hermes.ts: getSkills, setSkillEnabled, get/edit/deleteLearningNode,
  getUsageAnalytics, and all seven skills-hub fetchers take the optional
  trailing profile? (omitting preserves exact app-wide behavior).
- store/hub-actions.ts: runHubAction threads profile through spawn and
  getActionStatus polling so install/uninstall/update and their logs run
  against the scoped backend.
- hub.tsx: sources/search/preview queries keyed+scoped per profile;
  install/uninstall/update/scan route to the scoped profile.
- archive-skill-confirm-dialog.tsx: optional profile prop.

One-click hub installs (from Hermes-Bot-Mode):
- skills/embedded-hub-picker.tsx: collapsible, resizable iframe of the
  live Skills Hub (hermes-agent.nousresearch.com/docs/skills?embed=picker)
  on the Skills tab. Origin-checked hermes-skill-pick postMessages route
  through the standard hub action pipeline (background action, tailed
  log, optimistic flip, Skills list + slash-completion invalidation),
  scoped to the selected profile.
- i18n: skills.hub.picker* keys (en + zh; others fall back).

Tests: index.test.tsx — new case asserts picking a profile on the Skills
tab refetches skills scoped to it and routes toggles there (6/6);
toolset-config-panel 28/28. Full typecheck (3 tsconfigs) + eslint clean.
2026-08-15 16:21:56 -07:00
Teknium 951ae62ffc test(computer-use): pin 0.17+ split refs/content_refs merge behavior
Regression test for the _ref_map merge (salvaged from #79515): the live
0.19.3 driver splits action refs into refs[] while content_refs re-lists
every node with empty actions; the empty entries must not clobber the
action-bearing ones. Caught live: every typed click refused with
browser_ref_stale until the merge fix.
2026-08-15 15:36:19 -07:00
weisiwu bbd3462e25 fix(computer-use): merge refs+content_refs in _ref_map for cua-driver 0.17
cua-driver >= 0.17 splits the semantic_v2 snapshot payload: action-bearing
refs live in the `refs` array while `content_refs` carries every node
with EMPTY action lists. _ref_map only absorbed content_refs, so every
click/pointer/type ref was registered with no declared actions and all
typed-browser mutations failed with browser_ref_stale.

Merge refs + content_refs + snapshot.refs with set union so action info
is never dropped by an empty content entry. Verified against the 0.17
split format, the legacy refs-only format, the transitional dict format,
and the snapshot.refs fallback.
2026-08-15 15:36:19 -07:00
hermes-seaeye[bot] 17c7b0bebd fmt(js): npm run fix on merge (#87296)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 22:15:27 +00:00
Teknium 2e9dcb7c55 fix(gateway): session.history ships durable row_id stamps
The Desktop's content-based truncation-target resolution (and reactions)
address persisted turns by row_id, but session.history loaded the
transcript without include_row_ids=True, so _history_to_messages had no
stamp to forward and the projection silently stripped the one durable
address clients can use. Discovered live-testing the #87294 client flow:
resolveDurableRowId saw 0 stamped rows and degraded every edit to a
plain resubmit.
2026-08-15 15:09:11 -07:00
Teknium 3e8ab06107 fix(desktop): never send ordinal-only truncation — resolve durable row ids by content
Client half of #87059. The gateway now fails ordinal-only truncation
closed for durable sessions (#87150), which turned the mis-aimed cut into
a visible edit-resend error for any bubble without a bound rowId (edit
after an interrupted turn, unstamped resume). Make the Desktop always
produce a durable address or degrade safely:

- runRewindSubmit: when a truncation request lacks a durable address,
  resolve the target's row id by exact content against session.history
  (which ships row_id per persisted row). Resolution is
  exact-or-nothing: a unique text match wins; ambiguity is accepted only
  when the target is provably the newest persisted turn (the
  edit-after-interrupt shape). Anything else degrades to a PLAIN
  resubmit — never a guessed cut. The client ordinal is dropped either
  way (its space can diverge from the gateway's — the #87059 root).
- planReload/planRestore: degrade failed turns to a plain resubmit
  (extends the #86623 pattern to regenerate/restore) and carry the
  turn's persisted sourceText as the content key.
- rebindSurvivorRowIds: iterate the same failed-turn-aware ordinal
  space as the truncate math.
- session-tile-actions: reload goes through the shared runRewindSubmit
  primitive instead of a raw prompt.submit, so the tile surface gets the
  same discipline.
2026-08-15 15:09:11 -07:00
vondelomlo c2a50a8662 fix(desktop): skip failed turns in the backend-facing user ordinal space
A user turn whose submit failed keeps its optimistic bubble but never
reached the gateway, so counting it makes every later
truncate_before_user_ordinal overshoot the backend index (refused 4018,
regenerate dead for the rest of the session). Skip failed turns in the
one shared visible-user ordinal space (visibleUserMessageIndices) used by
truncate ordinals, ordinal->index resolution, and survivor-rowId
rebinding.

Based on #41275 by @vondelomlo, relocated onto the split
use-prompt-actions/ modules and widened from visibleUserOrdinal to the
shared index helper.
2026-08-15 15:09:11 -07:00
Teknium 20cf326bd1 fix(computer-use): align browser authorization with live-verified cua-driver 0.19.3 contract
Live-tested against the real cua-driver 0.19.3 binary (Linux x86_64):

- bounded serve flags corrected: the daemon accepts
  --session-policy/--approve-session-policy, not the docs'
  --capability-manifest names (which it rejects). Verified end-to-end:
  a bounded daemon with a real policy file starts and reports running.
- browser-approve verified real but interactive-only (refuses without a
  TTY) and its token is a legacy compatibility path disabled by default
  on current drivers (per the live browser_prepare schema). Kept as a
  passthrough; no longer presented as the primary route.
- NEW primary standard-mode route, verified live: launch the runtime
  with cua-driver's trusted-launcher grant. config opt-in
  computer_use.grant_existing_profile: true appends
  --grant existing-profile to the standard-mode MCP spawn (MCP
  initialize verified accepting the flag). Default false = attachment
  keeps failing closed. Never applied to bounded/unrestricted daemons.
- Skill, system prompt, tool schema, and docs updated to the verified
  ladder: config grant > bounded manifest > YOLO; token = legacy.
2026-08-15 15:04:32 -07:00
Teknium 48dd9c87cf feat(computer-use): user-facing authorization for cua-driver browser attachment
Completes the typed cua_browser_* route (PR #74166 lineage) with the
authorization surface that makes existing-profile attachment and
repeatable bounded automation reachable by real users:

- hermes computer-use browser-approve: CLI passthrough that mints
  cua-driver's five-minute single-use attachment token for one exact
  (pid, window_id). The user, never the model, is the token source.
- approval_token passthrough on cua_browser_prepare (schema + dispatch +
  browser_route), forwarded only for existing_profile and only as a
  non-empty string.
- computer_use.permission_mode: bounded + capability_manifest config:
  private per-session embedded daemon launched with
  --capability-manifest/--approve-capability-manifest; missing manifest
  fails loudly. 'unrestricted' is deliberately NOT a config value —
  it stays bound to the explicit per-session YOLO toggle.
- Skill + system-prompt + docs guidance for the three authorization
  rungs and the isolated-profile-first default.

E2E-verified against a temp HERMES_HOME: real config resolution to
bounded, loud failure without a manifest, real argparse path driving a
fake cua-driver binary, standard default preserved.
2026-08-15 15:04:32 -07:00
Teknium fe0a56ed16 fix(nemo_relay): bound plugin Relay marks so a wedged native pipeline cannot stall the agent
The plugin's _Runtime.run_in_session wrapper serves every mark/event it
emits (turn start/end, approvals, subagent marks) and runs synchronously
on the agent's conversation thread. It passed no timeout, so the host's
run_in_session default (timeout=None) made each mark an UNBOUNDED native
call. With a wedged native Relay pipeline the agent blocked between API
calls with zero activity ticks — observed live 2026-08-15: two cron jobs
died at the 600s inactivity kill and a gateway chat session at 1800s,
all with last_activity="API call #N completed".

The core's scope push/pop/flush/close sites were bounded with
_SCOPE_OP_TIMEOUT after the 2026-08-10 delegation stall; the plugin's
event marks were the missed sibling class.

Changes:
- plugins/observability/nemo_relay: the wrapper always passes
  timeout=relay_runtime._SCOPE_OP_TIMEOUT (10s) to the host. A breach
  costs one telemetry span, never the agent; it also sets scope_errored
  (so close_session skips the ATIF export for the wedged session) and
  warns once so the sick pipeline is visible.
- tests/plugins/test_nemo_relay_bounded_marks.py: proves the budget
  reaches the host (fails on the pre-fix code — sabotage-verified),
  a TimeoutError flags the session and disables its export, and the
  generic error path keeps its scope_errored contract.
2026-08-15 14:31:32 -07:00
Teknium 92c998c86c fix(update): restore quarantined hermes.exe shims after no-op installs on Windows
Both quarantine wrappers (_run_quarantined_install in main.py and
_run_install_cmd in _install_repair.py) renamed live hermes*.exe shims
aside before invoking the installer, but only renamed them back on
FAILURE. A SUCCESSFUL install that never rewrites entry points — uv
audits an already-satisfied editable install as a no-op — left the
shims quarantined as hermes.exe.old.<ms> and `hermes` disappeared from
PATH after a green install (#75584; reproduced live on a Windows
install recovering from the #86735 self-lock deferral).

Switch both sites from except/re-raise to try/finally so restore runs
on every path. _restore_quarantined_exes already skips shims the
installer actually replaced, so fresh output is never clobbered and
failure behavior is unchanged.

Regression tests cover both wrappers x {no-op success, rewriting
success, failure}; the no-op cases fail on the previous code.
2026-08-15 14:24:35 -07:00
Teknium 763b10c320 fix(gateway): run session-finalize plugin hooks off-loop and bounded
Session-finalize hooks ran synchronously on the gateway event loop from
three call sites (shutdown drain, session-expiry watcher, /new reset).
A plugin hook doing heavy blocking work froze the whole loop: adapter
heartbeats stopped, the drain machinery could not run, and systemd
eventually SIGKILLed the process mid-export. Observed live on a
multi-day 4.7G session where the nemo_relay observability plugin
serialized a full-session ATIF trace inside on_session_finalize.

Changes:
- gateway/run.py: new GatewayRunner._finalize_session_off_loop()
  dispatches hermes_cli.lifecycle.finalize_session via the gateway
  executor under asyncio.wait_for (10s budget), mirroring
  _cleanup_agent_resources_off_loop (#53175). Shutdown finalize and
  the session-expiry watcher now use it.
- gateway/slash_commands.py: /new reset path uses the same helper.
- plugins/observability/nemo_relay: ATIF export is now bounded
  (HERMES_NEMO_RELAY_ATIF_EXPORT_TIMEOUT_S, default 30s) and skipped
  entirely for sessions whose Relay scope operations already errored
  (their exporter state is unreliable and the export can be
  pathologically slow).
- tests/gateway/test_finalize_session_off_loop.py: regression tests
  proving the loop stays live under a wedged hook and the budget is
  enforced.
2026-08-15 14:20:27 -07:00
poisdahl 37280c7e0c Merge remote-tracking branch 'origin/main' into codex/81234-live-main-final 2026-08-15 23:03:10 +02:00
poisdahl fcea2175e2 Merge remote-tracking branch 'origin/main' into codex/81234-live-main-final
# Conflicts:
#	tests/test_tui_gateway_server.py
#	tui_gateway/methods_prompt.py
#	tui_gateway/methods_tools.py
2026-08-15 23:02:54 +02:00
hermes-seaeye[bot] 0c50bdbdea fmt(js): npm run fix on merge (#87257)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 20:44:31 +00:00
poisdahl 67ee481621 fix(gateway): preserve pending transcript state on failed rewinds 2026-08-15 22:43:00 +02:00
hermes-seaeye[bot] 9c7f92bf93 fmt(js): npm run fix on merge (#87251)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 20:38:02 +00:00
fangliquanflq 3863de3155 revert(gateway): keep profile truncation routing out of scope 2026-08-15 13:36:25 -07:00