Skip to content

release: v0.1.52 — live in-flight run progress - #112

Merged
adebnar merged 7 commits into
mainfrom
dev
Jul 20, 2026
Merged

release: v0.1.52 — live in-flight run progress#112
adebnar merged 7 commits into
mainfrom
dev

Conversation

@adebnar

@adebnar adebnar commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Promotes dev to production as v0.1.52.

Beta soak: v0.1.52-beta.1.

What ships

A1 — live in-flight run progress (#111). The app previously notified only when a run finished and showed nothing during the minutes one was executing. It now posts a live ongoing notification: indeterminate by default (acme · agent running / Calling tool: web_search), upgrading to a determinate N/M bar whenever the agent's todo tool reports counts. On API 36+ it uses the platform Notification.ProgressStyle so the system can promote it to a status-bar Live Update; API 26–35 falls back to NotificationCompat.setProgress. New run_progress channel at IMPORTANCE_LOW, separate from the MIN service channel. Toggleable in Settings → Notifications, on by default.

Also wires up session.info.running — which the client never parsed — as the authoritative busy/idle backstop, since message.complete alone misses interrupted and compacted turns.

Notable fixes made during review

  • Tenant isolation. The notification's tenant was read live at post time while its route was latched at run start, so a mid-run profile switch could label an acme run as globex while still routing to acme's session. Tenant is now latched into the run itself.
  • Stranded notification. scope.cancel() is cooperative, so an in-flight event could re-post after onDestroy()'s cancel, leaving an unswipeable ongoing notification. The cancel now hangs off job.invokeOnCompletion.

Verification

351 unit tests green, assembleBeta green, gitleaks clean, 0 open HIGH/CRITICAL Dependabot alerts. versionCode 56 / versionName 0.1.52.

Known non-blocking follow-ups: the notification accent uses the default hashed hue rather than a custom per-profile override; after a headless START_STICKY restart the title degrades to a generic "Agent running" (never a wrong tenant).

adebnar and others added 3 commits July 20, 2026 17:27
* docs: spec for live in-flight run progress (A1)

* docs: implementation plan for live in-flight run progress

* feat: pure run-progress reducer over gateway run events

* feat: map run progress to a platform-independent notification spec

* feat: persist and expose the live run-progress preference

* feat: render the live run-progress notification with API 36 progress style

* feat: drive the live run-progress notification from the gateway event stream

* Fix stranded run-progress notification race and split profile read

onDestroy() cancelled the collector scope then immediately called
notifier.cancelRunProgress(), but scope.cancel() is cooperative and
does not preempt a collector iteration already executing
synchronously. An in-flight iteration could still call
notifier.postRunProgress(...) after the cancel, with no ordering
guarantee, permanently stranding an ongoing notification. Hang the
final cancel off the Job's actual completion (invokeOnCompletion)
instead of off the cancel() call, so it fires only after every child
coroutine has genuinely finished.

Also collapse the two separate profiles.active.value reads in
updateRunProgress into one, so a profile switch landing between them
can no longer split a notification's title/route from its accent
colour across two different tenants.

* Latch tenant into run progress to fix accent/route drift

A run's tenant is now latched into RunProgress at message.start / session.info
time instead of being re-read from ProfileManager.active at notification-post
time. Opening a cross-profile session (SessionsViewModel.prepareOpen) switches
the active profile without resetting run state, so the two could drift and
post a notification with one tenant's accent/title while its route still
pointed at another tenant's session.

Also: a differing sessionId on a running session.info now starts a fresh run
instead of silently keeping the stale one, and the second reserved
notification id (1003, run-progress) is now guarded against id collisions the
same way the first (1001) already was.

@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 implements a live, in-flight run progress notification feature that tracks agent runs using gateway WebSocket events. It introduces a pure RunProgress reducer, maps state to a platform-independent RunProgressSpec, adds a settings toggle to persist user preferences, and renders notifications using API 36+ ProgressStyle with a backward-compatible fallback. The feedback highlights critical improvements: ensuring ProfileManager is refreshed during cold starts to avoid uninitialized state, isolating API 36+ classes in a nested object to prevent class verification errors on older Android versions, and defensively filtering out non-object elements in the todos array to ensure accurate progress counts.

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.

Comment thread app/src/main/java/com/hermes/client/notifications/HermesNotifier.kt Outdated
Comment thread app/src/main/java/com/hermes/client/data/network/ServerEvent.kt
Comment thread app/src/main/java/com/hermes/client/notifications/HermesNotifier.kt Fixed
adebnar added 4 commits July 20, 2026 14:00
- todoCounts(): skip non-object todos array elements entirely instead
  of letting a bare string/number/null inflate the total denominator
- HermesNotifier: isolate the API-36 ProgressStyle construction in its
  own class so no API-36 type is named in HermesNotifier's own method
  bodies (defensive class-verification hardening, no behaviour change)
- GatewayConnectionService.onCreate(): seed ProfileManager's active
  profile on a headless START_STICKY restart so the run-progress
  notification title still names the tenant
Kotlin's ::class.java compiles to a reflection call rather than a type
literal, so Intent(Context, Class) is not recognised as setting a
component and the resulting PendingIntents read as implicit. Use
setClassName() with the class's own name instead: identical at runtime,
rename-safe, and unambiguously explicit. Matters most for the direct-reply
intent, which must be FLAG_MUTABLE — mutable plus implicit is the
genuinely exploitable combination.
Static analysis constant-folds a flag literal at the call site but not a
value returned from a helper, so FLAG_IMMUTABLE was unprovable and the
PendingIntents read as mutable. Drops the pendingFlags() helper in favour
of the literal; no runtime change.
Mutations inside a Kotlin apply { } block are made on the lambda receiver,
which static analysis does not reliably attribute back to the variable, so
the component set there was invisible and the intents read as implicit.
Assign to the variable directly instead. No runtime change.
@adebnar
adebnar merged commit 9f08f39 into main Jul 20, 2026
9 checks passed
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.

2 participants