Skip to content

fix(mobile): bind needs-input acks to the answered raise - #8583

Merged
iscekic merged 7 commits into
mainfrom
drain/bind-attention-ack-raise
Oct 22, 2026
Merged

iscekic merged 7 commits into
mainfrom
drain/bind-attention-ack-raise

Conversation

@iscekic

@iscekic iscekic commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Evidence

Evidence: demonstrated — failing-before regressions reproduced submission races, and the real Android app with the published CLI reproduced post-answer badge resurrection; the relay fix kept the badge cleared through refetches and heartbeats while later unanswered raises, including after restart, remained visible.

Change

  • Needs-input acknowledgements persist the answered raise (statusUpdatedAt ?? status), including across app restarts. A later dated raise keeps its badge; legacy persisted acknowledgements without an identity are discarded.
  • Answer, skip, and permission handlers capture the observed raise before submitting the response. A late response cannot overwrite a newer dated raise already tracked by the acknowledgement store.
  • Active-session list enrichment emits statusUpdatedAt only when its stored status matches the resolved status. Mobile heartbeat and snapshot merges preserve the timestamp only while its status remains effective. An explicit undated status transition drops the prior status's timestamp.
  • Held question/permission states retain their timestamp across non-attention heartbeats. Unknown timestamps continue to use the existing status-only fallback and learn the first dated observation of that status. Tray and launcher reconciliation remain timestamp-less as approved; their badge readers use a dated identity when available.
  • In-app, notification, launcher, and widget answer paths pass the answered row's identity. ISO and PostgreSQL spellings of the same instant compare equally.
  • Relay status notifications synchronize the owning active-session snapshot and socket attachment before WebSocket fanout. Derived root notifications preserve attention held by other children. Subsequent undated heartbeats remain admissible; this change does not invent a heartbeat version or redesign producer event ordering.

Verification

Follow-up

  • Three handler race regressions failed before the pre-submit capture fix, then passed: answer, skip, and permission responses cannot adopt a later dated raise arriving during submission.
  • Two acknowledgement-order regressions failed before the ordering guard, then passed: late older responses preserve both an acknowledged newer raise and an unanswered newer raise.
  • Targeted mobile run: 9 suites, 245 tests passed. Covers the store, heartbeat/snapshot transitions, submission handlers, mounted Agents rows/tab badge, glanceable attention readers, and notification actions.
  • Server list-output regressions: 2 Jest suites, 18 tests passed. Covers live attention ahead of stored idle/busy, held dated attention, and cloud status resolution.
  • pnpm --filter kilo-app test completed successfully.
  • Mobile typecheck, full mobile lint, unused-export check, and web TypeScript check passed. Changed-file formatting and git diff --check passed.
  • Relay snapshot and child-resolution regressions failed before the relay fix. The final UserConnectionDO suite passes 312 tests, including attachment persistence, hibernation, owner transfer, same-ID reconnect, cold disconnect cleanup, null status, and independent child attention.
  • Full relay unit run: pnpm --filter cloudflare-session-ingest exec vitest run --maxWorkers=2 --testTimeout=15000 — 37 suites, 1,400 tests passed, 3 skipped. The unchanged oversized-parser test exceeded the default 5-second timeout on this loaded VM; the reported full run uses explicit headroom rather than changing that test.
  • Relay typecheck and lint passed. The real Worker attachment integration suite passed.
  • Timestamp provenance and acknowledgement contracts are documented beside their implementations. No wire field or required API type was added.

Follow-up Android and CLI proof

Ran the real Android development app against an isolated local backend/database using an owned fake account and the published CLI 7.8.15, with deterministic local fake-model inference and optional analytics disabled.

  • Before the relay fix, a completed question returned to NEEDS INPUT after a manual refetch. Correlated transcript, API, and socket traces showed one completed request, an authoritative idle event, then a stale relay snapshot restoring question without a timestamp.
  • After the fix, answering through the app cleared attention and it stayed cleared through manual pull-to-refresh and a 45-second observation spanning multiple CLI heartbeats.
  • A private observer fetched the actual relay snapshot immediately after explicit idle events. It returned idle before the next heartbeat on two runs, proving the CLI-to-relay snapshot path independently of the app cache.
  • A later real question was answered in the app. After force-stop/relaunch, attention stayed cleared; a new unanswered question restored NEEDS INPUT and the tab badge. Inspected focused screenshots are attached in the follow-up comment.

Device limits: permission responses and forced reversed submission completion are covered by regressions, not device proof. The pending question card did not reliably restore after leaving/reopening detail during this run; final answers were submitted through the real app while detail stayed mounted. Pending-interaction replay is not claimed as verified. Heartbeats have no producer transition identity/time, so subsequent undated heartbeats remain admissible under the existing contract.

Initial-session device evidence

The initial session exercised the real Android app against the local stack with an owned fake account, the published CLI as the remote agent, and the deterministic fake model. It observed NEEDS INPUT and a tab badge for a question, badge clearance after answering, and the badge returning for a later unanswered question. After answering and force-stopping/relaunching the app, a third question showed the badge again. Screenshots are attached in the existing PR comments. Those runs did not force out-of-order network completion; that ordering is covered by the regressions above.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: In-app acknowledgement identity source? Answer: Read the tray-cached raise (Recommended). Confidence: 0.75. Precedent: Fix repeated mistakes at the class level.
  2. Question: Hydration of older persisted acknowledgements without raise identity? Answer: Drop it (Recommended). Confidence: 0.80. Precedent: Replace obsolete paths rather than retaining parallel behavior.
  3. Question: Recorded notification Approve ask timestamp? Answer: Add the optional field (Recommended). Confidence: 0.75. Precedent: Fix the issue on every affected surface.
  4. Question: Tray and launcher reconciliation identity? Answer: Keep timestamp-less (Recommended). Confidence: 0.60. Precedent: none.
  5. Question: Extend this PR to relay status-snapshot precedence after the Android finding? Answer: Fix relay snapshot precedence. Confidence: 0.70. Precedent: Fix the root cause across affected surfaces and require valid end-to-end proof.
  6. Question: Temporary local publication-credential recovery? Answer: Temporary control credential. Confidence: 0.77. Precedent: Keep the work item converging through authorized recovery. Restore the original local routing after publication; gate rules and production configuration remain unchanged.

An ack stored only a raise-less marker, and the reconciler then adopted
whichever attention raise it observed next, so a question or permission
raised after an answer — while the app was backgrounded, offline, or
closed — kept its badge hidden, including after a restart.

Acks now name the raise the user answered (the row's status time when it
carried one, the status otherwise), reconcile resolves an ack only for its
own raise, and a hydrated ack that names no raise suppresses nothing.
Raise ids compare as instants through the shared Hermes-safe timestamp
reader, because stored rows hand over PostgreSQL text and live rows ISO
8601. Readers that only have a status still match the acked status.

The answer, skip, permission, notification and glanceable approve paths
all pass the raise they answered.
@iscekic iscekic added drain Work item from the findings drain janitor:features-workflows Janitor finding family: features-workflows labels Oct 22, 2026
@iscekic

iscekic commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

bot: End-to-end run on the Android emulator against the local dev stack, signed in as a seeded
throwaway account, with the published CLI as the remote agent and the deterministic fake model so an
agent actually asks.

Observed, in order:

  1. The agent asked its first question; the session detail showed the question card.
  2. The Agents row read NEEDS INPUT and the tab badge counted one needs-input session.
  3. After answering in the app, the badge cleared.
  4. A second question — one the user had not answered — brought the badge back.
  5. After answering that one, force-stopping and relaunching the app, a third question brought the badge
    back again: the persisted ack no longer suppresses a different raise.

The badge-pixel area of the tab bar is red-dominant in captures 2, 4 and 5 and not in capture 3, which
matches the accessibility labels read from the live UI at each step.

Question card for the first agent question

Agents row needs input with the tab badge before answering

Agents row after answering, no needs-input badge

Agents row needs input again for an unanswered second question

Agents row needs input again after a restart and a third question

The new ack contracts pushed the interaction-handlers suite past the
max-lines budget, and the badge hydration case nested two ternaries in
one expectation. Split the count into a named local and declare the
suite's single-file budget the way the neighbouring suites do.
@kilo-code-bot

kilo-code-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental revision adds relay-side persistence for authoritative status notifications and collision handling for replaced CLI sockets; reading the changed lines and their callers found no new correctness, security, or leak issues. Verification is by code inspection; the relay test suite was not executed in this read-only session ([INFERENCE]).

Files Reviewed (2 files)
  • services/session-ingest/src/dos/UserConnectionDO.ts
  • services/session-ingest/src/dos/UserConnectionDO.test.ts
Previous Review Summaries (4 snapshots, latest commit 815510d)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 815510d)

Status: No Issues Found | Recommendation: Merge

Executive Summary

This incremental revision closes the carried-forward finding: enrichment now emits statusUpdatedAt only when it dates the resolved status, heartbeat/snapshot merges drop a cached time whenever the resolved status changes, and the pre-submit raise read plus the newer-raise guard prevent a late response from clobbering a newer raise. No new correctness, security, or subscription-leak issues were found in the changed lines.

Assumptions: [INFERENCE] I did not run the app, the test suites, or the emulator flow; verification is by reading the changed code and its callers. The documented status-only ack behavior remains a deliberate limit: when a raise is observed live before the DB records it, the ack names only the status, so it still adopts the first later dated observation of that status.

Files Reviewed (10 files)
  • apps/web/src/lib/active-sessions-list.ts - no new issues
  • apps/web/src/lib/active-sessions-list.test.ts - no new issues
  • apps/web/src/lib/active-sessions-list.heartbeat.test.ts - no new issues
  • apps/mobile/src/lib/active-sessions-live.ts - no new issues
  • apps/mobile/src/lib/active-sessions-live.attention.test.ts - no new issues
  • apps/mobile/src/lib/hooks/use-session-raise-reader.ts - carried-forward finding re-verified as fixed
  • apps/mobile/src/lib/session-attention.ts - no new issues
  • apps/mobile/src/lib/session-attention.test.ts - no new issues
  • apps/mobile/src/components/agents/use-interaction-handlers.ts - no new issues
  • apps/mobile/src/components/agents/use-interaction-handlers.test.ts - no new issues

Previous review (commit f89c948)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The incremental revision adds a mounted tab-badge test for the same-status re-raise, which is sound; the unresolved risk remains that a tray row can pair a live attention status with a statusUpdatedAt from an earlier transition, so an in-app ack can name the wrong instant and clear prematurely.

Assumptions: [INFERENCE] I did not run the app or the test suite. The remaining finding is carried forward after a targeted re-check of current HEAD: mergeHeartbeatForActiveSessions sets status from the live WS row (effectiveStatus) while preserving the cached statusUpdatedAt (apps/mobile/src/lib/active-sessions-live.ts:253), and useSessionRaiseReader returns that pair unchanged (apps/mobile/src/lib/hooks/use-session-raise-reader.ts:45). The author's rebuttal covers applySessionStatusUpdated and attention→attention merges only, not a non-attention→attention heartbeat bringing a raise the DB has not recorded yet. No new subscriptions or listeners are introduced in this revision.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-session-raise-reader.ts 45 The reader pairs the tray row's status with statusUpdatedAt as if they described one transition. Status resolves from live-heartbeat-over-stored precedence (apps/web/src/lib/active-sessions-list.ts:280) and WS merges take the live status while preserving the cached time (apps/mobile/src/lib/active-sessions-live.ts:253 with readEnrichment); statusUpdatedAt remains the stored row's status_updated_at (active-sessions-list.ts:294-298). A raise the CLI reports before the DB records it therefore yields `{ status: 'question'
Files Reviewed (2 files)
  • apps/mobile/src/components/agents/agents-tab-badge.mounted.test.tsx - no new issues
  • apps/mobile/src/lib/hooks/use-session-raise-reader.ts - 1 carried-forward issue (targeted verification)

Fix these issues in Kilo Cloud

Previous review (commit 6d8b1ff)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The two prior ack-identity findings are addressed in this revision, but a tray row can still pair a freshly observed attention status with a statusUpdatedAt from the previous transition, so an in-app ack can be written against the wrong instant and clear prematurely.

Assumptions: [INFERENCE] I did not exercise the app on a device. The remaining issue is carried from the previous review after a targeted re-check: it requires a CLI heartbeat/sessions.list frame to report question/permission before the DB row records that status (the author's rebuttal only covers the attention→attention and session.status.updated paths). No new subscriptions or listeners are introduced in this revision, so no memory-leak concern was found.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-session-raise-reader.ts 45 The reader pairs the tray row's status with statusUpdatedAt as if they described one transition. Status resolves from live-heartbeat-over-stored precedence (apps/web/src/lib/active-sessions-list.ts:280), while statusUpdatedAt is always the stored row's status_updated_at (active-sessions-list.ts:294-298) and is preserved through WS merges even when a live row changes the status to attention (apps/mobile/src/lib/active-sessions-live.ts:253 with readEnrichment). A raise the CLI reports before the DB records it therefore yields `{ status: 'question'
Files Reviewed (13 files)
  • apps/mobile/src/app/(app)/(tabs)/_layout.tsx
  • apps/mobile/src/components/agents/agents-tab-badge.mounted.test.tsx
  • apps/mobile/src/components/agents/remote-session-row.tsx
  • apps/mobile/src/components/agents/session-preview-header.tsx
  • apps/mobile/src/components/agents/session-row.tsx
  • apps/mobile/src/components/agents/use-interaction-handlers.test.ts
  • apps/mobile/src/components/launcher-surfaces-mount.tsx
  • apps/mobile/src/lib/glanceable/approve-ask.test.ts
  • apps/mobile/src/lib/glanceable/attention-rows.ts
  • apps/mobile/src/lib/glanceable/widget-actions.test.ts
  • apps/mobile/src/lib/hooks/use-session-raise-reader.ts - 1 issue
  • apps/mobile/src/lib/session-attention.test.ts
  • apps/mobile/src/lib/session-attention.ts

Previous review (commit 5b28440)

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

The new raise-bound ack is not honoured by the status-only observers, so a same-status re-raise can stay hidden and a tray row whose status and statusUpdatedAt describe different points in time can clear an ack the user just wrote.

Assumptions: [INFERENCE] I did not exercise the app on a device; the PR body's device/E2E claims are taken at face value. The in-app ack reads the tray-cached raise after manager.answerQuestion resolves, so a raise landing during that round-trip could still be adopted (narrow window, unproven). No new subscriptions or listeners are introduced, so no memory-leak concern was found.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/session-attention.ts 462 The status-only fallback matches any ack whose status equals the caller's status. Every live observer (tab badge, tray row, launcher, isAnsweredAttentionRow) passes the status even when the row carries statusUpdatedAt, so a same-status re-raise stays resolved to answered/idle until the ack expires or a timestamp-bearing history row reconciles it.
apps/mobile/src/lib/hooks/use-session-raise-reader.ts 45 The reader pairs the row's resolved live/stored status with the stored row's statusUpdatedAt. For a raise the CLI reports before the DB records it, the timestamp belongs to the previous idle/busy status; the ack then names the wrong instant and a later timestamped reconcile clears the answer's ack.

SUGGESTION

File Line Issue
apps/mobile/src/lib/session-attention.ts 309 trackedRaise also reuses the raise of an already-acked entry, so a no-timestamp caller that answers a new raise of the same status makes the ack a no-op (and does not refresh its expiry). The reuse only makes sense for a cleared entry, which tracks the current unacked raise.
Files Reviewed (22 files)
  • apps/mobile/src/components/agents/agents-tab-badge.mounted.test.tsx
  • apps/mobile/src/components/agents/session-detail-content.test.ts
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/session-detail-queue.test.ts
  • apps/mobile/src/components/agents/session-row.mounted.test.tsx
  • apps/mobile/src/components/agents/use-interaction-handlers-scoping.test.ts
  • apps/mobile/src/components/agents/use-interaction-handlers.test.ts
  • apps/mobile/src/components/agents/use-interaction-handlers.ts
  • apps/mobile/src/lib/glanceable/approve-ask.test.ts
  • apps/mobile/src/lib/glanceable/approve-ask.ts
  • apps/mobile/src/lib/glanceable/approve-front-agent.test.ts
  • apps/mobile/src/lib/glanceable/approve-front-agent.ts
  • apps/mobile/src/lib/glanceable/attention-rows.test.ts
  • apps/mobile/src/lib/glanceable/waiting-ask.test.ts
  • apps/mobile/src/lib/glanceable/waiting-ask.ts
  • apps/mobile/src/lib/glanceable/widget-actions.test.ts
  • apps/mobile/src/lib/glanceable/widget-actions.ts
  • apps/mobile/src/lib/hooks/use-session-raise-reader.ts - 1 issue
  • apps/mobile/src/lib/notification-action-interaction.test.ts
  • apps/mobile/src/lib/notification-action-interaction.ts
  • apps/mobile/src/lib/session-attention.test.ts
  • apps/mobile/src/lib/session-attention.ts - 2 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-flash-latest · Input: 101.2K · Output: 31.8K · Cached: 2.2M

Review guidance: REVIEW.md from base branch main

The tray badge, launcher surfaces and Agents rows read a row they already
have the status time for, so reading the ack by status alone kept a
same-status re-raise hidden. `isAttentionAcked` now takes the raise a
reader sees (identity plus status): a row that cannot be dated, or an ack
written from a status-only answer, still matches by status, while a dated
row is matched against exactly the raise it names.

An ack no longer reuses a raise the store has already answered; only a
cleared entry tracks the raise still waiting for an answer.
Mounts the real badge with tray rows that carry their status time, acks
the dated raise, then pushes a later permission under the same status and
expects the count back.
@iscekic

iscekic commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re-verified on the emulator after the reviewer-driven change to how readers match a raise (the
reader now takes the row's raise identity plus its status).

Ran the same flow again on the real app against the local stack with the published CLI as the remote
agent and the deterministic fake model:

  1. A question was raised; the Agents row read NEEDS INPUT and the tab badge counted one.
  2. Answering it in the app cleared the badge, and it did not come back for the answered raise.
  3. A later question brought the badge back.

Row crops read NEEDS INPUT, then JUST NOW, then NEEDS INPUT; the tab badge's pixel area is
red-dominant only in the two captures where a raise is waiting.

Agents row needs input for the first question

Agents row after answering, no needs-input badge

Agents row needs input again for the later question

@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. labels Oct 22, 2026
@iscekic iscekic removed merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. labels Oct 22, 2026
@iscekic

iscekic commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Verified on the real Android development app with an owned fake account, an isolated local backend/database, and the published CLI 7.8.15 running as the remote. Optional analytics was disabled; inference used the deterministic local fake model.

Observed on the final implementation:

  • An unanswered question showed NEEDS INPUT on the Agents row and 1 on its tab badge.
  • Answering through the app cleared attention. It remained cleared through a manual pull-to-refresh and a 45-second observation spanning multiple CLI heartbeats.
  • A private observer fetched the actual relay snapshot immediately after explicit idle events: it returned idle in 89 ms and 194 ms, both before the next heartbeat.
  • A later question was answered through the app. After force-stop/relaunch, attention remained cleared; a new unanswered question then restored the row indicator and tab badge.

This run initially reproduced a real failure: a refresh could replace a newer idle state with an already-answered question from an older relay snapshot. The final checks above were performed after the relay snapshot fix, without temporary source instrumentation.

Limitations: permission responses and forced delayed-answer ordering were not exercised on device. A pending question's answer card did not reliably restore after leaving/reopening its detail during this run, so the final answers were submitted while the real detail remained mounted; pending-interaction replay is not claimed as verified here.

Attachments are focused composites of the session-row and Agents-tab regions from real captures, omitting unrelated navigation. They show only owned fake-session data: attention before answering, the cleared state after restart, and a new attention request after restart.

Unanswered question row and Agents badge

Cleared row and Agents badge after restart

New question row and Agents badge after restart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain janitor:features-workflows Janitor finding family: features-workflows merge:human Substantive functional risk requires human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants