fix(server): honor Grok hideFromScrollback and render background tasks#2198
fix(server): honor Grok hideFromScrollback and render background tasks#2198jasonhnd wants to merge 10 commits into
Conversation
Grok ACP does not advertise permission modes, so catalog sessions had no Ask / Always Approve picker and blocked on every tool permission card. Wire Paseo modes to Grok's documented controls: launch with `grok agent --always-approve stdio` when Always Approve is selected, toggle via `/always-approve on|off` mid-session, and keep a client-side auto-approve fallback if Grok still emits request_permission. Closes getpaseo#2053
GROK_MODES is now the single source for labels, icons, color tiers, and isUnattended. The provider registry reuses that list instead of inlining a duplicate. deriveModesFromACP re-attaches fallback metadata after ACP mode derivation so Always Approve stays unattended for create-config inheritance.
Registry definition.modes requires required icon/colorTier fields. Type the shared Grok mode list as AgentProviderModeDefinition[] so the single-source list typechecks for both registry and session defaultModes.
Grok ACP marks model-only wake-up chunks with _meta.hideFromScrollback and emits structured _x.ai/session/update task events. Suppress those chunks before message assembly and map task_backgrounded/task_completed to stable synthetic tool_call timeline items so long background work no longer leaks as raw user bubbles. Closes getpaseo#2182
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6922b81dca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return { | ||
| handled: true, | ||
| currentModeId: GROK_ASK_MODE_ID, |
There was a problem hiding this comment.
Delegate Grok mode changes when leaving upstream modes
When Grok advertises a real ACP mode such as plan, transformGrokSessionResponse appends the synthetic ask option to that same mode list. Selecting Ask then reaches this unconditional handled return instead of the generic ACP setSessionMode path, so a session currently in plan is reported as ask in Paseo while Grok remains in the upstream mode (and if always-approve was enabled but a current-mode update reset currentModeId to plan, this also skips sending /always-approve off).
Useful? React with 👍 / 👎.
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/providers/grok-background-tasks.ts | New module mapping Grok _x.ai/session/update notifications to synthetic tool_call timeline items; uses SHA-1 hash for stable callId (addressing prior collision concern), validates via Zod, separates lifecycle helpers cleanly. |
| packages/server/src/server/agent/providers/acp-agent.ts | Adds narrow opt-in hooks (shouldSuppressUserMessageChunk, extensionNotificationHandler, autoApproveModeIds, resolveSessionCommand) and extracts deliverStreamEvents for shared history-replay/live-stream delivery; Grok chunk suppression correctly precedes fallbackAssistantMessageId reset. |
| packages/server/src/server/agent/providers/grok-acp-agent.ts | Grok-specific adapter: session command resolution for always-approve, session response transformer injecting modes, and in-session mode writer via slash commands; all logic stays in this file and grok-background-tasks.ts. |
| packages/server/src/server/agent/providers/grok-acp-agent.test.ts | New 883-line test file covering launch flag injection, mode transformer, permission auto-approve, chunk suppression (live and history replay), and the full task lifecycle; directly exercises ACPAgentSession integration points. |
| packages/server/src/server/agent/providers/grok-background-tasks.test.ts | Unit tests for the background task module covering null method, wrong-session filter, malformed payload tolerance, all lifecycle outcomes, and callId non-collision. |
| packages/server/src/server/agent/providers/generic-acp-agent.ts | Exposes the new ACPAgentClient option fields through GenericACPAgentClient so GrokACPAgentClient can extend it; straightforward pass-through forwarding. |
| packages/server/src/server/agent/provider-registry.ts | Registers Grok with canonical modes and provider-specific client creation; follows the same conditional pattern already used for cursor and kiro. |
| packages/server/src/server/agent/providers/acp-agent.test.ts | Two new deriveModesFromACP tests verify that provider fallback metadata (icon, colorTier, isUnattended) is re-attached when mode information arrives via availableModes or config options. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Grok as Grok Process
participant Session as ACPAgentSession
participant BgTasks as grok-background-tasks
participant Timeline as Timeline / UI
Note over Session: user_message_chunk (hideFromScrollback=true)
Grok->>Session: sessionUpdate(user_message_chunk)
Session->>Session: shouldSuppressUserMessageChunk → true
Session-->>Timeline: (suppressed)
Note over Session: user_message_chunk (visible)
Grok->>Session: sessionUpdate(user_message_chunk)
Session->>Session: shouldSuppressUserMessageChunk → false
Session->>Timeline: pushEvent(user_message)
Note over Session: Background task lifecycle
Grok->>Session: extNotification(_x.ai/session/update, task_backgrounded)
Session->>BgTasks: extensionNotificationHandler()
BgTasks->>BgTasks: buildGrokBackgroundTaskCallId SHA-1
BgTasks-->>Session: tool_call running
Session->>Session: deliverStreamEvents
Session->>Timeline: tool_call running
Grok->>Session: extNotification(_x.ai/session/update, task_completed)
Session->>BgTasks: extensionNotificationHandler()
BgTasks->>BgTasks: resolveGrokTaskCompletionLifecycle
BgTasks-->>Session: tool_call completed/failed/canceled
Session->>Timeline: tool_call update
Note over Session: Always Approve fallback
Grok->>Session: requestPermission
Session->>Session: autoApproveModeIds.has(currentMode)
Session-->>Grok: outcome selected
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Grok as Grok Process
participant Session as ACPAgentSession
participant BgTasks as grok-background-tasks
participant Timeline as Timeline / UI
Note over Session: user_message_chunk (hideFromScrollback=true)
Grok->>Session: sessionUpdate(user_message_chunk)
Session->>Session: shouldSuppressUserMessageChunk → true
Session-->>Timeline: (suppressed)
Note over Session: user_message_chunk (visible)
Grok->>Session: sessionUpdate(user_message_chunk)
Session->>Session: shouldSuppressUserMessageChunk → false
Session->>Timeline: pushEvent(user_message)
Note over Session: Background task lifecycle
Grok->>Session: extNotification(_x.ai/session/update, task_backgrounded)
Session->>BgTasks: extensionNotificationHandler()
BgTasks->>BgTasks: buildGrokBackgroundTaskCallId SHA-1
BgTasks-->>Session: tool_call running
Session->>Session: deliverStreamEvents
Session->>Timeline: tool_call running
Grok->>Session: extNotification(_x.ai/session/update, task_completed)
Session->>BgTasks: extensionNotificationHandler()
BgTasks->>BgTasks: resolveGrokTaskCompletionLifecycle
BgTasks-->>Session: tool_call completed/failed/canceled
Session->>Timeline: tool_call update
Note over Session: Always Approve fallback
Grok->>Session: requestPermission
Session->>Session: autoApproveModeIds.has(currentMode)
Session-->>Grok: outcome selected
Reviews (7): Last reviewed commit: "Merge branch 'main' into fix/2182-grok-h..." | Re-trigger Greptile
Hash raw task_ids for synthetic tool callIds so normalized-character collisions cannot merge concurrent tasks. Reuse the shared ACP extension context type, build task metadata without conditional object spreads, and let generic setSessionMode handle Ask when leaving an upstream ACP mode.
Summary
user_message_chunkupdates when_meta.hideFromScrollback === true(live stream and history replay), before message assembly so hidden chunks withoutmessageIdcannot contaminate later visible user messages.shouldSuppressUserMessageChunk,extensionNotificationHandler) and route extension-driven timeline items through the same live-vs-history delivery path._x.ai/session/updatetask_backgrounded/task_completedat the adapter boundary and map them to synthetictool_callitems with a stablecallIdderived fromtask_id(completed / failed / canceled / missing output / truncated).GrokACPAgentClient+grok-background-tasks.ts; do not introduce a global<system-reminder>text filter.docs/providers.mdfor the Grok adapter and hook architecture.Fixes #2182
Dependency
Depends on #2177 (Grok ACP adapter / Always Approve). This branch is stacked on the latest
#2177head (feat/2053-grok-always-approve-mode). Merge #2177 first, then rebase this PR ontomainif needed.Test plan
npx vitest run packages/server/src/server/agent/providers/grok-background-tasks.test.ts packages/server/src/server/agent/providers/grok-acp-agent.test.ts --bail=1(34 passed)npm run typecheck --workspace=@getpaseo/server<system-reminder>user bubble; compact task/tool item for backgrounded → completed_metathey may send)