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
2 changes: 1 addition & 1 deletion docs/agent-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Product philosophy: **no live-tool / thinking-segment UX walls** — cancel with
| Thinking **segments** | **none** | Every reasoning segment paints |
| Thinking / line chars | **256 KiB** | Wasm `MAX_MSG_LEN` only (bridge hard edge) |
| Ring slots | **2048** | Wasm `MAX_MSG`; older drop when full; Load earlier for SessionStore |
| Tool summary length | **salient ≤320** | `salientToolBits` — path/counts/status only; **not** full read_file/exec/http bodies |
| Tool summary length | **salient ≤320** | `salientToolBits` — path/counts/status only; **not** full read_file/exec/http bodies. `read_file` status lines include `offset` / `limit` / `lines=returned/total`; L1 salient stays `path · N lines · M B` |
| Tool level-2 `preview` | **≤ 100k per tool** (`TOOL_RUN_PREVIEW_MAX_CHARS`), real head 40 / tail 10 lines + `… (M more lines)` | Bounded + redacted server-side; short single-line results omit it (static label). Whole-group encoded-detail budget + hard clamp keep any multi-preview `tool_run` row ≤ 262 144 B/msg |
| `tool_run` group payload | **≤ 262 144 B** (`TOOL_RUN_MSG_HARD_MAX`) | Host clips/omits memorized previews (explicit `…` or static label) rather than overflowing the ring/cloud per-msg cap |
| JSON end-of-turn toolTrace lines | **none** | All entries shown; level-2 detail stays the one-line `summary` (parity) |
Expand Down
1 change: 1 addition & 0 deletions docs/harness-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ see Group boundaries).
| Session | One `tool_run` message per group round-trips local + cloud and repaints collapsed on restore; **not** folded into the model prompt (display-only). Caveat: prior tool summaries no longer reach the model on a **continue after a mid-tool cancel** — the model sees only persisted assistant prose and may re-run or infer tools. That is the documented product rule (kept for the cancel/Copy-fed transcript). A **confirmed successful `change_dir`** still lands in the session `cwd` even when the turn later cancels / times out / hard-errors — the mid-tool-cancel re-run rule is unchanged, but the next turn boots where the model actually worked instead of against a silently-reset cwd. |
| Reload | Thinking is ephemeral and **never survives refresh**; `tool_run` + assistant are the durable transcript. On hydrate the host coalesces **consecutive** `tool_run` rows into scannable groups (`mergeToolRunPayloads`, rolling at `TOOL_RUN_ITEMS_MAX` + re-clamping the detail budget) so a long session doesn't read as a wall of `N×1` collapsed cards; rows separated by an assistant/user/error/turn-end line stay distinct. Counts stay exact after coalescing (recounted). **Coalescing is bridge/display-only** — `SessionStore` and the cloud row still hold the original N×1 `tool_run` messages (cloud PUT size, future non-bridge UIs, and debug dumps keep the uncoalesced wall; the merged groups exist only in the Wasm ring). |
| Headerless chrome | Tool-run rows are **headerless** — there is **no** `tools` kind band above the control. The only chrome is the `N tools called` expander header + its **Copy** control on that header row (never a `system` header). Same spirit as #325 DoD #5 (no System chrome on tool activity) |
| `read_file` window | Agent `read_file` returns a line-numbered window (`N→content`). Default **`limit` = 1000** lines, **`offset` ≥ 1** (1-based). Status line: `offset=… limit=… lines=returned/totalInContent`. A mid-file or default-capped read is truncated and does **not** authorize edit. 16 MiB `maxBytes` prefix is unchanged |



Expand Down
6 changes: 3 additions & 3 deletions docs/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ alternate tools (MCP / builtin HTTP) gets **403** `Sandbox access denied.` (or
|--------|------|------|---------|
| `GET` | `/health` | none | `{ ok: true, version: 2, daemonVersion: N, workspaceRoot?: "/…" }` — the per-binding jail root `R` (daemon ≥ **2**); `workspaceRoot` is **omitted** when the jail root cannot yet be resolved (liveness/version always stay 200) |
| `POST` | `/v1/list_dir` | Bearer | List directory entries |
| `POST` | `/v1/read_file` | Bearer | Read file (max 16 MiB); additive `mtimeMs` + `size` when daemon supports |
| `POST` | `/v1/read_file` | Bearer | Read file (max 16 MiB prefix from byte 0); additive `mtimeMs` + `size` when daemon supports. The **agent** `read_file` tool then windows that prefix: `offset` (1-based line, default 1) + `limit` (default 1000) return `N→` numbered lines. Daemon request shape is still `path` + `maxBytes` |
| `POST` | `/v1/write_file` | Bearer | Write file (max 16 MiB); post-write fingerprint when supported |
| `POST` | `/v1/str_replace` | Bearer | Exact string replace (unique match or `replace_all`); post-write fingerprint when supported. **Unique match and `replace_all` replace literally** — `$` in `new_string` is never a `String.replace` template (`$&`/`` $` ``/`$'`/`$$` are written byte-for-byte) |
| `POST` | `/v1/stat` | Bearer | Path metadata `{ path, type, size, mtimeMs? }` — path missing: **404** with path-missing body (e.g. `Path not found`); not bare `Not found` (that is unknown-route). No file content |
Expand Down Expand Up @@ -711,9 +711,9 @@ Agent filesystem tools enforce **read-before-edit** on the shared sandbox jail:

| Rule | Behavior |
|------|----------|
| Edit existing file | A successful full **`read_file`** of that path is required **in this agent run** before **`str_replace`** or overwriting with **`write_file`** |
| Edit existing file | A successful **full** **`read_file`** of that path is required **in this agent run** before **`str_replace`** or overwriting with **`write_file`**. Full means `offset` 1 and the window reached the end of the returned content, not clipped by `limit` or `maxBytes` |
| Create new file | **`write_file`** to a path that does not exist yet does **not** require a prior read. Existence is decided only when **`stat` reports true path-missing** (e.g. ENOENT / daemon `Path not found`) — not on bare HTTP 404 or generic `Not found` from a missing route |
| Truncated read | Does **not** authorize edit — read the full file first |
| Truncated read | Does **not** authorize edit — including a default 1000-line window on a longer file, any `offset` other than 1, or a `maxBytes` prefix. Re-read the full file (`offset` 1 and a `limit` that covers every line) first |
| Concurrent change | Before each mutate, tools **re-stat** the path. If mtime/size changed since the last observation (another browser tab, device, agent run, **`exec`**, or human on the same workspace), the tool soft-fails and the model must **`read_file` again** |
| Soft fail | Tools return `ERROR write_file:` / `ERROR str_replace:` strings — they do not throw |
| Unknown existence | If pre-mutate **`stat` fails** for a reason other than path-missing (including a daemon that does not implement **`POST /v1/stat`** yet), tools **fail closed** — they do **not** treat the path as create-new and do **not** overwrite |
Expand Down
11 changes: 11 additions & 0 deletions lib/agent/agentStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,17 @@ describe('salientToolBits cwd tools', () => {
expect(bits).toContain('cwd=invincible');
expect(bits).toContain('2 lines');
});

it('read_file windowed status line stays path · N lines · M B', () => {
const raw =
'read_file src/foo.ts offset=40 limit=20 lines=20/412 (truncated):\n40→mid\n41→next';
const bits = salientToolBits('read_file', raw);
expect(bits).toContain('src/foo.ts');
expect(bits).toContain('truncated');
expect(bits).toMatch(/2 lines/);
expect(bits).not.toContain('offset=');
expect(bits).not.toContain('40→mid');
});
});

describe('metaSandboxSwitchActiveId + activeSandboxId typed field (Phase 2 #627)', () => {
Expand Down
4 changes: 2 additions & 2 deletions lib/agent/agentStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ export function salientToolBits(name: string, resultText: string): string {
return text.replace(/\s+/g, ' ').trim().slice(0, TOOL_LINE_SALIENT_MAX);
}

// read_file path (truncated)? (cwd=...)?:\n<body>
// read_file path [offset= N limit= N lines= N[/N]] (truncated)? (cwd=...)?:\n<body>
const readM = text.match(
/^read_file\s+(\S+)((?:\s*\(truncated\))?)((?:\s+cwd=\S+)?)\s*:\s*\n?([\s\S]*)$/i,
/^read_file\s+(\S+)(?:\s+offset=\d+\s+limit=\d+\s+lines=\d+(?:\/\d+)?)?((?:\s*\(truncated\))?)((?:\s+cwd=\S+)?)\s*:\s*\n?([\s\S]*)$/i,
);
if (readM || name === 'read_file' || /(^|_)read_file$/i.test(name)) {
if (readM) {
Expand Down
5 changes: 4 additions & 1 deletion lib/agent/fileFreshness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export function createRunFileFreshness(): RunFileFreshness {
return {
recordRead(path, info) {
if (info.truncated) {
grants.set(path, { kind: 'truncated' });
// Never downgrade an existing grant (full → windowed peek).
// If the file changed on disk between reads, gate 2 (stale
// fingerprint check) catches it at edit time.
if (!grants.has(path)) grants.set(path, { kind: 'truncated' });
return;
}
const fp: DiskFingerprint = {};
Expand Down
3 changes: 3 additions & 0 deletions lib/agent/runAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,9 @@ describe('DEFAULT_AGENT_SYSTEM standing orders', () => {
expect(DEFAULT_AGENT_SYSTEM).toMatch(/read_file a path in this agent run/i);
expect(DEFAULT_AGENT_SYSTEM).toMatch(/read_file again before editing/i);
expect(DEFAULT_AGENT_SYSTEM).toMatch(/Creating a new file with write_file does not require/i);
expect(DEFAULT_AGENT_SYSTEM).toMatch(/offset=1/);
expect(DEFAULT_AGENT_SYSTEM).toMatch(/limit/);
expect(DEFAULT_AGENT_SYSTEM).toMatch(/maxBytes/);
});

it('must mention /tmp', () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/agent/runAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export const DEFAULT_AGENT_SYSTEM = [
'You are the Invincible coding agent.',
'The workspace is a remote sandbox root. Prefer tools (list_dir, read_file, write_file, str_replace, exec, change_dir, pwd) for filesystem and command work. Use str_replace for surgical edits (unique old_string unless replace_all); write_file to create or fully rewrite. For multi-line process input prefer exec stdin (heredoc alias ok) on BYO sandboxes; if exec rejects stdin (Vercel backend), write_file the input and pass the path via args instead — never claim stdin was fed when the tool errors.',
'Logical cwd starts at the workspace root (or the session cwd). Prefer change_dir into the project once, then short relative paths under that cwd. Prefer change_dir as its own step before a burst of path tools. Use pwd to inspect cwd.',
'Must read_file a path in this agent run before str_replace or overwriting an existing file with write_file. Creating a new file with write_file does not require a prior read. If tools report the file changed since your last read (another edit, command, concurrent session, or device on the same sandbox), read_file again before editing.',
'Must read_file a path in this agent run (with offset=1 covering every line of the returned content, not truncated by limit or maxBytes) before str_replace or overwriting an existing file with write_file. Creating a new file with write_file does not require a prior read. If tools report the file changed since your last read (another edit, command, concurrent session, or device on the same sandbox), read_file again before editing.',
'Tool results always show workspace-root-relative paths (and cwd= when not at root). Paths that already include the cwd prefix also work. Absolute paths are accepted when they resolve inside the sandbox root and are canonicalized to the same file as their relative form — but never invent host absolute paths outside the sandbox.',
'The workspace root is writable — use it. Never reach for /tmp.',
'Be concise in final answers; cite workspace-relative paths when useful.',
Expand Down
Loading