Skip to content

fix(state): adopt upstream v21 FTS + stop per-turn message duplication (+ upstream merge) - #117

Merged
exiao merged 515 commits into
live-configfrom
fix/state-v21-fts-and-persist-dedup
Jul 15, 2026
Merged

fix(state): adopt upstream v21 FTS + stop per-turn message duplication (+ upstream merge)#117
exiao merged 515 commits into
live-configfrom
fix/state-v21-fts-and-persist-dedup

Conversation

@exiao

@exiao exiao commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Three commits.

1. Adopt upstream v21 FTS/schema (drop local v20 divergence)

hermes_state.py had diverged (local #110 v20 view-backed FTS + WAL watchdog + trigram gate; #114 trigram LIKE fallback) while the live DB had already migrated to upstream's v21 inline FTS — code(20)/DB(21) inconsistent, every upstream pull conflicting on this file. hermes_state.py reset to upstream/main verbatim (SCHEMA_VERSION 20→21, matches the DB); removed the two gateway/run.py wal_watchdog call sites. Verified against a copy of the live DB: opens at v21, no destructive re-migration, FTS + append work. Trade-off: keeps ~0.9GB inline FTS bloat (upstream NousResearch#22478) and drops the WAL watchdog; acceptable now the write-storm is fixed.

2. Stop per-turn history re-persistence (reopens NousResearch#860 / NousResearch#42039)

The gateway re-persists the whole prior history every turn: _flush_messages_to_session_db's in-memory _DB_PERSISTED_MARKER is _-prefixed and stripped by the wire sanitizer, and reloaded/deep-copied history returns unmarked/without object identity, so every prior message is re-INSERTed. One live session stored its first message 62×; state.db held 63,111 dup rows (21%). The marker fix (NousResearch#50372) doesn't survive its own sanitizer; upstream's _session_persist_lock (2026-07-15) only guards concurrency (merging upstream leaves the duplication byte-identical). Fix: natural-key idempotency guard in append_message (session_id, role, timestamp, content, tool_call_id, tool_calls) inside BEGIN IMMEDIATE. Migration-free.

3. Merge latest upstream/main

Conflicts resolved: kept both rotation test classes; took upstream docs (AGENTS.md); combined approval.py's new _is_verification_artifact_cleanup with the local force-push carve-out; kept the local credential-redaction block.

Validation

Dedup repro all-clean pre/post-merge (reload/deepcopy went U0×32→U0×1); 7 edge cases (distinct + same-content/different-timestamp not collapsed, no data loss); 60 persist/message + 6 rotation + 352/353 approval tests pass. The 1 approval failure is a pre-existing upstream macOS-only tempdir issue (realpath("/tmp")→/private/tmp), passes on Linux CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PsbWmnp8Q5o8oupgYAsjaE

OutThisLife and others added 30 commits July 13, 2026 17:57
The Thinking disclosure rendered blank for every reasoning-emitting model
(Fable, DeepSeek, GPT-5.5, ...). Two causes:

1. ReasoningTextPart read a `text` prop that assistant-ui never populates —
   reasoning parts arrive via context, same as text parts — so it always got
   an empty string. Read the text via useMessagePartReasoning() instead,
   mirroring how MarkdownText uses useMessagePartText().

2. The reasoning-only SmoothStreamingText / useSmoothReveal layer stalled at
   revealed="": the reasoning part stays isRunning for the whole message while
   the answer streams and thrashes re-renders, so the char-reveal never
   advanced past 0. Render reasoning through the same DeferStreamingText →
   surface path the assistant answer uses, and drop the dead smoothing code.
run_job() constructs SessionDB() synchronously with no timeout of its
own, unlike the agent's run_conversation call further down, which is
already bounded by HERMES_CRON_TIMEOUT. A wedged sqlite3.connect (e.g.
a stale flock from a crashed sibling process) hangs this call
indefinitely.

That hang is invisible to every existing cron safeguard because it
happens before _submit_with_guard's future exists: the finally block
that discards the job ID from _running_job_ids never runs. The job
stays wedged "running" — every later tick logs "already running —
skipping" — until the whole gateway process is restarted.

Observed in production: a cron job's worker thread was confirmed via
a live py-spy thread dump to be parked inside SessionDB.__init__'s
sqlite3.connect for 3+ days, silently skipping every scheduled fire
in between across a gateway process that otherwise stayed healthy.

Bound the SessionDB() construction with its own timeout
(HERMES_CRON_SESSION_DB_TIMEOUT, default 10s), following the same
bounded-thread-pool pattern already used elsewhere in this file (the
delivery retry path, and the agent inactivity watchdog just below).
On timeout, log at ERROR and proceed with session_db=None instead of
degrading silently to debug level, since an actual hang here is a new
condition worth surfacing.

Adds tests/cron/test_sessiondb_init_hang.py, including an end-to-end
regression proving the dispatch guard is released and a subsequent
tick can fire the same job again after a simulated hang.
Salvage of NousResearch#63935. The original fix read HERMES_CRON_SESSION_DB_TIMEOUT
from a bare env var, but AGENTS.md requires non-secret behavioral
settings to live in config.yaml with an env var bridge only for
backward compatibility.

Changes:
- Add cron.session_db_timeout_seconds to DEFAULT_CONFIG (default 10s)
- Resolution order: HERMES_CRON_SESSION_DB_TIMEOUT env override →
  cron.session_db_timeout_seconds in config.yaml → 10s default
  (mirrors the existing script_timeout_seconds pattern)
- 0 = unlimited (opt-in for debugging, skips the bound)
- Strengthen test: assert the warning is logged on invalid env value
  (caplog was taken but never asserted)
- Add test: verify config.yaml resolution path works end-to-end

Co-authored-by: LoicHmh <26006141+LoicHmh@users.noreply.github.com>
Retain a distinct CLI history baseline during the signal window before a turn's normal persistence flush. When CLI history aliases the live agent list, use marker-only persistence so a genuinely unflushed tail is written.
Preserve one durable staged input across terminal close and the worker's early turn flush, without duplicating resumed transcripts or creating a session with a null prompt. Fixes NousResearch#63766.
Burgunthy and others added 22 commits July 15, 2026 09:50
STANDARD_PROFILES, normalize_profile, validate_profile_name, and
is_standard_profile in hermes_constants were superseded by
hermes_cli.profiles.{normalize_profile_name, validate_profile_name}
but never removed. profile_routing.py is updated to import from the
canonical location; the old helpers are deleted.

Lazy import inside parse_profile_routes avoids the circular dependency
at module load time (hermes_constants -> hermes_cli -> hermes_constants).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_adapter_credential_fingerprint only looked at adapter.token directly,
but Discord (and similar) adapters store the bot token on their config
sub-object, not on self. Every Discord adapter in a multiplexed
gateway therefore returned None, the same-token conflict check was
silently skipped, and N adapters all polled the same bot token —
producing a per-message race where whichever adapter won the GIL
answered the user.

Adds a config-token fallback (token, then bot_token) so the check
actually fires for config-backed adapters. Direct adapter.token
still takes precedence when both exist.

Tests cover: config-backed token produces a fingerprint, distinct
tokens produce distinct fingerprints, direct token wins over config,
config without token attributes returns None.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
load_gateway_config only forwarded the top-level multiplex_profiles
key, ignoring the nested gateway.multiplex_profiles form. The latter
is what `hermes config set gateway.multiplex_profiles true` writes,
so users who ran that command got multiplex_profiles=False silently
— no warning, no fallback, profile_routes just stopped matching.

Loader now checks the top-level key first, falls back to the nested
gateway section, and only then defaults to False. Same precedence is
applied to other nested-form keys (profile_routes already did this).

Tests cover: top-level honored, nested honored (regression test for
the silent-fallback bug), default False, top-level overrides nested.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two follow-ups observed after deploying profile routing:

1. sessions.profile_name was NULL even when the agent ran inside the
   routed profile scope. _insert_session_row never wrote it,
   get_or_create_session / reset_session never passed it through, and
   the agent-side _ensure_db_session fallback had no way to read it.
   - Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
     auto-adds it on existing DBs.
   - _insert_session_row takes profile_name and writes it.
   - SessionStore passes source.profile (or old_entry.origin.profile
     on reset) into db_create_kwargs.
   - _ensure_db_session reads the active profile via
     get_active_profile_name() inside _profile_runtime_scope.

2. DiscordAdapter._text_batch_key called build_session_key without
   profile=, so the batch key always landed in agent:main even when
   the routed profile differed — diverging from the agent session
   key namespace (agent:crypto-trader, agent:ai-expert, ...).
   Pass event.source.profile through so both namespaces agree.

Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord:thread:...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…way_runner

Addresses hermes-sweeper review on NousResearch#20096.

Problem 1 (profile_routing.py): route matching returned True on a chat_id
hit before the guild_id constraint was consulted, so a route declaring both
guild_id and chat_id matched on chat_id alone. Restored conjunctive (AND)
semantics — every declared discriminator must hold; hierarchical parent_chat_id
matching is preserved. Added a regression test for the guild+chat case.

Problem 2 (base.py / run.py): gateway_runner was injected only when an adapter
pre-declared the attribute, and only Discord did — so build_source never called
_profile_name_for_source for Telegram/Feishu/Slack/etc., despite the
platform-generic claim. Declared gateway_runner on BasePlatformAdapter and made
the plugin-registry injection unconditional, so profile routing now reaches
every platform. Added non-Discord (Telegram) resolution coverage and an
injection-inheritance test.

Also adds docs/profile-routing.md documenting gateway.profile_routes
(matching rules, specificity, profile isolation) — requested in review.

Co-Authored-By: Claude <noreply@anthropic.com>
Completes the review's ask for "adapter-to-session-key integration coverage
for Discord and a non-Discord platform" on NousResearch#20096.

Drives a concrete adapter's real BasePlatformAdapter.build_source with an
injected gateway_runner, asserts the matched route's profile is stamped on
the source, and that build_session_key scopes the key under agent:<profile>:
(versus the shared agent:main: namespace). Covers Discord and Telegram — the
Telegram case is the bug-#2 path that previously fell through to default.
Adds a regression anchor: without gateway_runner, profile stays None and the
key lands in agent:main (the silent fallback the fix removes for non-Discord).

Co-Authored-By: Claude <noreply@anthropic.com>
…h-key routing to all adapters

Follow-ups on the salvaged NousResearch#20096 profile-routing feature:

- _profile_name_for_source now returns None unless gateway.multiplex_profiles
  is on. Routing stamps source.profile, which namespaces session/batch keys,
  but the profile-scoped agent run only activates under multiplexing — without
  the gate, configured routes with multiplexing off split batch/session keys
  into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
  that builds batch keys via build_session_key (telegram, whatsapp, matrix,
  feishu, wecom, weixin) — routing is platform-generic, so the batch-key
  namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
  unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
  (ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
  docs/profile-routing.md (routing requires multiplexing), fix the
  platform-only specificity row (0, not 1), and document profile_routes in
  website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
  build_source end-to-end stays in agent:main when off).
…d slots

Three fixes in model-settings.tsx:

1. Filter incomplete slots before autosave: sanitizeMoaRefsForSave() strips
   reference slots with empty model before any autosave (the 600ms debounce
   was sending half-filled provider-but-no-model slots to the backend, where
   _clean_slot rejects them and _normalize_preset falls back to hardcoded
   defaults). Presets with zero valid refs keep their empty reference_models
   array rather than being silently dropped. The aggregator slot is also
   sanitized when its model is empty.

2. Add withActive() to MoA provider dropdowns: the reference and aggregator
   provider Selects filtered to authenticated-only, so unauthenticated
   current values (e.g. openai-codex) rendered blank. Mirror the existing
   pattern from the model Select.

3. Add generation counter to scheduleMoaSave(): stale save responses could
   overwrite newer state. Bump a counter on each save and skip setMoa/setError
   if a newer save was scheduled in the meantime.
…esktop autosave until slots are complete

Follow-up hardening on top of NousResearch#64158 (@DavidMetcalfe):

Backend (the root-cause fix):
- hermes_cli/moa_config.py: add validate_moa_payload() — strict write-time
  counterpart to the deliberately tolerant normalize_moa_config(). Flags
  half-filled slots, empty reference lists, recursive moa slots, naming the
  exact preset/slot.
- hermes_cli/web_server.py: PUT /api/model/moa validates before normalizing
  and returns 422 with the specific problems instead of silently swapping the
  user's preset for hardcoded defaults (NousResearch#64156). Also declares
  fanout / reference_max_tokens / reasoning_effort on the Pydantic payload so
  client round-trips no longer erase hand-set values.

Desktop:
- Replace sanitize-then-send with hold-while-incomplete: the debounced
  autosave is deferred (not repaired) while any slot is half-filled, and
  flushes once the model pick completes the edit. Mid-edit UI state is never
  repainted by a save response (generation guard covers held edits too).
- updateMoaSlot only clears the model when the provider actually changed.
- Explicit preset ops (set default / add / delete) cancel the pending
  autosave and invalidate in-flight responses so the two writers can't race.
- Stable row keys (preset+index) so mid-edit rows don't remount; cleared
  model shows the 'Model' placeholder instead of vanishing.

Both TS clients' MoaConfigResponse types now declare the round-tripped
fields (fanout, reference_max_tokens, reasoning_effort).

Tests: 12 new backend unit tests (validate_moa_payload contract incl.
validate/normalize agreement), 3 new web_server endpoint tests (422 on
half-filled ref/aggregator, fanout round-trip), 3 new desktop vitest cases
(autosave held while half-filled, flush on completion, same-provider
reselect no-op). E2E validated against a live TestClient with isolated
HERMES_HOME: bug sequence now 422s with config untouched.

Fixes NousResearch#64156
…x only) (NousResearch#65103)

The docs search theme (@easyops-cn/docusaurus-search-local) defaults
fuzzyMatchingDistance to 1, so every term also matched words one edit
away. Two user-visible failures on the 14.4 MB production index:

- Wrong results: 'keet' returned 'Microsoft Teams Meetings',
  'google_meet', 'Keep the Model Loaded' etc. — 'meet' and 'keep' are
  both one edit from 'keet', and the stemmer indexes 'meetings' as
  'meet'.
- Search appearing to die: fuzzy matching multiplies the generated
  lunr queries (distance matrix x maybe-typing variants x
  leave-one-out terms — up to 210 queries per keystroke on multi-word
  input), and fuzzy REQUIRED terms are the expensive scan kind. A
  typo'd 3-word query stalled the single-threaded search Web Worker
  for 25+ seconds; every later keystroke's search queued behind it,
  so the bar stopped returning results.

Setting fuzzyMatchingDistance: 0 keeps exact-word-or-prefix semantics
(keet -> keet*), which is the behavior users asked for. Validated by
running the plugin's shipped smartQueries/tokenize code against the
downloaded production search-index.json: legitimate queries (cron,
telegram, prefix 'memor') return identical results; worst-case typo
queries drop from 210 queries / 365-532ms per keystroke to 50-105 /
53-188ms; false 'keet' matches gone.
…e + bearer token routing

Three fixes for the Bedrock Claude path:

1. Streaming fallback: When AnthropicBedrock SDK raises 'Unexpected event
   order' (SDK misparses Bedrock error events as message_start), auto-switch
   to native Converse API for the rest of the session instead of failing
   after 3 retries.

2. Image base64 decode (NousResearch#33317): data URL payloads were passed as base64
   strings to source.bytes, but boto3 re-encodes at the wire layer. Now
   decoded to raw bytes before passing to Converse API.

3. Bearer token routing (NousResearch#28156): Users with AWS_BEARER_TOKEN_BEDROCK are
   now routed through Converse API regardless of model, since the
   AnthropicBedrock SDK only supports SigV4 signing.

3 new tests. 121 bedrock_adapter tests passing.
Rewrites the cherry-picked test to import os locally and monkeypatch the
resolver seams instead of patching bedrock_adapter internals; adds the
inverse assertion (no bearer -> AnthropicBedrock SDK path preserved).
…ousResearch#28156)

Bug 2 of NousResearch#28156: the picker offered us./global. inference profiles to
EU-region endpoints (unroutable — AWS rejects them regardless of
credentials) and _RECOMMENDED hardcoded us.anthropic.* ids, so non-US
pickers pinned profiles their endpoint can't invoke.

- bedrock_model_routable_from_region(): geo-prefixed profiles are only
  offered in their own geography (full AWS prefix set incl. apac./jp./
  ca./sa./me./af.); bare ids and global.* pass everywhere; unknown
  region shapes hide nothing.
- Recommendations match geo-agnostically on the base model id, so an EU
  picker pins eu.anthropic.claude-sonnet-4-6; in-region geo profiles
  sort above global.* for the same model (addresses the global.*-first
  ordering complaint from the issue thread).
- Dedup generalized from (us., global.) to all profile prefixes.
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts
feat(desktop): contribution-driven shell on a layout-tree model
… FTS + WAL watchdog

hermes_state.py had diverged from upstream (local #110: v20 view-backed
external-content FTS + WAL watchdog + trigram gate; #114: trigram LIKE fallback),
while the live DB had already migrated to upstream's v21 inline FTS — leaving
code(20) vs DB(21) inconsistent and every upstream pull conflicting on this file.

Reset hermes_state.py to upstream/main verbatim (SCHEMA_VERSION 20->21, matches
the DB) and remove the two gateway/run.py wal_watchdog call sites (#110) that
referenced the now-removed method. Verified against a copy of the live DB: opens
at v21 with no destructive re-migration, FTS search + append work, the orphaned
messages_search_v view is harmless. Upstream's every-50-writes
wal_checkpoint(TRUNCATE) remains as WAL bounding.

Trade-off: keeps the ~0.9GB inline FTS bloat (upstream NousResearch#22478 still open) and
drops the WAL watchdog; both acceptable now that the duplication write-storm is
fixed. Goal: zero divergence in hermes_state.py so upstream pulls stop breaking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsbWmnp8Q5o8oupgYAsjaE
When an agent turn finishes while the user is viewing a different
session, the sidebar now shows a steady green dot on that session —
distinct from the blue pulsing dot of a running turn and the gray dot
of an idle one. Opening the session clears the indicator.

The unread state is ephemeral renderer-side state, matching the
existing $workingSessionIds and $attentionSessionIds pattern: no
persistence, no backend involvement, wiped on gateway-mode switch.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: dschnurbusch <dschnurbusch@users.noreply.github.com>
Co-authored-by: Flow Digital Inc. <flow-digital-ny@users.noreply.github.com>
…e-persistence

The gateway re-persists the whole prior history every turn. _flush_messages_to_session_db
dedups via an in-memory _DB_PERSISTED_MARKER, but that marker is _-prefixed and stripped
by the wire sanitizer, and history reloaded via get_messages_as_conversation
(resume/undo/restore) or deep-copied for the API request returns unmarked and without
object identity — so every prior message is re-INSERTed each turn. On one live Signal
session this stored the first message 62x; state.db held 63,111 duplicate rows (21%),
driving a 2.2GB DB + 2.2GB WAL and inflating model context.

This is NousResearch#860 / NousResearch#42039 reopened: the marker fix (NousResearch#50372) does not survive its own
sanitizer, and upstream's 2026-07-15 _session_persist_lock only guards concurrency, not
this deterministic single-threaded path (verified: merging upstream/main leaves the
duplication byte-identical).

Guard append_message with a natural-key idempotency check (session_id, role, timestamp,
content, tool_call_id, tool_calls) inside the BEGIN IMMEDIATE txn: a byte-identical row
is a re-persist (new messages always get a fresh time.time() timestamp), so return the
existing id without inserting or double-counting. Migration-free; defense-in-depth for
the whole re-persist class regardless of which caller loses the marker.

Verified: 5-mode multi-turn repro all clean (was U0x32 -> U0x1); 7 edge cases incl.
distinct messages and same-content/different-timestamp NOT collapsed (no data loss);
60 existing append/message/persist/codex tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsbWmnp8Q5o8oupgYAsjaE
…d-indicator

feat(desktop): green unread dot for background-finished sessions
Conflicts resolved:
- hermes_state.py: took upstream's new `profile_name` sessions column; the
  append_message idempotency guard merged cleanly and is intact.
- tests/agent/test_compression_rotation_state.py: kept BOTH test classes
  (local TestUserIdPropagatesOnRotation + upstream TestFallbackStreakFollowsRotation).
- AGENTS.md: took upstream's newer desktop-slash-commands + testing-philosophy docs.
- tools/approval.py: took upstream's new _is_verification_artifact_cleanup; COMBINED
  it with the local branch-aware force-push carve-out (skip_force_push, used in the
  loop body); kept the local credential-redaction block (display-only, prevents
  sk-... leaks into screenshottable chat renders).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsbWmnp8Q5o8oupgYAsjaE
@claude

claude Bot commented Jul 15, 2026

Copy link
Copy Markdown

Claude encountered an error after 2s —— View job


I'll analyze this and get back to you.

@exiao
exiao merged commit 7acac88 into live-config Jul 15, 2026
24 of 30 checks passed
@exiao
exiao deleted the fix/state-v21-fts-and-persist-dedup branch July 15, 2026 18:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e90a8feb8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread hermes_state.py
Comment on lines +3866 to +3869
"""SELECT id FROM messages
WHERE session_id = ? AND role = ? AND timestamp = ?
AND content IS ? AND tool_call_id IS ? AND tool_calls IS ?
LIMIT 1""",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include platform IDs in append-message dedupe

When a real platform delivers two identical messages in the same second, the gateway persists the platform event time as the message timestamp, so this new natural-key check treats the second message as a re-flush and returns the first row id without inserting or incrementing message_count. Because the key ignores platform_message_id (and other distinct metadata), repeated inputs like two Telegram messages with the same text and second-precision timestamp are silently dropped from state/replay; either include the durable platform id in the key or keep this dedupe scoped to known internal re-persistence paths.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces comprehensive updates to the Hermes Agent, focusing on robust error handling, platform-specific optimizations (especially for Windows/WSL), and layout/state modularization. Key improvements include extracting Windows child process hiding and tree-killing logic into dedicated modules, implementing WSL path bridging, refactoring the context compressor to handle malformed arguments gracefully, and modularizing session and route tiles into layout-tree pane contributions. Additionally, support was added for DeepInfra and Upstage Solar models, along with a turn-end guard for Kanban workers and orphan recovery for interrupted side-effecting tools. The review feedback points out a redundant check in the write_file tool result summarization within agent/context_compressor.py that could introduce a subtle bug when the content is falsy.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if tool_name == "write_file":
path = args.get("path", "?")
written_lines = args.get("content", "").count("\n") + 1 if args.get("content") else "?"
written_lines = _str_arg(args, "content").count("\n") + 1 if args.get("content") else "?"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line is redundant as it checks args.get("content") before calling _str_arg, which already handles retrieving and processing the argument. This also introduces a subtle bug: if the model provides 0 as content, args.get("content") would be falsy, resulting in ?, while the intent is likely to count it as one line.

A clearer and more correct implementation would be to use _str_arg directly.

Suggested change
written_lines = _str_arg(args, "content").count("\n") + 1 if args.get("content") else "?"
written_lines = (lambda c: c.count('\n') + 1 if c else '?')(_str_arg(args, 'content'))
References
  1. Avoid redundant defensive checks (such as checking if an object is None before a function call) if the target function already handles None safely.

exiao pushed a commit that referenced this pull request Jul 15, 2026
Real merge (history-preserving) so live-config actually contains upstream's
history — the earlier squash-merge of #117 lost the ancestry and left the
branch showing 514 behind.

Conflicts resolved (5 files):
- tools/approval.py: kept local branch-aware force-push carve-out + credential
  redaction; took upstream's _is_verification_artifact_cleanup (merged clean).
- hermes_cli/kanban_db.py: kept local workspace-scheme healing (upstream absent).
- tests/agent/test_compression_rotation_state.py: kept local TestUserIdPropagatesOnRotation.
- apps/desktop/.../fallback.tsx + .test.ts: took upstream's UNBOUNDABLE_TOOLS
  generalization (clarify + image_generate), a superset of the local clarify-only guard.

hermes_state.py stays at upstream v21 + the append_message idempotency guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsbWmnp8Q5o8oupgYAsjaE
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.