Skip to content

Add context command support for chat turns#1124

Open
gptme-thomas wants to merge 2 commits into
moltis-org:mainfrom
gptme-thomas:pr/context-command
Open

Add context command support for chat turns#1124
gptme-thomas wants to merge 2 commits into
moltis-org:mainfrom
gptme-thomas:pr/context-command

Conversation

@gptme-thomas

Copy link
Copy Markdown

Summary

Adds an optional chat.context_command that runs before each chat turn and appends stdout to the prompt context. This lets deployments inject generated runtime context without manually pasting it into each session.

The implementation covers:

  • config schema, template, validation, and docs
  • shared context command runner with timeout/error handling
  • normal chat turn context merging
  • external-agent context snapshots
  • Codex app-server context forwarding so external-agent turns receive the generated context

Validation

  • cargo fmt --all -- --check
  • cargo test -p moltis-common context_command -- --nocapture
  • cargo test -p moltis-config context_command -- --nocapture
  • cargo test -p moltis-chat merge_context_sections -- --nocapture
  • cargo test -p moltis-gateway context_from_history_includes_project_context -- --nocapture
  • cargo test -p moltis-external-agents runtimes::codex::tests -- --nocapture

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces chat.context_command, an optional operator-configured shell command that runs before each chat turn and appends its stdout to the prompt context. The feature covers normal chat turns and external-agent sessions uniformly.

  • New context_command module (crates/common/src/context_command.rs) executes the command via sh -c / cmd /C, enforces a 30 s timeout, and returns stdout on success, None on any failure.
  • resolve_turn_context replaces the existing resolve_project_context call sites and merges the two context sources via merge_context_sections; the gateway path populates ContextSnapshot.project_context so external agents (including the Codex app-server) receive the generated context through build_process_input.
  • Codex app-server session gains a top-level turn timeout and a new /params/delta fallback in extract_message.

Confidence Score: 4/5

Safe to merge; the feature is well-tested and fails gracefully, but there are a few quality issues worth addressing before the command sees heavy production use.

The core logic is correct and all error paths return None rather than blocking chat. Three issues stand out: (1) a successful command that produces no output logs a warn!, which will mislead operators who conditionally emit context; (2) resolve_project_context and run_context_command are awaited serially even though they are fully independent, so every chat turn pays both latencies in sequence; (3) working_dir is passed as None at every call site, meaning the command always runs from the server process cwd — operators writing relative-path scripts will be surprised. None of these change the correctness of the feature, but the warn! spam and serial latency are likely to surface quickly in production.

crates/common/src/context_command.rs (warn on empty output) and crates/chat/src/service/types.rs (sequential context resolution)

Important Files Changed

Filename Overview
crates/common/src/context_command.rs New module: runs a shell command via sh/cmd, captures stdout, enforces a 30 s timeout. Empty-but-successful output triggers warn! (should be debug!); working_dir is always None at every call site.
crates/chat/src/service/types.rs Adds resolve_turn_context and merge_context_sections; the two async sources are awaited sequentially rather than via tokio::join!, adding avoidable latency on every chat turn.
crates/gateway/src/external_agents.rs Calls run_context_command and passes the result into context_from_history_with_project_context; straightforward and well-tested.
crates/external-agents/src/runtimes/codex.rs CodexAppServerSession now forwards context snapshot via build_process_input and wraps the full turn in a top-level timeout; also adds /params/delta as a new extract_message fallback.
crates/config/src/schema/chat.rs Adds context_command: Option field to ChatConfig with serde default; clean change.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ChatService
    participant ProjectContext as Project Context (DB/FS)
    participant ContextCmd as context_command (shell)
    participant LLM as LLM / External Agent

    Client->>ChatService: send_turn(prompt)
    ChatService->>ProjectContext: resolve_project_context()
    ProjectContext-->>ChatService: "Option<String>"
    ChatService->>ContextCmd: run_context_command() [30s timeout]
    ContextCmd-->>ChatService: "Option<String>"
    ChatService->>ChatService: merge_context_sections()
    ChatService->>LLM: prompt + merged context
    LLM-->>ChatService: response
    ChatService-->>Client: response
Loading

Reviews (1): Last reviewed commit: "Add context command support for chat tur..." | Re-trigger Greptile

Comment on lines +56 to +58
if text.trim().is_empty() {
warn!("context_command produced no output");
None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 warn on empty output will spam logs for conditional commands

A context command that deliberately produces no output under some conditions (e.g., it only emits context when the system is degraded, or it gates on a feature flag) will fire this warn! on every "quiet" turn. Because the command exit code is 0, this is not a failure — the operator chose to emit nothing. Treating an empty-but-successful result as a warning will mislead on-call engineers and pollute logs. debug! (or silently returning None) is the right level here.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +598 to +611
/// Resolve all dynamic prompt context for a turn.
pub(in crate::service) async fn resolve_turn_context(
&self,
session_key: &str,
conn_id: Option<&str>,
) -> Option<String> {
let project_context = self.resolve_project_context(session_key, conn_id).await;
let command_context = moltis_common::context_command::run_context_command(
self.config.chat.context_command.as_deref(),
None,
)
.await;
merge_context_sections(project_context, command_context)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Project-context and command-context resolved sequentially

resolve_project_context (a DB/filesystem call) and run_context_command (a subprocess that may run up to 30 s) are awaited one after the other. Their results are independent, so they can be fetched in parallel with tokio::join!, cutting every chat-turn's latency overhead by the slower of the two rather than their sum. With a 30-second command timeout the serial worst case is notably worse.

Comment on lines +14 to +23
pub async fn run_context_command(
command: Option<&str>,
working_dir: Option<&Path>,
) -> Option<String> {
let command = command.map(str::trim).filter(|value| !value.is_empty())?;

let mut cmd = shell_command(command);
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 working_dir is always None at every call site

Both resolve_turn_context (chat service) and the gateway ExternalAgentChatService call run_context_command(..., None). The working_dir parameter therefore has no effect today, but operators configuring commands like ./scripts/gen-context.sh or cat .context will get surprising failures because the script runs from the server process's cwd, not the project directory. Consider passing the active project's working directory at call sites, or at least documenting that the command runs in the server process cwd.

- Downgrade empty-output log from warn! to debug! — operators may
  conditionally emit context, so silence is not an error.
- Resolve project context and context command concurrently via
  tokio::join! instead of sequentially, avoiding double latency on
  every chat turn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant