Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ br create --title "Fix auth bug" --type task --priority 1
br close <id> --reason "Fixed in commit abc123"
```

## Long-running Background Shells

proto captures the output of shell commands run with `is_background: true` to disk so detached processes never silently lose their stdout. The agent gets a stable task ID and an absolute output file path it can read at any time, plus an automatic `<task_notification>` on the next turn when the task exits.

- **Output file:** `<projectTempDir>/<sessionId>/tasks/<taskId>.output` — written by the OS via shell-level redirection, so it keeps growing even after the parent wrapper exits.
- **Completion:** when the bg process exits, the next user prompt is prefixed with a `<task_notification>` block carrying `task_id`, `output_file`, `status` (completed/failed/killed), and `exit_code`. The agent can then `read_file` the output for results.
- **`/bg`** lists running and recently-completed background tasks.
- **`bg_stop` tool** — sends SIGTERM to the process group, escalating to SIGKILL after a 3s grace.

This is what fixes the "agent runs an eval, can't find the results" failure mode that plagued earlier versions where backgrounded `&` commands streamed into nowhere.

## Memory

proto has a persistent memory system inspired by Claude Code. Memories are individual markdown files with YAML frontmatter, organized by type and stored per-project or globally.
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/divergence-from-upstream.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,18 @@ Consumed by:
`task-output.ts`, `task-ready.ts`, `task-stop.ts`, `task-update.ts`
(backed by `services/task-store.ts`). Distinct from the AskUserQuestion
/ TodoWrite split — these track long-running async work.
- **Background-shell disk capture + `bg_stop`** — `backgroundShells/`
module + `tools/bg-stop.ts`. When a shell command runs with
`is_background: true`, stdout/stderr is redirected at the shell level
to `<projectTempDir>/<sessionId>/tasks/<taskId>.output`. The OS keeps
writing even after the wrapper exits, so detached processes (long
evals, build watchers, dev servers) never silently lose output.
`BackgroundShellRegistry` tracks each task; a watcher polls the `.exit`
sentinel and marks it complete. The next user turn carries a
`<task_notification>` block (`task_id`, `output_file`, `status`,
`exit_code`). `bg_stop` SIGTERMs the process group with SIGKILL
fallback. Listed via `/bg`. Mirrors cc-2.18's task framework, scoped
to local shells only. **Non-Windows.**
- **LSP** — `tools/lsp.ts` exposing language-server intelligence; a
fork-only setting (`general.lsp`) gates it.

Expand Down
1 change: 1 addition & 0 deletions docs/reference/tools/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default {
'file-system': 'File System',
'multi-file': 'Multi-File Read',
shell: 'Shell',
'bg-stop': 'Stop Background Shell',
'todo-write': 'Todo Write',
task: 'Task',
'exit-plan-mode': 'Exit Plan Mode',
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/tools/bg-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Stop Background Shell (`bg_stop`)

Stops a long-running background shell task spawned by [`run_shell_command`](./shell) with `is_background: true`. Sends SIGTERM to the process group, escalating to SIGKILL after a 3-second grace.

## Parameters

| Parameter | Required | Description |
| --------- | -------- | ---------------------------------------------------------------------- |
| `task_id` | Yes | The opaque ID returned by the original shell tool result |
| `reason` | No | Free-form reason; recorded for audit and surfaced in the result string |

## Behavior

1. Looks up the task in the in-session registry.
2. If the task already finished, returns its current status without signaling — no-op.
3. Sends `SIGTERM` to the process group (negative pid). Falls back to signaling the leader directly on `EPERM`.
4. Schedules a `SIGKILL` to the process group after **3 seconds** if the leader is still alive.
5. Optimistically marks the task `killed` in the registry. The watcher sees the same exit and is a no-op once the status flipped.
6. The next user prompt is prefixed with a `<task_notification>` block carrying `status: killed`.

## Output

```
Background task "7f9c…" stopped (SIGTERM: <reason if provided>).
```
Comment on lines +23 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add languages to the fenced examples.

markdownlint-cli2 is already flagging these three blocks with MD040, so this page will stay lint-noisy until each fence has a language (text is enough here).

Suggested fix
-```
+```text
 Background task "7f9c…" stopped (SIGTERM: <reason if provided>).

- +text
No background task with ID "<task_id>".


-```
+```text
Background command started.
Task ID: 7f9c…
Output file: /tmp/proto/<project-hash>/<session>/tasks/7f9c…output
PID: 54322
</details>


Also applies to: 29-31, 37-42

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 23-23: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/reference/tools/bg-stop.md around lines 23 - 25, Add a language tag
(use "text") to each fenced code block examples that are currently untagged: the
block containing Background task "7f9c…" stopped (SIGTERM: <reason if provided>)., the block containing No background task with ID "<task_id>".,
and the multi-line block beginning with Background command started. (including
Task ID/Output file/PID lines); update each opening totext so
markdownlint MD040 is resolved.


</details>

<!-- fingerprinting:phantom:medusa:grasshopper:a20f7c3f-f901-41ac-9101-2b9115f8b8cd -->

<!-- d98c2f50 -->

<!-- This is an auto-generated comment by CodeRabbit -->


If the task was not found:

```
No background task with ID "<task_id>".
```

## Finding `task_id`

The shell tool's return value for a backgrounded command lists the task ID and output path:

```
Background command started.
Task ID: 7f9c…
Output file: /tmp/proto/<project-hash>/<session>/tasks/7f9c…output
PID: 54322
```

You can also list current background tasks from the TUI with the `/bg` slash command.

## Confirmation

`bg_stop` is classified as a `think`-kind tool — no user confirmation is required for the tool call itself. The signal still goes to the process group with the user's permissions.

## See Also

- [`run_shell_command`](./shell) — the `is_background: true` parameter
- `/bg` slash command — list current background tasks
21 changes: 20 additions & 1 deletion docs/reference/tools/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,29 @@ Executes a shell command. On macOS/Linux, runs with `bash -c`. On Windows, runs
- Build watchers (`webpack --watch`)
- Database servers (`mongod`, `redis-server`)
- Any process that runs indefinitely
- Long batch jobs whose stdout you want to inspect later (evals, data processing)

## Output

Returns `Command`, `Directory`, `Stdout`, `Stderr`, `Error`, `Exit Code`, `Signal`, and `Background PIDs`.
For **foreground** commands, the tool returns `Command`, `Directory`, `Stdout`, `Stderr`, `Error`, `Exit Code`, `Signal`, and `Process Group PGID`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Foreground output keys are documented incorrectly.

The tool does not return separate Stdout and Stderr fields here; packages/core/src/tools/shell.ts still emits Output and Error for foreground runs. Documenting the wrong keys will send users looking for fields that never appear.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/tools/shell.md` at line 37, The docs entry incorrectly lists
separate Stdout and Stderr for foreground runs; update the documentation to
match the actual keys emitted by packages/core/src/tools/shell.ts (it emits
Output and Error for foreground commands). Edit the line in
docs/reference/tools/shell.md to replace `Stdout`, `Stderr` with `Output`,
`Error` and ensure the rest of the sentence references the same set (`Command`,
`Directory`, `Output`, `Error`, `Exit Code`, `Signal`, `Process Group PGID`) so
docs match the shell tool's behavior.


For **background** commands on non-Windows, the tool returns:

- `Task ID` — opaque stable ID used to refer to the task later
- `Output file` — absolute path where stdout+stderr is captured (writes continue after the wrapper exits)
- `PID` — actual subprocess PID

Read the output file at any time with the [`read_file`](./read-file) tool to inspect progress or final results — there is no need to poll. When the process exits, the **next user prompt** is prefixed with a `<task_notification>` block carrying `task_id`, `output_file`, `status` (`completed`/`failed`/`killed`), `exit_code`, and a human summary.

## Background tasks

Output files live at `<projectTempDir>/<sessionId>/tasks/<taskId>.output` and are written via shell-level redirection (`( cmd ) > file 2>&1 &`). The OS keeps writing even after the parent shell exits, so detached processes never silently lose output.

To stop a runaway background task, use the [`bg_stop`](./bg-stop) tool with the `task_id`. It SIGTERMs the process group and escalates to SIGKILL after a 3-second grace.

To list running and recently-completed background tasks from the TUI, run `/bg`.

> **Windows.** Background tasks rely on POSIX shell redirection and process-group signaling. On Windows, `is_background: true` falls back to the simpler "fire and return" path used in earlier versions; output is not captured to disk.

## Confirmation

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@protolabsai/proto",
"version": "0.26.17",
"version": "0.26.21",
"publishConfig": {
"access": "public"
},
Expand All @@ -20,7 +20,7 @@
"url": "https://github.com/protoLabsAI/protoCLI/issues"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.17"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.21"
},
"scripts": {
"start": "cross-env node scripts/start.js",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@protolabs/proto",
"version": "0.26.16",
"version": "0.26.20",
"description": "proto",
"repository": {
"type": "git",
Expand Down Expand Up @@ -37,7 +37,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.17"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.26.21"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.14.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { insightCommand } from '../ui/commands/insightCommand.js';
import { teamCommand } from '../ui/commands/teamCommand.js';
import { voiceCommand } from '../ui/commands/voiceCommand.js';
import { recapCommand } from '../ui/commands/recapCommand.js';
import { bgCommand } from '../ui/commands/bgCommand.js';

/**
* Loads the core, hard-coded slash commands that are an integral part
Expand Down Expand Up @@ -114,6 +115,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
insightCommand,
voiceCommand,
recapCommand,
bgCommand,
];

return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
Expand Down
84 changes: 84 additions & 0 deletions packages/cli/src/ui/commands/bgCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2025 protoLabs Studio
* SPDX-License-Identifier: Apache-2.0
*/

import type {
CommandContext,
SlashCommand,
SlashCommandActionReturn,
} from './types.js';
import { CommandKind } from './types.js';

function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
const rs = s % 60;
if (m < 60) return `${m}m${rs > 0 ? ` ${rs}s` : ''}`;
const h = Math.floor(m / 60);
const rm = m % 60;
return `${h}h${rm > 0 ? ` ${rm}m` : ''}`;
}

function listAction(context: CommandContext): SlashCommandActionReturn {
const config = context.services.config;
if (!config) {
return {
type: 'message',
messageType: 'error',
content: 'Config not available.',
};
}
const registry = config.getBackgroundShellRegistry();
const tasks = registry.list();

if (tasks.length === 0) {
return {
type: 'message',
messageType: 'info',
content:
'No background tasks. Run a shell command with is_background: true to start one.',
};
}

const now = Date.now();
const lines: string[] = ['Background shell tasks (newest first):', ''];
for (const t of tasks) {
const ageMs = (t.endTime ?? now) - t.startTime;
const status =
t.status === 'running'
? `running (${formatDuration(ageMs)})`
: `${t.status}${t.exitCode != null ? ` exit=${t.exitCode}` : ''} (ran ${formatDuration(ageMs)})`;
lines.push(` ${t.id} ${status}`);
lines.push(` cmd: ${t.command}`);
lines.push(` out: ${t.outputPath}`);
if (t.pid != null) lines.push(` pid: ${t.pid}`);
lines.push('');
}
lines.push(
'Stop a running task with the bg_stop tool (or kill <pid> manually).',
);
return {
type: 'message',
messageType: 'info',
content: lines.join('\n'),
};
}

export const bgCommand: SlashCommand = {
name: 'bg',
description: 'List long-running background shell tasks',
kind: CommandKind.BUILT_IN,
subCommands: [
{
name: 'list',
description: 'List background tasks',
kind: CommandKind.BUILT_IN,
action: listAction,
},
],
action: listAction,
};
Loading
Loading