mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-19 21:09:50 +00:00
0f7a3da68bc45ebb817135fc6fc2f1f13f75d3dd
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f0e5ba9c0f |
feat(codex): mid-turn Steer via app-server turn/steer (#888) (#1606)
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(steer): persist indeterminate outcomes without replay * fix(steer): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(steer): keep ACK failures held and reconcile busy cancel * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(store): reserve schema v25 for steer delivery state * fix(steer): keep held cancel state and notify requeue * fix(steer): release explicitly cancelled unknown reservations * fix(codex): reject cancelled reservations before native steer * fix(codex): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(steer): own abandoned app-server lifecycle and consume races * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones |
||
|
|
53e33bead1 |
merge: B-M4a Android FCM push + notification actions
# Conflicts: # android/app/src/main/kotlin/app/hapi/companion/MainActivity.kt # android/app/src/main/kotlin/app/hapi/companion/di/AppGraph.kt # android/app/src/main/res/values/strings.xml |
||
|
|
0f3bf5ca1b |
merge: B-M4d Android scratchlist
# Conflicts: # android/app/src/main/kotlin/app/hapi/companion/Navigation.kt # android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatScreen.kt # android/app/src/main/kotlin/app/hapi/companion/feature/chat/composer/ChatComposer.kt # android/core/data/src/main/kotlin/app/hapi/data/api/HapiApi.kt |
||
|
|
bf63d2ad7b |
feat(android): FCM push + notification actions (B-M4a)
Gradle/Firebase: firebase-messaging + work-runtime-ktx in the catalog and
:app; com.google.gms.google-services applied CONDITIONALLY (only when
app/google-services.json exists) so the repo builds green without any
Firebase config; committed google-services.json.example + README section
(CI-injected official builds / self-build drop-in / v1.x runtime
FirebaseOptions path); real config gitignored. push/PushBinding.kt is the
availability seam: no Firebase -> every push path no-ops.
Registration (:core:data push/DeviceRegistrar): stable DataStore UUID
deviceId; POST /api/devices/register {token, platform:'phone', deviceId}
fanned out to EVERY paired hub on app start, on pairing (roster addition),
and on onNewToken; transient failures retry via a per-hub WorkManager
unique work item; best-effort DELETE on sign-out while the hub's JWT still
works.
Service (fcm/HapiFirebaseMessagingService): data-only contract v1 decoding
in :core:data push/PushPayload — channels permission_requests(HIGH) /
ready / task_notifications (created on app start), type-<sessionId>
coalescing tags, severity accent colors, notifySummary-driven ready
bodies, unknown type/contractVersion degrade to plain title/body (default
channel, no actions). Suppress-when-open: foreground + that session's
chat composed -> skip (SSE already shows it). Tap -> internal MainActivity
intent route (no public URI) -> existing navigation opens the chat.
Actions: permission-request Allow/Deny and ready/task RemoteInput Reply +
Dismiss -> NotificationActionReceiver -> expedited CoroutineWorkers
(approve/deny {} bodies, reply {text, localId}) through on-demand
HubSessions built from stored credentials (HapiWorkerFactory +
Configuration.Provider + on-demand WorkManager init — no HubGraph needed
in background); notification updates pending -> done / "Already handled"
(404 request-gone) / inactive / failed. Multi-hub: payload has no hub URL,
so workers try the ACTIVE hub first, then other paired hubs on 404
session-miss (single-hub users always hit first try).
Hub check: android.priority=HIGH is already set unconditionally for every
FCM message (hub/src/fcm/fcmService.ts) — no hub change needed.
Tests (34 new, :core:data): payload keys/severities/unknown contract
version, channel routing, suppress-when-open; registrar fan-out /
addition-only / retry-on-transient / null-token no-op via fake seams;
action wire bodies + Bearer header + multi-hub resolution through a real
HubSession against two MockWebServers. Full gate green with AND without
google-services.json (protocol 227, data 182, app 96 tests; debug +
release/R8/lintVital assemble).
|
||
|
|
8e5ec6a3cb | merge: B-M3f Android composer attachments | ||
|
|
33d186f444 |
feat(android): scratchlist (B-M4d)
Per-session notes/drafts workbench, the native twin of the web ScratchlistPanel + use-hub-scratchlist (tiann/hapi#893): - wire/ScratchlistApi.kt: entry/attachment/limits DTOs mirroring shared/src schemas, caps (200 entries / 10k chars), typed error codes (scratchlist_at_cap, _attachment_too_large, _attachment_in_use, ...). - HapiApi: entries CRUD (POST idempotent-on-entryId), limits GET, base64-JSON attachment upload, raw-bytes fetch over the cached image client, attachment delete. - SessionStore now surfaces scratchlistUpdatedAt patches as a scratchlistInvalidations SharedFlow (bare refetch trigger, sse.md). - ScratchlistStore: per-session entries StateFlow, refresh on open + on SSE signal (observed sessions only, 16 ms coalesced), optimistic create/update/delete with surgical rollback, friendly atCap state (local pre-check + hub 409), uploads-in-flight progress, cached limits with offline defaults. ScratchlistAttachmentGuard holds the pure pre-upload budget verdicts (fits / downscale-to-target / reject). - feature/scratchlist: chat/{id}/scratchlist route, entry cards (text preview, relative age, authed Coil thumbnails), edit sheet (text + attachment strip with photo picker, downscale-to-JPEG import, remove, delete), FAB new note, full-screen viewer (generated-image pattern). - Chat seams: top-bar notepad badge (entry count), composer overflow "Park draft to scratchlist" (clears only after the hub accepts), and per-entry "To composer" that inserts into the live ChatViewModel of the chat entry below the route. Tests: store CRUD optimistic/rollback + cap 409 + invalidation-signal refetch + upload progress/413 + attachment delete 409 (MockWebServer), guard verdicts, ChatViewModel park/insert/badge seams (fake store). |
||
|
|
ac5cebf32f |
feat(android): composer attachments (B-M3f)
Composer attachment tray wired end to end: "+" bottom sheet (photo library / camera / files), upload-on-pick against POST upload (JSON+base64), per-chip uploading -> ready/failed states with retry and best-effort delete-on-remove, AttachmentMetadata riding SendMessageRequest and the optimistic row so user bubbles thumbnail instantly, and a UserTextBlockView upgrade decoding wire previewUrl data URLs (web-sent messages thumbnail too). Mobile-data compression policy (differs from web, which uploads originals): recompressible images over 4 MB downscale to 2048 px JPEG q85 (filename swaps to .jpg); GIF/SVG/non-images keep originals; hard 50 MB reject with a notice, plus a capped read guarding unknown-size picks. previewUrl embeds a <=512 px JPEG thumb instead of the web's full-size data URL to keep send bodies small. Simplifications noted in KDoc: attachments do not persist in drafts v1 (holder onCleared discards un-sent uploads after best-effort deletes); inactive sessions fail the upload chip until a text send auto-resumes. Scheduled sends guard the wire constraint (scheduledAt excludes attachments) with a loud check. Seam: ChatSessionApi now extends AttachmentUploadApi (HapiApi methods gain override); camera captures use a FileProvider cache scratch, rememberSaveable across rotation. Tests: pure policy decisions (plan/sample-size/filenames/data URLs), controller state machine incl. mid-upload removal orphan cleanup, VM send/retry/refusal flows with metadata assertions, MockWebServer wire shapes for upload + delete. Full gate green (protocol/data/app tests + assembleDebug). |
||
|
|
75f830002b |
merge: B-M4e Android usage/storage dashboards + settings
# Conflicts: # android/app/src/main/kotlin/app/hapi/companion/Navigation.kt |
||
|
|
769e0ff390 |
feat(android): usage/storage dashboards + settings scaffold (B-M4e)
- wire: UsageApi.kt (UsageSummaryResponse/buckets/totals, SqliteStorageUsageResponse) - api: GET /api/usage/summary?range&timeZone + GET /api/storage/sqlite on HapiApi - feature/settings: settings home (theme system/light/dark/OLED + Material You toggle, language persist-only until M5, About with app/protocol/hub health), usage dashboard (range control, 8 stat tiles, Canvas daily bars with tap tooltip, byAgent/byModel bar lists), storage dashboard (Canvas donut + rows) - owner gating: JwtPeek ns == 'default' hides Usage/Storage entries (web SettingsNav parity); screens map 403 to an owner-only explanation - theme plumbing: ThemePrefs/LanguagePrefs on hapi_prefs DataStore, exposed via AppGraph, applied at MainActivity setContent through HapiTheme(oled/dynamic) - tests: usage/storage wire decode, HapiApi query+403 shapes, formatting/ hit-rate/calendar-fill/slice math, theme+language round-trips, ns gating, usage/storage/settings viewmodels |
||
|
|
210f9b3ff7 |
feat(android): files/git browser + file viewer (B-M4c)
:core:protocol git/: GitStatusParser (porcelain v2 --branch: branch headers
incl. detached/initial, 1/2/u records, untracked/ignored) + NumstatParser
(counts, binary, brace + plain rename normalization) + buildGitStatusFiles
merge — behavior cross-checked against web/src/lib/gitParsers.ts by running
the same inputs through the TS implementation, quirks preserved.
Endpoints: HapiApi gains git-status, git-diff-numstat(?staged),
git-diff-file(path, staged?), file read (base64), files search (?query&limit),
directory (?path); wire types in new wire/FilesApi.kt.
feature/files: FilesScreen (chat/{id}/files) with Changes (branch header,
staged/unstaged sections, status letters + ±counts), Browse (lazy directory
tree, dirs-first sort, hidden-file toggle), Search (300ms debounced) tabs;
FileViewerScreen (chat/{id}/file?path&staged&mode&line) with diff mode
(UnifiedDiffParser → DiffView, staged/unstaged toggle) ⇄ full mode (CodeBlock
with 400-line highlight cap, markdown Source/Preview via the shared Markdown
renderer, image bitmaps), copy path, middle-ellipsis path. Chat wiring: file
citations open the viewer in full mode (cited line shown as a hint chip — no
per-line highlight inside the single-Text CodeBlock), top-bar folder icon
opens the files browser.
Tests: parser fixtures (renames, binary, detached, conflicts, untracked
dirs) + VM tests with fakes (numstat merge, degraded numstat banner, lazy
directory loading/cache, hidden filter, search debounce, diff auto-fallback,
staged reload, base64/image/binary decode).
|
||
|
|
c164af0738 |
feat(android): dictation, slash commands, session ops (B-M3ce)
- Voice dictation: DictationController (seam over recorder + api, JVM-tested), MediaRecorder m4a/AAC recorder, provider discovery via GET /api/voice/transcription/providers (new HapiApi method + wire types), mic button + recording chip + RECORD_AUDIO flow in the composer, transcript appended with space separator (web appendTranscript twin). - Slash commands: '/' at start-of-input opens a dropdown merging metadata.slashCommands names with GET /slash-commands (RPC wins dedupe, exact>prefix>contains filter); tap inserts '/name '. Skills '$' deferred. - Session ops: SessionStore gains rename (optimistic + roll-forward), delete (optimistic + 409 restore), reopen (marks returned id active); list long-press sheet + chat top-bar overflow wire Rename/Reopen/Delete; chat reopen reuses the supersede path (window seed + draft move + ChatEvent.SessionSuperseded); 422 formatted via formatReopenError; inactive-session affordance bar above the composer. - Additive wire/API: TranscriptionProvidersResponse/TranscriptionProviderInfo, HapiApi.getTranscriptionProviders, ChatSessionApi.getSlashCommands, SessionListStore rename/delete/reopen. - Manifest: RECORD_AUDIO + microphone uses-feature required=false. README blurb. - Tests: DictationController (happy/no-provider/errors/cancel), slash merge/filter/trigger, store rename/delete/reopen (MockWebServer), VM reopen-supersede/delete/rename/slash suggestions; full gate green (protocol 227, data 148, app 96 tests; assembleDebug OK). |
||
|
|
ca138c912c | merge: B-M3ab Android composer + permission actions + session config | ||
|
|
78a5ff9961 |
feat(android): composer, permission actions, session config (B-M3ab)
Interaction layer turning the read-only chat into a working remote control:
- Composer: multiline input bar with optimistic sends (appendOptimistic ->
POST -> status settle), queue-by-default delivery with a long-press
Send&Steer intent while a turn is active, abort button during thinking,
tap-to-retry on failed rows, per-session drafts (DataStore, hub-scoped
keys), attachment chip seam for M4.
- session_inactive (409) recovery: one POST /resume then retry; a
superseding session id seeds the new window, migrates the draft and
emits SessionSuperseded for renavigation (web resolveSessionId parity).
- Queued bar: uninvoked sends with Cancel (DELETE; invoked-race ingests the
authoritative row as sent), Edit (cancel + composer prefill, draft-kept
guard) and Steer (POST steer; invoked answers reconcile a missed consume).
reconcileQueuedState now runs on chat open and on session-pipe gap.
- Permission actions: flavor-exact bodies mirroring PermissionFooter.tsx --
claude {} / allowTools / mode:acceptEdits, codex-family decision:
approved / approved_for_session / abort -- plus AskUserQuestion flat
answers and request_user_input nested answers forms; optimistic
Resolving/AlreadyHandled overrides settled by the agentState patch.
- Session config sheet: catalog-driven permission-mode picker, claude
static model/effort catalogs (ported to :core:protocol catalog), codex
models via GET /codex-models (new HapiApi endpoint + wire types) with
per-model reasoning efforts; optimistic detail updates rolled back to
server truth on error.
- Lifecycle: ProcessLifecycleOwner -> SseEngine.setLifecycleForeground +
POST /api/visibility per subscription (VisibilityReporter fed by the new
SyncTargets.onHandshake hook); the global SSE pipe moved from the session
list VM to HubGraph lifetime (GlobalSsePipe) so queued/consumed
bookkeeping and list badges stay fresh while a chat is open.
- Tests: VM-level interaction suite (optimistic send/failure/retry,
inactive-resume both id paths, cancel invoked-race, steer, exact
approve/deny body JSON incl. both answers formats, config optimistic +
rollback, drafts) + GlobalSsePipe tests; full gate green (554 tests,
assembleDebug).
|
||
|
|
e640aa42fd |
feat(android): new session flow (B-M3d)
Machine -> directory -> agent/options -> spawn, web NewSession parity:
- feature/newsession: NewSessionScreen + NewSessionViewModel (plain class
over fakeable seams), NewSessionForm/Logic (exact SpawnSessionRequest
mapping), NewSessionGateway, DataStore-backed prefs (last machine,
per-machine recent paths LRU cap 8, form draft so backing out keeps input)
- directory: server-side autocomplete (list-directory on the parent path,
debounced 250ms with per-parent cache), recent-path chips, paths/exists
probe with the web's missing-dir affordances (worktree = error, simple =
two-tap create-and-make-directory)
- options per flavor: claude static models+effort; codex machine catalog
(hidden on rpc_target_missing) + reasoning effort + collaboration mode +
fast tier gate; grok/codex-family native permission modes; claude/agy/
cursor YOLO toggle with native-mode hint; pi managed note; copilot agent
mode; worktree name validation; startingMode omitted (remote default)
- wire/api: CodexModelsResponse + GET /machines/:id/codex-models (flagged
addition), MockWebServer coverage
- nav: newSession route (optional machineId), FAB on the session list,
spawn success navigate-replaces to chat/{id}
- tests: spawn-body exactness (4 configs), debounce/parent derivation/
listing cache, recent LRU, worktree validation, two-tap missing dir,
draft restore + codex catalog reconcile (21 new app tests green)
|
||
|
|
b23afe24fe | feat(android): read-only chat screen + store wiring (B-M2d2) | ||
|
|
dfcf9849b9 | merge: B-M2c Android message window store, pagination fixtures green | ||
|
|
99df45e99a |
feat(android): message window store port, pagination fixtures green (B-M2c)
:core:protocol window/ — pure state machine ported function-for-function from web/src/lib/message-window-store.ts + messages.ts: merge-by-(id|localId) with optimistic echo reconciliation, position ordering (invokedAt ?? createdAt, seq, ASCII id tie-break), trim-preserving-queued with the codex agent-run budget, epoch reset handling, latest-replace with request-baseline identity preservation, consumed/cancelled/queued-reconcile transitions, tail/history modes, hydrate/persist shapes. MessageRetention ports the null-decision tree of normalizeDecryptedMessage (dedup with the B-M2a pipeline port flagged). :core:data store/ — per-session MessageWindowStore (StateFlow + Mutex, single-flight tail sync with trailing drain, fetchOlder with epoch-reset resync, SSE ingest hooks, optimistic sends, queued-state reconciliation, seedFrom for resume id changes) + WindowSnapshots (atomic JSON files, LRU 10; JsonSnapshotStore dedup TODO) + MessageWindowStores registry. Minimal MessagesApi interface (sealed MessagesQuery) extracted over the two message endpoints, implemented by HapiApi. Gate: PaginationFixtureTest replays all 11 shared/fixtures/pagination scripts against the real store — requests, older-load outcomes, reconcile candidates and the final window projection all exact — 11/11 green; plus targeted concurrency/snapshot/seed unit tests. :app:assembleDebug green. |
||
|
|
f702cbf844 |
feat(android): session/machine stores + session list (B-M2b)
:core:protocol — summary-side patch path ported from the web reference: - wire/SummaryPatching.kt: patchSessionSummary port with the summary path's >= versioned gates (replicated web divergence vs the detail path's strict >, documented), derived-field recompute (pendingRequestsCount/Kinds/Requests capped 5, todoProgress), render-irrelevance filter (activeAt-only keep-alive suppression), toSessionSummary/toSessionSummaryMetadata projection, and the deprecated canApplyVersionedSummaryPatch legacy gate. - wire/SessionSorting.kt: exact sortSessionSummaries comparator (globalPinned > pinned > active > pendingRequestsCount among active > updatedAt desc; Long-safe, stable). - SessionMetadata grows the per-flavor agent session id fields the agentSessionId projection needs. :core:data store/ — StateFlow stores with per-hub JSON snapshots: - JsonSnapshotStore: debounced (500 ms) atomic tmp+fsync+rename snapshots, synchronous cold-start load, corrupt files degrade to null. - SessionStore: sorted summary list + per-id detail cache (strict-> detail patching via SessionPatching), full-session upsert preserving hub-computed scheduled fields, REST fallback for unparseable payloads, coalesced refresh (16 ms batch like the web), optimistic pin/archive. - MachineStore: full/patch/null machine-updated decision tree per sse.md. - LastSeenStore: per-session last-seen watermarks + once-per-scope baseline seeding + unread derivation (sessionLastSeen.ts/sessionAttention.ts port). - StoreSyncTargets: SyncEventRouter fan-in — global-scope message-stream events refresh the list, gap handshakes full-resync. :app feature/sessions/ — standalone screen + plain-constructor ViewModel: - SessionListScreen: pinned section, status dot with thinking pulse, flavor/ machine/worktree meta line, pending badge, todo chip, unread dot, machine filter chips, PullToRefreshBox, offline banner, empty states, long-press pin/archive sheet; taps emit onOpenSession only (no navigation). - SessionListViewModel: store combine -> UiState, owns the global SSE subscription while started (subscribe after collector registration), entry refresh, error SharedFlow for snackbars. Tests: SummaryPatching/SessionSorting JVM tests (cases mined from useSSE.test.ts), store tests (stale/equal/newer patches, full replace, keep-alive identity, snapshot round-trips, optimistic rollback), ViewModel combine + handshake tests with fake stores. All of :core:protocol:test, :core:data:test, :app:testDebugUnitTest, :app:assembleDebug green. |
||
|
|
f275bae4cb |
merge: B-M1c Android SSE engine + reconnect state machine
# Conflicts: # android/README.md # android/core/data/build.gradle.kts # android/gradle/libs.versions.toml |
||
|
|
483155ece3 |
feat(android): SSE engine + reconnect state machine (B-M1c)
:core:data app.hapi.data.sse — the /api/events transport per
docs/api/client-contract/sse.md (reference web/src/hooks/useSSE.ts):
- SseConnection: SseTransport seam (Connected/Event/Failure flow) with the
okhttp-sse implementation on a dedicated client (readTimeout=0, no cache,
own dispatcher), buildEventsUrl, Last-Event-ID header on resume, and an
acceptEncodingIdentity fallback flag for the gzip escape hatch.
- ReconnectPolicy: normative constants + pure backoff schedule (immediate
first retry, 1s..30s exponential, 300s ceiling after 8 attempts, 0..500ms
injected jitter).
- SseEngine: per-key (global / session:<id>) connection loops — handshake
gate on connection-changed{status:connected} with ok/gap resume verdict,
per-key cursors advanced only after the downstream hand-off
(at-least-once), 10s connect deadline, 90s staleness watchdog ticking 10s,
one silent 401 re-auth per cycle costing no backoff attempt, background
retry deferral + 45s foreground stale check; all timing via injected
delay/clock for virtual-time tests.
- SyncEventRouter: 13-type union fan-out to the SyncTargets seam (M2 wires
stores), handshake gap -> requestFullResync, Unknown ignored.
Tests (32): virtual-time engine suite (fake transport + turbine), policy
schedule with seeded jitter, router mapping, and MockWebServer integration
through the real okhttp transport — including proof that gzip SSE frames
surface incrementally (flush-per-event body withheld behind a throttle).
Gzip also verified against a live local hub (Content-Encoding: gzip,
handshake decoded instantly, heartbeat +30.0s mid-stream).
|
||
|
|
6f1dc23d77 |
feat(android): auth + HapiApi transport (B-M1b)
:core:data auth + API transport for track B M1:
- auth/: JwtPeek (unverified {uid,ns,exp} peek for proactive refresh),
CredentialStore interface + EncryptedPrefsCredentialStore
(security-crypto) + in-memory impl, HubUrls origin normalization,
HubRegistry (ordered multi-hub roster + active hub behind a storage
seam), AuthInterceptor + single-flight TokenAuthenticator (401 ->
re-exchange -> retry once, Mutex single-flight, terminal AuthEvents,
ensureFreshToken() for SSE pre-connect)
- api/: HapiApi (plain OkHttp + kotlinx.serialization, one suspend fun
per v1 endpoint incl. generated-image bytes + multipart transcription
helper), ApiError with (status, code) per errors.md
- HubSession: per-hub factory wiring clients (timeouts, auth decoration,
256 MB image cache), DI-free
- :core:protocol wire/: request DTOs (ApiRequests.kt) + response DTOs
(auth, health, resume/reopen, cancel/steer/queued-state, spawn,
slash-commands/skills, machine list-directory/paths-exists, uploads,
transcription)
- tests: 45 unit tests (MockWebServer) — refresh/retry-once, concurrent
single-flight, terminal 401 paths, ApiError code mapping, request
shapes (cursors+epoch, deliveryMode, nested answers, explicit nulls),
JwtPeek/HubUrls/HubRegistry
|
||
|
|
83bd25aa3b | feat(android): scaffold Compose app + core modules + CI (B-M0) |