mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-12 20:07:31 +00:00
17ee052d9a9063b332508a47e3bd56d784c09ba7
708
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
17ee052d9a |
fix(web): preserve the visible chat window during rewind (#1766)
* fix(web): preserve chat window during rewind * fix(web): scope rewind invalidation preservation * fix(web): clear unknown rewind boundaries * fix(web): deduplicate rewind invalidations * fix(web): retain rewind dedupe history |
||
|
|
9e954d7c1b | fix(web): center inactive session notice text (#1770) | ||
|
|
bb055ae74e | fix(web): preserve sidebar viewport across pin updates (#1776) | ||
|
|
3873e58496 |
fix(web): keep streamed reasoning/text block ids stable across snapshot rows (#1741)
* fix(web): keep streamed reasoning/text block ids stable across snapshot rows
Streaming snapshots of one stream (pi/codex reasoning and text) arrive as
separate message rows, and the window store retires older rows as newer
snapshots land. The timeline derived the block id from whichever row was
first seen, so the id (and the threadMessageId built from it) churned on
every snapshot, remounting the rendered reasoning panel mid-stream and
replaying its open animation — the panel visibly flashed/re-rendered on
every snapshot tick.
Derive the block id from the stream id when present (unique per stream,
stable across snapshot rows) so the block is updated in place and the
smooth streaming keeps appending to the previous text. Row-derived ids
remain the fallback for content without a stream id.
Also rerun gen:fixtures to refresh the two golden fixtures affected by
the new id shape.
* fix(ios,android): mirror stream-stable block ids in native chat ports
The native HapiKit (Swift) and protocol (Kotlin) chat pipelines are ports
of the web reducerTimeline and are pinned by the same golden fixtures in
shared/fixtures/chat. After the web-side change to derive streamed
reasoning/text block ids from the stream id, the ports still produced
row-derived ids, so the iOS/Android fixture conformance suites went red
on the two refreshed fixtures.
Apply the same streamId-first id derivation (row-derived fallback kept)
to both ports so all three pipelines project identical block ids.
* fix(web,ios,android): reject blank stream ids as block identity
Blank ('' or whitespace-only) stream ids are not streams per the wire
semantics in shared/src/messages.ts (readReasoningStreamId trims before
accepting). The previous nullish fallback let accepted payloads carrying
blank ids through, so every such row shared one empty block id: the
merge maps collided and assistant-ui occurrence suffixes churned with
list position, reintroducing remounts.
Normalize with a trim guard in all three pipelines (web, HapiKit,
protocol) and add a web regression test covering both empty and
whitespace-only ids.
* fix(ios): use normalized stream id for block construction identity
The blank-id guard was applied to lookup and map insertion but block
construction still read the raw optional, so accepted payloads carrying
blank/whitespace ids produced blocks sharing one blank SwiftUI identity
instead of falling back to row-derived ids (web/Android already used the
normalized local). Hoist the nonBlankStreamId result and reuse it for
lookup, block identity, and insertion in both the text and reasoning
branches.
Also add native coverage for stream identity: stream-id derivation for
text/reasoning plus blank ('' and whitespace-only) fallbacks, which the
golden fixtures do not exercise.
* fix(web): pin blank stream-id identity contract in golden fixtures
Update the two stale fixture descriptions (stream-keyed blocks are now
keyed by the stream id, not the first message) and add a generated
conformance fixture covering empty and whitespace-only codex data.id
values for both reasoning and text: blank ids are not stream identities,
so each payload keeps its own row-derived block id instead of collapsing
onto a shared blank identity. Web, iOS, and Android all run this same
golden fixture.
* feat(hub): make title provider max_tokens and timeout env-tunable
Reasoning models used as title providers (e.g. GLM thinking models) need
more than 64 completion tokens and more than the hardcoded 10s timeout to
emit a title, and the only workaround was patching the compiled binary
after every install.
Expose both knobs via HAPI_TITLE_PROVIDER_MAX_TOKENS and
HAPI_TITLE_PROVIDER_TIMEOUT_MS, following the existing
HAPI_TITLE_SUGGESTION_RATE_LIMIT pattern; defaults are unchanged.
* docs(hub): document title provider max_tokens/timeout env knobs
Add the two new HAPI_TITLE_PROVIDER_* variables to the title-provider
configuration table in the installation guide, and extend the provider
test to cover the timeout abort path (the signal fires and rejects the
in-flight request).
---------
Co-authored-by: HongChenGG <HongChenGG@users.noreply.github.com>
|
||
|
|
aa0c1dc808 |
fix(codex): fail closed on ambiguous Web Rewind boundaries (#1707)
* fix(codex): fail closed on ambiguous Web Rewind boundaries * fix(codex): reject malformed native rewind history * fix(codex): reject duplicate rewind identifiers * feat(codex): offer Fork fallback for ambiguous rewind * fix(codex): gate rewind Fork fallback on exact native boundary * fix(codex): reject unresolved rewind boundaries safely * chore: trigger PR checks * fix(codex): gate safe rewind fallback on fork support * fix(codex): require complete user ids for rewind fallback |
||
|
|
e5a8212f4a | feat(session): validate agents and browse workspace directories | ||
|
|
a9d09cb3ff |
feat(web): support composer attachment drag reordering (#1682)
* feat(web): support composer attachment drag reordering * fix(web): handle forward attachment reordering * fix(web): enlarge file attachment touch targets * fix(web): preserve staged attachment order |
||
|
|
be1ef2a2e4 |
feat(dsh): integrate DeepSeek Harness through ACP (#1632)
* feat(dsh): add DeepSeek Harness ACP flavor * fix(dsh): update mobile flavor catalogs * fix(dsh): keep mobile spawn policy managed * fix(dsh): keep managed policy and prompt retry * fix(dsh): suppress unsupported runner policy flags * fix(dsh): align native managed-policy UX |
||
|
|
661e9b4eb7 | feat(web): show Claude round usage metadata (#1655) | ||
|
|
dd2e978fe3 |
feat(web): add mark unread session action (#1649)
* feat(web): add mark unread session action * fix(web): keep explicit unread state on selected sessions * fix(web): address mark unread review feedback * fix(web): preserve newer manual unread state |
||
|
|
e3f13c4eb6 | feat(web): anchor composer settings sheet to the clicked value button's section (#1627) | ||
|
|
fdf4fdaa38 |
feat(web,hub): make large conversation export warning-only (#1648)
* feat(export): warn before large session downloads * fix(export): surface server size errors |
||
|
|
338e405442 | fix(brand): sharpen iOS and PWA icons | ||
|
|
0aebf39c78 |
fix(opencode): keep one stored message per reasoning stream (#1643)
* fix(acp): carry the live reasoning marker on the wire payload ACP agents stream thoughts a token at a time, so the handler coalesces them into a buffer and re-sends the whole buffer under a stable stream id every 250ms. The converter dropped the marker that says a payload is one of those throttled snapshots, leaving the hub unable to tell a replaceable snapshot from the settled message that closes the stream. Mirrors how the text variant already forwards streamSnapshot. * fix(hub): keep one stored message per reasoning stream OpenCode reasoning arrives as a series of growing snapshots sharing one stream id, and every snapshot was persisted as its own message. A 26h session reached 48,844 rows and 63MB, and because the web budgets a fixed number of messages, its 400-message window covered barely three minutes of conversation — scrolling up walked through duplicate snapshots instead of history. Retire a stream's earlier live snapshots once their replacement is stored. Sweeping only after the insert matters: the two statements are separate transactions, so clearing first would leave a window where a crash takes the whole stream. Only rows marked live are eligible and the replacement is spared, so a stream always keeps at least one row and the settled message that closes it is never removed. Live rendering is unchanged: the web still receives every snapshot and already folds them by stream id. * fix(web): spend the message window on conversation, not repeated snapshots The window budgets raw messages, but a reasoning stream renders as a single folded block no matter how many snapshots it arrived in. On sessions recorded before the hub started retiring them, those snapshots fill the window on their own: in one 26h session the newest 400 messages covered 202 seconds, so scrolling up paged through duplicates instead of history. Collapse each stream to its newest snapshot before trimming. Rendering is unchanged — the timeline already folds them by stream id — and rows without a stream id are never touched. * fix(ios,android): port reasoning-snapshot compaction to the native windows The window logic in HapiProtocol and :core:protocol is a one-to-one port of the web store, so collapsing superseded reasoning snapshots only on the web left the native windows budgeting raw snapshot rows. The hub stores one row per stream now, but a client that already holds the older snapshots still spends its window on them. Add the same stream-id reader and compaction to both ports, in the shape each already uses for agent-run rows, and pin the behaviour with a pagination fixture. Both fixture suites enumerate shared/fixtures/pagination from disk, so the ports cannot drift from the web again without CI saying so. |
||
|
|
9bdccf0cff | fix(web): unify message action buttons (#1645) | ||
|
|
0f7a3da68b |
feat(cursor): mid-turn Steer via concurrent ACP session/prompt (#888) (#1609)
* 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) * feat(acp): split request dispatch from completion and add soft steer - AcpStdioTransport.sendRequestWithDispatch separates stdin-accepted dispatch from the JSON-RPC response, keeping sendRequest behavior unchanged - AcpSdkBackend tracks concurrent session/prompt requests with an activePromptRequests counter (main prompt + soft steers); response completion stays pending until every concurrent prompt settles - beginSoftSteerPrompt kicks off a concurrent session/prompt (Cursor GUI Send semantics — no cancel, no handler swap) returning {dispatched, completed}; softSteerPrompt awaits the full response for direct callers * feat(cursor): mid-turn soft steer via concurrent session/prompt (#888) - CursorAcpRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, rejects control commands and mode mismatches, then soft-injects via beginSoftSteerPrompt without canceling the in-flight turn - Acks the hub once stdin accepts the inject (not on turn completion) to stay inside the 30s RPC window; the launcher stays busy until the concurrent prompt settles so handlers are not swapped mid-inject - steeringActive agent state mirrors the active-turn window; abort and cleanup reset it and invalidate pending steers - Legacy stream-json Cursor sessions register a steer handler that reports unsupported * 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 * feat(shared): advertise cursor in the steer gate now that its handler lands Cursor ACP sessions pass the web and hub steer gates; legacy stream-json cursor sessions stay excluded. * 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(cursor): consume the row at dispatch; keep waiters for prompt gating - The hub RPC acks and the queue row is consumed as soon as stdin accepts the concurrent session/prompt; completion is background-only. A dispatched steer is never restored (no duplicate via the next prompt) - softSteerWaiters are registered before awaiting dispatch so the main loop's finally cannot start the next prompt mid-inject; they still gate prompt handover on completion - tests updated: post-dispatch ACP rejection keeps the row consumed * fix(cursor,hub): never hang teardown on unresolved soft steer; align diagnostics - Prompt-finally waits for soft-steer completion only when not exiting; the outer finally no longer waits at all — cleanup() disconnects the ACP transport, which rejects pending requests and settles the waiters - syncEngine gate diagnostics and JSDoc name all supported flavors (Pi, Codex, Cursor ACP) - regression test: Switch with an unresolved soft-steer completion still reaches teardown * 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(cursor): abort drops soft-steer waiters so the next prompt never blocks - Ordinary Abort (shouldExit false) now clears softSteerWaiters: the prompt finally cannot wait forever on a soft steer whose completion is unbounded; the ACP cancel rejects in-flight requests, and cleanup() settles leftovers on session end - regression test: unresolved soft-steer completion after Abort no longer blocks the next prompt * 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(cursor,acp): abort force-settles soft-steer bookkeeping - AcpSdkBackend.abortSoftSteers() drops the concurrent-prompt counter and notifies response-complete so the next turn's waitForResponseComplete() cannot block on a soft steer that will never settle after abort - handleAbort calls it before clearing the waiters; the main prompt's own finishPromptRequest stays guarded by Math.max(0, ...) - unit tests cover counter release and no-op when idle * 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 * test(acp): match finishPromptRequest epoch signature in whitebox test * 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(cursor): abort releases an in-progress soft-steer wait - The prompt-finally wait races Promise.allSettled against the abort signal: an Abort that clears the waiters now also releases a wait that already started, so the launcher always reaches the next queued prompt * 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(cursor,acp): commit on ACP acceptance; distinguish transport failures - AcpStdioTransport marks transport-level failures (timeout, closed, stdin write) as indeterminate; explicit JSON-RPC error responses are not - The steer handler commits + consumes on completion (ACP acceptance) and restores the row on an explicit rejection; an indeterminate transport failure keeps the row reserved so a delivered instruction is never re-sent, and the ACK reaches the hub even when abort reset the queue - launcher/transport tests updated for the three outcomes * 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 * test(cursor): match tri-state cancel contract for dispatching steers * fix(cursor): drop duplicate promptInFlight declaration after upstream merge * 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(cursor,acp,web): indeterminate close marks, steer gating precision - AcpStdioTransport.rejectAllPending marks close/protocol failures indeterminate, so an accepted-but-close-interrupted soft steer restores nothing (no duplicate delivery) - the abort race in the soft-steer wait removes its listener in finally (no accumulation across repeated waits) - SessionChat gates the Steer button on agentState.steeringActive for codex/cursor instead of the queued-grace thinking flag, so Steer is not exposed before the launcher can accept it - codex pre-dispatch abort never enters reconciliation (merged from #1606) * fix(steer): persist indeterminate outcomes without replay * fix(cursor): hold ambiguous steers for explicit resolution * fix(steer): make ambiguous delivery restart-safe * fix(cursor): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(cursor): reject steers when prompt generation changes * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(cursor): preserve soft-steer reservations across abort * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(cursor): hold ambiguous dispatch failures * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(cursor): bound ACP dispatch acknowledgements * fix(steer): keep ACK failures held and reconcile busy cancel * fix(cursor): preserve state when dispatch ACK is uncertain * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(cursor): distinguish held cancel from removal * fix(store): reserve schema v25 for steer delivery state * fix(cursor): suppress late ACP updates after abort * fix(steer): keep held cancel state and notify requeue * fix(cursor): isolate late updates after abort * fix(steer): release explicitly cancelled unknown reservations * test(cursor): cover explicit held cancellation * fix(codex): reject cancelled reservations before native steer * fix(cursor): reject cancelled reservations before ACP steer * fix(codex): make reservation restore atomic with state * fix(cursor): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(cursor): hard-stop abandoned writes and update native queue state * fix(steer): own abandoned app-server lifecycle and consume races * fix(cursor): isolate aborts and add native retry resolution * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(native): reconcile retry responses * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): resync busy cancel outcomes * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(cursor): hold restore failures for explicit resolution * 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(cursor): drain foreground prompt after soft-steer abort * 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 * fix(cursor): drain soft steers before handler replacement * fix(cursor): preserve buffered output on abort |
||
|
|
47c0768c27 | feat(brand): refresh HAPI icons across platforms | ||
|
|
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 |
||
|
|
2fbd98dfe4 |
fix(pi): offer model-accurate thinking levels in the create-session form (#1626)
* fix(web): filter Pi effort options by model thinkingLevelMap in new session form The create-session EffortField called getPiThinkingLevelOptions without the selected model's thinkingLevelMap, so the Pi effort select always showed the static off..high list: levels the model marks unsupported stayed visible and xhigh/max never appeared even for models that opt in. Pass the map through piSelectedModel (mirrors HappyComposer), and extend the stale-effort reset effect so a level unsupported by the newly selected model falls back to auto. * feat(cli): probe machine Pi models over RPC to carry thinkingLevelMap The machine-level Pi model probe parsed the `pi --list-models` text table, which only exposes provider/model/thinking-yes-no — thinkingLevelMap (and name/contextWindow) never reached the create-session form, so model-accurate thinking levels could not render there (xhigh/max are map-opt-in and were permanently hidden; see the companion web commit). Replace the table probe with a short-lived `pi --mode rpc` child (--no-session --no-extensions --no-skills --no-prompt-templates --no-tools) that issues get_available_models and reuses the session path's parsePiModels schema, so machine and session catalogs share one wire contract. The lightweight probe also measures faster than the table probe (~0.8s vs ~1.6-2.4s) and drops the whitespace-table parsing entirely. Cache/inflight dedupe/timeout structure is unchanged. * fix: address review findings on the Pi model probe and effort reset Three review findings on the RPC probe / effort-map change: - (high) Probe teardown killed only the direct child PID: with shell:true on Windows that is the shell, orphaning the interactive pi RPC process on every successful probe and on timeout; finish() also resolved before the process was confirmed gone. Use killProcessByChildProcess (taskkill /T on Windows, Unix tree kill, SIGKILL escalation) and settle only after the tree teardown completes. - (medium) An explicit get_available_models success:false response was discarded, so Pi's own error text was lost and the interactive child hung until the generic 15s timeout. parsePiModelsProbeLine now returns a three-way result (unrelated/models/error), also validating the response id, and the probe rejects immediately with Pi's error. - (medium) Switching an xhigh/max-capable model back to Default left the now-hidden effort in state and submitted it while the select visually fell back to auto. The reset effect now also covers model === 'auto' (undefined map) while still not resetting mid-resolve for a concrete model. Tests: probe-line failure/foreign-id cases; NewSession restore-to-Default reset and map-opt-in retention (the latter guards the former against a false green from the restore path). * fix(web): type the Pi model test mock as PiModelSummary The inline mock element type omitted thinkingLevelMap, so the new capability-driven tests failed typecheck (TS2353) and the required test job stopped before the unit tests ran. Use the shared PiModelSummary type so the mock cannot drift from the wire contract again. * fix(web): reconcile hidden Pi effort when model discovery fails A restored explicit Pi model never resolves when the machine catalog request fails, so piSelectedModel stays null for good. The reset effect required a resolved model, so it skipped reconciliation, while EffortField rendered with an undefined map and hid xhigh/max. Creation is only gated on the loading state, not on the error, so handleCreate could still forward the stale hidden level for Pi to reject or clamp. Treat a failed catalog as a settled selection (alongside Default and a resolved model) and reconcile against the undefined map; keep skipping the reset while a concrete model is still resolving without an error, so a restored xhigh/max survives until the map can prove it valid. Test asserts the spawn payload carries no effort after a failed catalog with a restored xhigh; verified it fails when the error branch is reverted. * fix(cli): honor a failed probe process-tree teardown killProcessByChildProcess reports survivors by resolving false, but the probe discarded that result and settled anyway. The Windows graceful path is taskkill /T without /F and escalates nothing on its own, so a probe child that refuses the signal would be reported as cleaned up and the catalog cached, letting interactive Pi processes accumulate across refreshes. Escalate to the forced teardown when the graceful one reports survivors, and reject (caching nothing, so the next call re-probes) when even that fails. Skip the check when the child has no pid: nothing can leak, and replacing a spawn ENOENT with a teardown error would only obscure the real failure. Adds probe lifecycle tests (spawn + teardown helper mocked) for the escalation path, the reject-and-do-not-cache path, the no-pid spawn-failure path, and the timeout path; verified they fail when the escalation is reverted. * fix(cli): keep extensions enabled for the Pi model probe --no-extensions silently dropped providers contributed through pi.registerProvider, which the old `pi --list-models` probe did list. Users with such an extension would have lost those models in the create-session form only. Verified with a project-local .pi/extensions provider in the same cwd: the probe with --no-extensions returned 29 models, while both the default run and the old table probe returned 30 including the extension's model (and its thinkingLevelMap). Dropping the flag restores parity; the extension provider now comes back with its map intact. Discovery that cannot contribute models stays disabled (--no-session, --no-skills, --no-prompt-templates, --no-tools). Cost: the probe now measures ~1.4-2.0s instead of ~0.65s, still at or below the old table probe (~1.6-2.4s) and fronted by the existing 60s cache. * fix(cli): probe Pi models from the home directory, not the runner cwd Under launchd/systemd the runner cwd is `/`, and starting Pi there is pathological: project discovery plus extensions that scan from the working directory walk the whole filesystem root. With extensions enabled (required so pi.registerProvider models still surface) the RPC probe took 16.8s at cwd=/, past the 15s timeout, so machine-level model discovery failed 100% of the time and the create-session form showed only 'Pi model discovery timed out'. Measured on macOS with 9 global extensions: cwd=/ extensions on 16.8s -> timeout cwd=/ extensions off 0.6s -> would lose extension providers cwd=$HOME extensions on 1.4s -> 29 models, complete The catalog is machine-scoped and does not depend on cwd, so probing from the home directory is both safe and representative. Verified from cwd=/ under the runner's exact launchd environment: 1.3-1.7s, 29 models. The old `pi --list-models` probe was immune because it never initialized a session; this regression arrived with the RPC probe and was missed because every earlier verification ran from a project directory. Tests assert the spawn cwd is homedir() and that --no-extensions stays absent; verified the cwd test fails when the option is removed. * fix(cli): keep the probe in the runner cwd, fall back to home only at a root Forcing every probe to the home directory fixed the launchd timeout but broke project-local discovery: a runner started inside a project stopped seeing that project's .pi/extensions providers, which the replaced --list-models probe did surface. Verified with a project-local provider: present from the project dir (30 models), absent from home (29). Use the runner cwd normally and fall back to home only when cwd is a filesystem root -- the launchd/systemd case where Pi startup walks the whole tree (16.8s, past PROBE_TIMEOUT_MS) and where there is no project to lose anyway. process.cwd() can also throw for a deleted directory, so that falls back to home too. Verified under the runner's launchd environment: from / -> home, 1.6s, 29 models; from a project dir -> that dir, 1.4s, 30 models including the project-local provider. Tests cover all three branches and were checked in both directions: pinning to home fails the project-cwd test, pinning to cwd fails the root test. |
||
|
|
f110845be6 |
merge: K6 fixtures batch 2 (events, permissions, sidechains, flavors, catalogs)
# Conflicts: # shared/fixtures/README.md # web/scripts/fixtures/generate.ts |
||
|
|
e1bfdd6265 |
feat(fixtures): SSE patch + pagination scenario fixtures (K7)
Two new golden-fixture suites, expectations machine-generated from the web
implementation (same source-of-truth principle as the chat suite):
- shared/fixtures/sse/ (12 cases): session-updated versioned-patch
application via applySessionDetailPatch — strict version gates for
metadata/agentState/todos/teamState, out-of-order arrival, teamState:null
clear, max-monotonic updatedAt, flat last-write-wins fields, sub-minute
activeAt keep-alive drop, scratchlistUpdatedAt trigger, and the pinned
actual behavior that activeTurnStartedAt is NOT applied by the patch path.
Inputs are stored schema-normalized and validated against SessionSchema /
strict SessionPatchSchema at generation time; per-patch applied/unchanged
verdicts are part of the contract.
- shared/fixtures/pagination/ (11 cases): op scripts driving the real
message-window store with a scripted ApiClient — latest page + SSE ingest,
before-cursor older pages, epoch-mismatch reset (window discard + recorded
internal latest request), reset:true replace preserving optimistic rows,
localId echo reconciliation, messages-consumed invokedAt stamping (no
cursor advance), message-cancelled removal, cancel-too-late invoked-row
ingest, hidden-row cursor advance, 400-row trim preserving queued rows,
and queued-state gap recovery. Documents pin the exact requests the store
issued, older-load outcomes, reconcile candidates, and a minimal window
projection (ids/order, queued/optimistic flags, hasMore, epoch, viewMode,
compound cursors).
Generator: web/scripts/fixtures/{sse,pagination}/ extend the K5 framework
(canonical serialization reused; suites pruned of stale files). Web
self-conformance tests replay every fixture against the real implementation
in bun run test:web. README documents schemas, projections, replay
contracts, and native consumption for both suites.
No time injection needed: the store's only Date.now() reads gate
notification throttling, which never reaches persisted or projected state.
|
||
|
|
c1ecb4798e |
feat(fixtures): batch 2 — events, permissions, sidechains, flavors, catalogs (K6)
34 new golden chat fixtures generated from the web pipeline, plus a machine-generated modes catalog: - Claude output: away_summary/microcompact_boundary/compact_boundary system subtypes, <task-notification> summary extraction, multi-block assistant message with an array-of-parts (text+image) tool_result - AgentEvent union completed: switch/message/error/title-changed(+dedupe)/ limit-warning(pipe text)/api-error folding/turn-duration/compact-summary/ abort-restore/unknown-type passthrough; thread-goal updated+cleared (silent) - Permission lifecycle via agentState.completedRequests: approved_for_session (mode+allowTools), denied with reason, canceled, AskUserQuestion flat answers, request_user_input nested answers (all with explicit createdAt) - Sidechain: Task tool_use with parentToolUseId-grouped subagent children - codex family: error, context_compacted, generated-image, review JSON, thread-goal messages, CodexBash call/result pair, exploration tool group - cli-output: cli-origin command block + stdout merge - agy flavor (output envelope): agy_message, agy_tool_action run_command mapping; cursor ACP plan snapshot (copilot skipped: identical codex shapes) - generator now also emits shared/fixtures/catalogs/modes.json from shared/src/modes.ts (per-flavor permission modes with labels/tones); self-conformance test covers it; README documents the catalogs contract |
||
|
|
9479f667b5 | feat(fixtures): golden chat fixtures generated from web pipeline (K4+K5) | ||
|
|
76e00f0091 |
feat(hub): add background-only ServerChan fallback (#1608)
* feat(hub): add background-only ServerChan fallback * fix(hub): validate ServerChan background setting type * fix(web): preserve pending visibility transitions * fix(web): guard visibility reports across subscriptions |
||
|
|
6c6f4b4929 |
feat(agy): replace fragile PTY/TUI wrapper with headless print-mode transport (#1591)
Replace the Antigravity (agy) integration — a PTY wrapping the TUI with
output-marker scraping ('? for shortcuts', 'Generating', trust dialogs,
/model picker navigation, quota-screen regex) — with a headless print-mode
transport: every user turn spawns `agy -p <msg> --conversation <uuid>
--output-format stream-json`, and NDJSON events (init/step_update/result)
map onto the existing transcript-entry channel (sendAgySessionMessage), so
hub/web rendering is unchanged. ~8.3k LOC (incl. tests) removed.
Fixes #1588. Design: docs/design/agy-headless-transport.md.
CLI:
- new cli/src/agy/headless/: agyNdjsonParser (pure functions, malformed-line
tolerance, step conversation-id adoption), AgyPlannerAccumulator (per-step
delta accumulation with settling retries), AgyHeadlessDriver (per-turn
spawn/kill loop, NDJSON chunk buffering, authoritative delivery ack via
user_input/result, interrupt + retry + shutdown lifecycle with consume/
restore, process-tree termination, SSH agent preserved, prompt log
redaction, per-turn model snapshot with conversation-DB fallback)
- runAgy/loop/session rewired; agy is remote-only (no PTY, no local mode,
no local-switch action); queued batches snapshot model/effort/mode
- deleted agyPty, agyPtyLauncher, agyHookCarrier(+scope cache), agyModelKeys,
agyQuestionKeys, agyAskQuestion, agySessionScanner, agyPermissionHandler
(+tests); buildAgyHooksJson removed; startHookServer agy-pre-invocation
route → 200 no-op
- runner: agy reopen/resume via generic --existing-session-id; commands/
agy.ts defaults remote; resume rejects ACTIVE agy sessions (remote-only,
in-flight turns cannot hand off)
- MCP stays user-managed (agy reads ~/.gemini/config/mcp_config.json and
workspace .agents/mcp_config.json natively in headless — verified)
Hub/web:
- machines.ts drops agy→pty forcing and rejects non-remote startingMode
- NewSession drops agy startingMode='pty'; terminal toggle disappears
automatically; RemoteModeDisplay hides the local-switch hint when absent
- docs/guide/agents.md updated: headless print mode, no PTY/hooks, MCP via
user's own mcp_config.json
Tests: 47 parser+driver tests (fake-binary e2e, chunk-split NDJSON, delivery
ack semantics, interrupt/retry/shutdown races, model attribution, EOF
framing, malformed envelopes); full suite green (cli ~2340, hub 1093,
web 2474, shared 262). Real-binary smoke on agy 1.1.13: single turn exit 0,
--conversation resume keeps the same conversation_id.
|
||
|
|
a6feb6e8ba |
feat: unified agent configuration descriptors (session config consolidation) (#1469)
* feat(config): add agent config descriptor protocol and advertise via runner capability Introduce shared agent configuration descriptors covering model, effort, permission, and secondary settings per agent flavor, plus the canonical HAPI YOLO -> native permission mode mapping. Runners advertise the builtin descriptors through the runner-state capability so hubs and web can render configuration without hardcoded flavor branches. Migrate the OpenCode create-session model picker from a bespoke radio list to the shared SelectControl combobox. * feat(web): render create-session permission from agent config descriptor Replace the flavor-branched Grok/Codex-family/YOLO permission block with a descriptor-driven PermissionField. Pi now reports permission as managed instead of silently ignoring the YOLO toggle, and YOLO-only flavors show the native permission mode the preference maps to. Removes the superseded GrokPermissionModeSelector and CodexFamilyPermissionModeSelector components. * ci: retry flaky claudeRemote 5s-timeout failure * fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model The parent initialization effect treated every null selected model as 'uninitialized' and auto-picked a concrete advertised model, clobbering the user's explicit Default choice (and a restored Default preference). null now means explicit Default and is preserved; only undefined (no choice made yet) triggers probe-based initialization. Add parent-level regression tests for Default persistence and remembered-model restore. * fix(web): accept undefined selected model in OpencodeModelSelector props * feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support Pi's agent config descriptor now advertises model (machine) and effort (static thinking levels) for create AND session availability: - cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight deduped) and parses the provider/model table; startup model match accepts provider-qualified ids - hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough - web: NewSession renders Pi models grouped by provider through the generic ModelSelector and a new descriptor-driven EffortField (replaces the per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload forwards Pi model + thinking-level effort (runner already supported --model/ --effort for pi) * fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning - ModelSelector now renders grouped options as <optgroup> (Pi models are provider-grouped; identical modelIds from different providers stay distinct) - PermissionField only receives autoPermissionModeSupported for Grok — a cached Grok probe result no longer leaks the Grok warning onto other agents Addresses HAPI Bot Minor findings on #1469. * fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog - ModelSelector buckets options with a reduce instead of Object.groupBy (Safari < 17.4 has no polyfill — New Session would throw on those clients) - Pi restored model/effort are cleared when the value is absent from the live machine catalog, and Create waits for the catalog while a non-default Pi choice is being validated (mirrors Codex/Grok/Copilot handling) Addresses HAPI Bot findings on #1469. * fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import - PiSession gains startupModelSettled; the startup set_thinking_level waits for the requested model's set_model attempt to settle first, so a level the default model rejects is not lost before the requested model is confirmed (set_model and set_thinking_level were already serialized by the runtime mutation lock; this pins the model-first ordering) - Create Session hides Pi model/effort controls while a Pi history import is selected — the import reopens the native session as-is and would silently ignore launch-only model/effort values Addresses HAPI Bot findings on #1469. * fix(pi): settle startup-model gate when model discovery fails or returns no models A failed or empty get_available_models response would leave the startupModelSettled gate unresolved, stranding a requested startup effort indefinitely. Resolve the gate on the error path and the empty-models path; adds regression tests for both. |
||
|
|
79ffe9aa0e |
feat(web): composer model/effort value buttons and first-class permission (part of #1438) (#1475)
* feat(web): composer model/effort value buttons and settings order Wide composers now show [model] and [effort] value buttons for non-Pi flavors (labels from the current session values), opening the settings sheet on click. Narrow viewports collapse to the settings button only via a new useNarrowViewport hook. The settings sheet reorders to Model -> Effort -> Permission -> other settings (Fast mode, collaboration, Copilot agent mode) so permission is first-class. Toolbar customization gains 'model'/'effort' items with settings labels. Pi keeps its dedicated model/thinking panels unchanged (unified descriptor-driven sheet is a follow-up). * fix(web): satisfy strict types in composer value-button test harness * fix(web): address review findings on composer model/effort value buttons - Normalize null/'auto'/'default' model wire values onto the value:null option so default-model sessions keep a localized label button (Major) - Exempt model/effort value buttons from the settings outside-click dismissal so a second click closes the sheet instead of reopening it - Read matchMedia synchronously in useNarrowViewport so narrow first paints never flash the wide toolbar - Add regression tests: model=null/'auto' labels, toggle-close behavior, and initial narrow-viewport render * feat(web): fold Pi into the generic composer model/effort value buttons Pi sessions previously exposed model/effort twice: dedicated 'Pi model' / 'Pi thinking level' toolbar buttons (PiModelPanel/PiThinkingLevelPanel) AND the settings sheet's generic Model/Effort sections. Consolidate so Pi looks exactly like every other flavor: - Pi now uses the generic model/effort value buttons; labels resolve from the provider-qualified piModels catalog (name -> modelId -> session id). - The settings sheet's Model section already renders provider-grouped Pi rows and Effort renders Pi thinking levels, so the dedicated panels and their toolbar slots are deleted. - Keep Pi's mid-turn control affordance (#1442): Pi turns hold thread.isDisabled for minutes, so Pi model/effort controls stay enabled while a turn is running (configurationControlsDisabled instead of controlsDisabled), including the sheet rows. - Drop 'piModel'/'piThinking' toolbar layout items; persisted layouts normalize them away automatically. Tests: pi value-button label/sheet tests, mid-turn model selection via the unified sheet, toolbar layout defaults. * fix(web): keep a session-settings trigger on narrow viewports and mid-turn Pi Address the HAPI review bot's two Minor findings on the Pi consolidation: - Narrow viewports collapse the model/effort value buttons into the settings sheet, so a persisted toolbar layout hiding the gear left no session-settings trigger at all. ComposerButtons now forces the gear back into the rendered layout on narrow viewports (wide layouts keep honoring the user's hidden choice). - The Pi mid-turn live-control rule only reached the value buttons and sheet rows; with those buttons gone on narrow, the gear was still disabled by controlsDisabled for the whole (minutes-long) Pi turn. HappyComposer now passes settingsDisabled={modelEffortControlsDisabled} so the gear stays clickable mid-turn for Pi exactly like the buttons. Tests: narrow + hidden-gear layout keeps Settings; narrow Pi mid-turn gear stays enabled and opens the provider-grouped sheet. * fix(web): address HAPI Bot Pi settings-sheet findings Three Minor findings from the review bot on the unified Pi sheet: - Provider-qualified selection: rows compared only modelId, so duplicate model IDs across providers all looked selected. Compare against piSelectedModel's provider+modelId when available. - Thinking-level reset: the removed Pi panel toggled the current level back to null; the unified effort rows only submitted concrete values. Re-clicking the selected effort row now clears it for Pi. - Memo staleness: the settings-sheet memo did not depend on modelEffortControlsDisabled, so a Pi disabled-state transition while the sheet was open left rows enabled from the prior render. Tests: colliding model IDs highlight only the matching provider row, re-clicking the selected effort row sends null, and a rerender with active=false disables the open sheet's rows. * fix(web): include piSelectedModel in settings-sheet memo deps The overlays memo reads piSelectedModel for provider-qualified row highlighting but only declared the derived selectedPiModel. When piSelectedModel hydrates from absent to a qualifier that resolves to the same catalog object, the memo is reused and duplicate model IDs stay highlighted across providers. Add the raw prop to the dep array. * fix(web): gate Pi model rows on catalog; reset drill-down via value button Address the HAPI Bot review on the unified composer settings sheet: - Pi no longer falls back to the generic synthesized modelOptions rows when its provider catalog is empty/loading. Selecting one of those would post a bare model id that runPi cannot resolve to a provider (first cached match or 409). The Model section now only renders for Pi when piModelGroups exists, and renders grouped rows exclusively. - Closing the sheet through the model/effort value button now goes through handleSettingsToggle, so a Cursor variant drill-down resets to the base model list on reopen (previously only the gear and outside-click paths cleared it). Tests: empty Pi catalog hides Model section; value-button close resets Cursor drill-down. * fix(web): hide Pi effort controls until the selected model resolves Address the HAPI Bot review: with the catalog still loading/failed there is no selectedPiModel to derive a capability map from, but the unified effort control stayed enabled (mid-turn Pi controls are intentionally live). Selecting a level would send set_thinking_level for a model that may not support reasoning, and the RPC can be rejected after the sheet closed. The old dedicated panel guarded this state. showEffortSettings now requires a resolved, reasoning-capable Pi model; the effort value button hides the same way. With an empty catalog Pi exposes no settings trigger at all, matching the old control states. Tests: unresolved Pi catalog mid-turn exposes no effort action. * fix(web): hide the Pi model trigger until the catalog resolves Address the HAPI Bot Minor finding: with an empty/loading Pi catalog the model value button fell back to the bare session model id and rendered an enabled trigger that opens no Model section. Show the button only once the provider-qualified catalog entry resolves. |
||
|
|
7909c46fff |
feat(search): support wildcard patterns across search fields (#1571)
* feat(search): add wildcard matching to search fields * fix(search): harden wildcard matching and file globs * fix(search): align file matching with shared wildcard semantics * fix(search): bound file wildcard search in runner * fix(search): normalize outline queries through shared matcher * fix(web): remove duplicate markdown test context field |
||
|
|
8dc4a50fee |
fix(web): prevent replaying historical assistant output (#1572)
* fix(web): prevent replaying historical assistant output * fix(web): handle history pagination during typing handoff * fix(web): preserve streaming handoff across history windows * fix(web): distinguish hydrated active runs from new output * fix(web): preserve handoff across tail hydration * fix(web): preserve first output in user-only turns |
||
|
|
ec19699418 |
fix(web): preserve session sidebar scroll during navigation (#1552)
* fix(web): preserve session sidebar scroll during navigation * fix(web): preserve nested session pane scroll * fix(web): avoid repeated file scroll restoration * fix(web): preserve scroll when leaving chat file previews * fix(web): preserve scroll after session resolution |
||
|
|
5c81ed69e2 |
fix(runner) preserve native instructions (#1557)
* fix(codex): preserve native instructions Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol. * fix(codex): filter new goal statuses |
||
|
|
e32334fbb2 |
fix(web): prioritize latest tail on session re-entry (#1560)
* fix(web): prioritize latest tail on session re-entry * fix(web): start latest sync immediately on re-entry |
||
|
|
767e40a71f |
fix(web): preserve current Fork action while reading history (#1573)
* fix(web): preserve current Fork action while reading history * fix(web): invalidate stale Fork boundary during history updates * fix(web): invalidate Fork boundary after message consumption |
||
|
|
30504252b9 |
feat(web): keep quiet active sessions in the pinned top section — Running / Active / Inactive tiers (#1590)
* feat(web): keep quiet active sessions in the pinned top section (#1589) When pin-in-progress is on, any connected session now floats above the project folders instead of dropping down the moment it finishes executing. The sidebar reads as three tiers: 1. In progress - running and pending work (unchanged) 2. Active sessions - quiet but connected (finished executing, operator usually continues the conversation) 3. Inactive - everything else, in directory groups Also remove the duplicate showSessionSummaryInChat fixture property in markdown-a.test.tsx that fails web typecheck on upstream main. * test(web): cover Active section collapse, keyboard toggle, and search expansion * fix(web): keep action-only project headers when every row floated A directory whose sessions all floated to the pinned sections lost its header entirely, taking copy-path and new-session-in-directory with it. Render an action-only header for fully-floated projects so the directory actions stay available. |
||
|
|
72483229c7 |
fix(web): restore Ctrl+A select-all on the chat page (#1595)
* fix(web): restore Ctrl+A select-all on the chat page Chromium's SelectAll collapses to an empty caret when the page contains a contenteditable (the rich composer) but focus is outside it, so Ctrl+A + Ctrl+C on the Happy page copied nothing. Take over Ctrl/Cmd+A at window scope when focus is outside the composer/inputs and select the message thread manually. Also removes a duplicate property in markdown-a.test.tsx that broke `bun typecheck` on upstream/main. * fix(web): restrict select-all takeover to unshifted Ctrl/Cmd+A; wire e2e spec into CI Address review findings: - leave Ctrl+Shift+A to the browser (matches native Chromium, where the shift variant is unbound) - run the composer-copy Chromium regression spec in CI alongside terminal-wrap-fidelity.spec.ts - move the spec to the root e2e/ dir so the root playwright config picks it up |
||
|
|
1f2fbcb142 |
fix(web): map codex-enveloped compact-summary to the chat block (#1582)
* fix(web): map codex-enveloped compact-summary to the chat block The merged #1570 renders Pi compaction summaries as a dedicated chat block via the event envelope. A compact-summary arriving in the codex payload envelope (older import paths, future producers) would still be silently dropped by the codex-content filter; map it to the same agent-event the live pi wrapper emits, mirroring the existing context_compacted handling. Adds a normalizeAgent regression test and tightens the /compact thinking-state test to assert the last keepAlive flips true (RPC outstanding) then false (settled). Verified: bun typecheck clean; web normalizeAgent 12/12, cli runPi 57/57 in an isolated TMPDIR. * fix(web): remove duplicate showSessionSummaryInChat in markdown-a fixture Regression from #1530: the fixture object literal sets the key twice, which breaks `bun typecheck` on upstream/main (Test workflow failing on push). One-line cleanup; no behavior change. Verified: bun typecheck exit 0 across cli/web/hub. |
||
|
|
901f17d0ca |
feat(pi): support Pi slash commands from HAPI web (compact/session/model/help) (#1570)
* feat(pi): support Pi slash commands from HAPI web (compact/session/model/help)
Pi runs as 'pi --mode rpc' over piped stdio, so TUI slash commands typed in
web chat previously fell through to the LLM as plain text and silently did
nothing (notably /compact).
- shared: add Pi builtin slash command list (help/compact/session/model) so
the web / menu exposes them; web test updated to match
- cli: intercept Pi builtin commands in runPi's user-message path
* /compact [instructions] -> Pi compact RPC (120s timeout, works while
streaming; summary + token delta reported back as chat messages)
* /session -> get_session_stats formatted stats
* /model [modelId] -> list/switch via set_model
* /help -> supported-commands list
* other Pi TUI builtins (/tree, /export, /reload, ...) -> explicit
terminal-only notice instead of silent LLM pass-through
* unknown slash text still passes through (extension commands, skills,
templates keep working)
- gate the prompt pump with piCompactInFlight so queued prompts are not
rejected by Pi mid-compaction; buffer commands until ready like prompts
- ListSlashCommands RPC merges HAPI builtins with Pi extension commands
- tests: parser unit tests + runPi integration tests (compact execution,
streaming steer interception, failure reporting, FIFO blocking, model
switch, unsupported commands, slash list merge)
- docs: document Pi slash command support in docs/guide/agents.md
* fix(pi): address review findings on slash command lifecycle
- compact timeout: fail the session (indeterminate outcome, runtime lease
poisoned) instead of reopening the prompt FIFO into a possibly-compacting
Pi; pump only when cleanup has not been initiated
- special commands: release the cancellation reservation before executing so
a cancel landing mid-command is not acknowledged (hub would delete the
queued row while the command still runs)
- tests: drop the duplicated slash-command describe block; add focused tests
for compaction timeout with a queued prompt and cancellation during an
in-flight special command
* fix(pi): route slash commands through the prompt FIFO and reject ambiguous models
- slash commands now share the prompt FIFO with ordinary messages: a
/compact or /model typed after a queued prompt dispatches only after it
(and after the active turn settles), instead of jumping the queue from
the preparation chain
- the pump dispatches special entries out-of-band while piSpecialCommandInFlight
keeps the FIFO blocked; steer promotion refuses slash commands
- /model <id> prefers an exact provider/modelId match and reports bare IDs
shared by multiple providers as ambiguous instead of picking the first
- tests: FIFO ordering (queued prompt before /compact), steer-delivered
/compact queued until settle, ambiguous/qualified model selection
* fix(pi): keep /compact interruptible, honor extension precedence, require token boundary
- head-of-line /compact dispatches even while Pi is streaming (Pi's
compact() aborts the active generation itself); every other queued item
still waits for the stream to settle, preserving FIFO order
- discovered extension commands / prompt templates override same-name
builtins at message time, matching the slash-list merge precedence
- parsePiSpecialCommand requires a command-token boundary, so path-like
text such as /compact.md or /model/config stays an ordinary prompt
- tests: interrupt rule, extension collision, reserved-name path prefixes,
non-compact commands waiting for stream settle
* fix(pi): honor cancellation acknowledged during slash-command discovery
A cancel arriving while the chain awaits get_commands (cold cache) was
acknowledged via the preparing reservation but never re-checked, so a
canceled /compact could still execute. Re-check the cancellation marker
after discovery and drop the message before dispatch.
* fix(pi): qualify /model selectors and report failed slash RPCs once
- /model lists provider-qualified selectors (openai/gpt-5.2) so duplicate
bare IDs remain usable and copy-pasteable; current model is qualified too
- compact/set_model failures are owned by the awaited slash/config handlers:
the common response handler no longer emits the raw Pi error a second time
- tests: qualified listing with duplicate providers, single-message failure
reporting for rejected /compact and /model
* fix(pi): consume slash-command queue row at dispatch
Special commands (/compact, /session, /model, /help) are executed by HAPI
itself and never delivered to Pi as prompts. Consumption was deferred until
the command finished, so a /compact run — an LLM summarization pass that can
take minutes — left the row stuck in the web queued bar for its whole
duration, then surfaced as a sent message. Consume the row the moment
dispatch starts; failures still surface via the explicit event message.
* fix(pi): guard special-command dispatch against unexpected rejections
* ci: retry Codex PR Review after infra failure (proxy 503)
* fix(pi): keep session queued-thinking grace during /compact dispatch
The queued-thinking grace is session-scoped, so clearing it while
acknowledging a dispatch-time /compact row also drops the grace for any
prompt queued behind it. /compact keeps running for minutes without
toggling Pi thinking state, which would leave the web session looking idle
while compaction and the following prompt are still pending. Only the
fast, synchronous commands (/session, /model, /help) clear the grace.
* fix(pi): render compaction summary as a dedicated chat block
The manual /compact RPC result was reported as two plain message
events ("📦 Compaction completed (tokens: …)" + "📦 Compaction
summary: …"), which the web chat renders as tiny centered status
lines — unusable for a real summary payload. Emit a structured
compact-summary event instead (summary + token delta) and render
it as an independent block: header with the delta and the summary
markdown in a scrollable panel.
Also emit the same structured event when importing Pi session
files (compaction entries), and queue the event lossless like
other user-visible messages so a disconnect cannot drop it.
Verified: bun typecheck clean; bun run test exit 0 (cli 2481
passed, web 2451 passed, hub/shared clean); runPi/loop/apiSession/
piSessions/presentation suites green.
* fix(pi): address HAPI Bot findings on compact dispatch and import
- Track compaction as thinking for its whole duration: /compact runs for
minutes without a Pi streaming event, so the 15s queued-thinking grace
alone left the web session looking idle while compaction and any queued
prompts were still pending (updateThinkingState around the compact RPC).
- Imported Pi compaction summaries must use the event envelope
(content.type: 'event') like the live wrapper's compact RPC result; the
codex payload envelope is dropped by the web normalizer. Extend
CodexImportedMessageSchema with the event variant.
* fix(pi): /model retries discovery when the model cache is empty
Startup model discovery can be late or fail once; using only the cached
catalog made /model report valid models as unknown. getPiModels() falls
back to the get_available_models RPC on an empty cache, used for both
listing and switching.
* fix(pi): interrupt in-flight /compact on Abort; surface startup model rejection
- The Abort action no longer waits on the runtime-mutation lease when a
manual /compact is in flight (compaction can hold it for up to 120s,
blowing the 25s abort deadline and failing closed). It sends the abort
RPC directly so Pi cancels its compaction AbortController; the compact
RPC's 'Compaction cancelled' error is not double-reported as a failure
since Pi already emits the compaction_end(aborted) lifecycle event.
- A rejected detached startup set_model now emits a visible ⚠️ event into
chat instead of only a debug log, restoring the pre-existing behavior.
* fix(pi): close the Abort race when /compact is queued on the mutation lock
Abort previously assumed an in-flight /compact always had its RPC issued;
the command is marked active at queue dispatch, but the compact RPC is sent
only after the runtime-mutation lock is acquired. An Abort landing in that
gap acknowledged success while the compact RPC still ran afterwards.
Track the compact's rpcStarted/cancelled state: Abort cancels a not-yet-
started compact in place (the queued callback skips it), and interrupts a
started one via the abort RPC as before.
* fix(pi): persist provider-qualified selection after /model switch
The success path updated currentModel/currentProvider and keepalive with a
bare model ID, leaving metadata.piSelectedModel on the previous provider.
The web picker prefers that metadata for selection, context-window
resolution, and effort options, so a switch like openai/gpt-5.2 ->
azure/gpt-5.2 was invisible. Persist piSelectedModel with the full
provider/modelId pair on every confirmed switch.
* fix(pi): retire pending extension UI requests when /compact interrupts a turn
The streaming-interrupt path sent the compact RPC without cancelling
pending extension UI requests first, unlike the Abort path. Editor
requests have no timeout, so the web could stay stuck on a stale
input/permission card and a later answer could be routed to the aborted
turn. Cancel all pending requests (with a response) before compacting.
* fix(pi): fail closed when the direct compact-abort RPC times out
The in-flight /compact abort branch awaited the abort RPC without the
ordinary Abort path's timeout handling: an unanswered abort left the
compaction outcome indeterminate (the compact RPC keeps the mutation
lease for up to 120s) while the wrapper still looked live. Fail the
session on PiRpcTimeoutError, mirroring the standard abort fail-closed
path.
---------
Co-authored-by: swear01 <swear01@users.noreply.github.com>
|
||
|
|
339607bf3e | fix(web): keep single tildes literal in markdown (#1471) | ||
|
|
386ee4121b |
fix(web): hide chat viewport focus outline after Home/End (#1495)
* fix(web): hide chat scroll focus outline * fix(web): keep chat viewport keyboard focus visible * test(web): cover chat viewport keyboard focus |
||
|
|
0e60697895 | fix(web): prevent Rewind crashes when message history resets (#1530) | ||
|
|
e83dbdd40a |
fix(web): add clear action to collapsed session search (#1544)
* fix(web): add clear action to collapsed session search * fix(web): improve light theme clear icon contrast |
||
|
|
44703d0153 |
fix(web): localize Fork and Rewind labels in Simplified Chinese (#1546)
* fix(web): localize Fork and Rewind labels in Chinese * test(web): cover localized history confirmation dialogs |
||
|
|
f803967e5d | fix(web): stabilize autocomplete dropdown layout (#1547) | ||
|
|
b2ae6b90a3 |
fix(web): keep share actions visible during generation (#1550)
* fix(web): keep share actions visible during generation * test(web): cover sharing during generation |
||
|
|
a794be0811 |
fix(web): recover silently-dead SSE connections quickly, stop banner noise (#1562)
A suspended mobile tab can lose its SSE connection without the browser ever noticing (no FIN/RST arrives). On resume the client used to wait for the 90s heartbeat watchdog or lean on the native EventSource retry, which can hang on a dead pooled socket - leaving a persistent "Reconnecting... (stream error)" banner while the UI keeps working. - useSSE: on visibility resume, distrust the connection after one missed heartbeat interval (45s) instead of 90s - useSSE: abandon connection attempts that don't open within 10s (hung on a dead pooled socket after resume) instead of waiting for the watchdog; force bypasses the one-shot reconnect guard for the new attempt cycle - useSSE: first reconnect attempt is immediate; exponential backoff starts from the second attempt - ReconnectingBanner: localize connect-timeout and transport-error reasons (transport-error is already emitted by current code but fell through unlabeled) Fixes #1559. Reimplements the reconnect half of #989 (closed unmerged) on top of the current scheduler (hidden-tab deferral, slow backoff, replay cursor). |
||
|
|
235d6dd6fd | fix(web): clarify displayed media labels (#1565) | ||
|
|
7be4babfbd |
fix(web): shorten Codex sync label and size action menu to content (#1569)
* fix(web): refine Codex sync action menu * test(web): cover action menu trigger anchoring |
||
|
|
e314522982 | fix(web): lower context warning thresholds (#1576) | ||
|
|
e0354b09c0 | fix(web): align new-session and settings controls (#1578) | ||
|
|
ad72229923 |
feat(sessions): add on-demand AI title suggestions (#1577)
* feat(sessions): add on-demand AI title suggestions * fix(sessions): address title suggestion review feedback * fix(web): ignore stale title generation results |