@oh-my-pi/pi-coding-agent 14.4.1 → 14.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/package.json +7 -7
- package/src/cli.ts +0 -1
- package/src/config/prompt-templates.ts +0 -30
- package/src/config/settings-schema.ts +68 -36
- package/src/config/settings.ts +1 -1
- package/src/edit/index.ts +1 -53
- package/src/edit/line-hash.ts +0 -53
- package/src/edit/modes/atom.ts +82 -47
- package/src/edit/modes/hashline.ts +6 -8
- package/src/edit/renderer.ts +6 -8
- package/src/edit/streaming.ts +90 -114
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +10 -15
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/modes/components/session-observer-overlay.ts +635 -295
- package/src/modes/components/settings-defs.ts +1 -5
- package/src/modes/components/tool-execution.ts +2 -5
- package/src/modes/controllers/btw-controller.ts +17 -105
- package/src/modes/controllers/command-controller.ts +16 -5
- package/src/modes/controllers/selector-controller.ts +32 -19
- package/src/modes/controllers/todo-command-controller.ts +537 -0
- package/src/modes/interactive-mode.ts +45 -10
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +17 -0
- package/src/prompts/system/irc-incoming.md +8 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -0
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/atom.md +37 -26
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/grep.md +2 -5
- package/src/prompts/tools/irc.md +49 -0
- package/src/prompts/tools/job.md +11 -0
- package/src/prompts/tools/read.md +12 -13
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +14 -5
- package/src/registry/agent-registry.ts +139 -0
- package/src/sdk.ts +35 -0
- package/src/session/agent-session.ts +226 -6
- package/src/session/session-manager.ts +13 -0
- package/src/session/session-storage.ts +4 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +32 -0
- package/src/task/executor.ts +14 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/fetch.ts +18 -6
- package/src/tools/fs-cache-invalidation.ts +0 -5
- package/src/tools/grep.ts +4 -124
- package/src/tools/index.ts +12 -6
- package/src/tools/irc.ts +258 -0
- package/src/tools/job.ts +489 -0
- package/src/tools/match-line-format.ts +7 -6
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +36 -126
- package/src/tools/renderers.ts +2 -0
- package/src/tools/todo-write.ts +243 -12
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-display-mode.ts +0 -3
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +3 -0
- package/src/web/search/providers/searxng.ts +238 -0
- package/src/web/search/types.ts +3 -1
- package/src/cli/read-cli.ts +0 -67
- package/src/commands/read.ts +0 -33
- package/src/edit/modes/chunk.ts +0 -832
- package/src/prompts/tools/cancel-job.md +0 -5
- package/src/prompts/tools/chunk-edit.md +0 -158
- package/src/prompts/tools/poll.md +0 -5
- package/src/prompts/tools/read-chunk.md +0 -73
- package/src/tools/cancel-job.ts +0 -95
- package/src/tools/poll-tool.ts +0 -173
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
|
|
2
2
|
|
|
3
|
-
export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["
|
|
3
|
+
export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["bash-tool-runtime.md","blob-artifact-architecture.md","compaction.md","config-usage.md","custom-tools.md","environment-variables.md","extension-loading.md","extensions.md","fs-scan-cache-architecture.md","gemini-manifest-extensions.md","handoff-generation-pipeline.md","hooks.md","marketplace.md","mcp-config.md","mcp-protocol-transports.md","mcp-runtime-lifecycle.md","mcp-server-tool-authoring.md","memory.md","models.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","plugin-manager-installer-plumbing.md","porting-from-pi-mono.md","porting-to-natives.md","provider-streaming-internals.md","python-repl.md","resolve-tool-runtime.md","rpc.md","rulebook-matching-pipeline.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","skills.md","skills/authoring-extensions.md","skills/authoring-hooks.md","skills/authoring-marketplaces.md","skills/examples/hello-extension/README.md","skills/examples/mini-marketplace/README.md","skills/examples/safety-hook/README.md","slash-command-internals.md","task-agent-discovery.md","theme.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","tui.md"];
|
|
4
4
|
|
|
5
5
|
export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
6
|
-
"apply_patch_spec.md": "# `apply_patch`: Codex Patch Format Specification\n\nThis document is a full, reimplementation-quality specification of the `apply_patch`\ntool used by the Codex coding harness. It covers:\n\n1. How the tool is exposed to the model (schema + freeform grammar variants).\n2. The exact prompt / instruction text shown to the model.\n3. The patch format grammar.\n4. The parser (lexical rules, lenient mode, streaming mode, errors).\n5. The application algorithm (Add / Delete / Update / Move), including the\n `seek_sequence` fuzzy matcher.\n6. Invocation forms the harness accepts (freeform args, JSON args, shell\n heredoc wrappers, stdin).\n7. Result presentation and error reporting.\n8. Test-derived edge cases.\n\nAll normative behavior below is drawn from the `codex-rs/apply-patch` crate\n(the parser in `src/parser.rs`, the applier in `src/lib.rs`, the matcher in\n`src/seek_sequence.rs`) and the tool registration in `codex-rs/tools`.\n\n---\n\n## 1. Tool registration\n\n`apply_patch` is registered in the tool registry when the current model's\n`apply_patch_tool_type` metadata is set (see\n`codex-rs/models-manager/models.json` — GPT-5.x variants use `\"freeform\"`;\nolder / non-reasoning models use `\"function\"`). It is registered as\n`supports_parallel_tool_calls = false`, meaning the harness will not issue\ntwo concurrent `apply_patch` calls. (Registration in\n`codex-rs/tools/src/tool_registry_plan.rs`.)\n\nThere are two wire formats for the same underlying command.\n\n### 1.1 Freeform (GPT-5 and later)\n\nThe freeform variant uses OpenAI's custom-tool mechanism: the model emits a\nsingle opaque string whose shape is constrained by a Lark grammar.\n\n```jsonc\n{\n \"type\": \"custom\",\n \"name\": \"apply_patch\",\n \"description\": \"Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.\",\n \"format\": {\n \"type\": \"grammar\",\n \"syntax\": \"lark\",\n \"definition\": \"<see §3.2>\"\n }\n}\n```\n\n(See `codex-rs/tools/src/apply_patch_tool.rs::create_apply_patch_freeform_tool`.)\n\nThe freeform call payload is the patch text itself — no JSON envelope, no\nwrapping quotes. Example (the model types this verbatim as the tool's\n\"input\"):\n\n```\n*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n```\n\n### 1.2 JSON function (legacy / gpt-oss)\n\nFor providers that only support function-style tool calls, a JSON variant is\nregistered:\n\n```jsonc\n{\n \"type\": \"function\",\n \"name\": \"apply_patch\",\n \"description\": \"<APPLY_PATCH_JSON_TOOL_DESCRIPTION, see §2>\",\n \"strict\": false,\n \"parameters\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"input\"],\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"The entire contents of the apply_patch command\"\n }\n }\n }\n}\n```\n\n(See `codex-rs/tools/src/apply_patch_tool.rs::create_apply_patch_json_tool`.)\n\n### 1.3 Handler dispatch\n\nBoth variants land in the same handler, which normalizes the arguments into\n`ApplyPatchToolArgs { input: String }` and passes `input` to\n`apply_patch::apply_patch(...)` (see\n`codex-rs/core/src/tools/handlers/apply_patch.rs`). The `input` is the full\npatch text, including the `*** Begin Patch` / `*** End Patch` envelope.\n\n---\n\n## 2. Agent prompt (verbatim)\n\nThe model is taught the format via two equivalent pieces of text: the\nMarkdown file `codex-rs/apply-patch/apply_patch_tool_instructions.md`\n(embedded in system prompts), and the string constant\n`APPLY_PATCH_JSON_TOOL_DESCRIPTION` in\n`codex-rs/tools/src/apply_patch_tool.rs` which is used as the JSON tool's\n`description`. They are identical in content.\n\nA reimplementation SHOULD ship the following text verbatim (note: the\noriginal uses Unicode curly quotes in a few places — they are reproduced\nhere; the final \"smart quote\" versus \"typewriter quote\" choice is not\nsemantic):\n\n````markdown\n## `apply_patch`\n\nUse the `apply_patch` shell command to edit files.\nYour patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:\n\n*** Begin Patch\n[ one or more file sections ]\n*** End Patch\n\nWithin that envelope, you get a sequence of file operations.\nYou MUST include a header to specify the action you are taking.\nEach operation starts with one of three headers:\n\n*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).\n*** Delete File: <path> - remove an existing file. Nothing follows.\n*** Update File: <path> - patch an existing file in place (optionally with a rename).\n\nMay be immediately followed by *** Move to: <new path> if you want to rename the file.\nThen one or more \"hunks\", each introduced by @@ (optionally followed by a hunk header).\nWithin a hunk each line starts with:\n\nFor instructions on [context_before] and [context_after]:\n- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change's [context_after] lines in the second change's [context_before] lines.\n- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:\n@@ class BaseClass\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n\n- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:\n\n@@ class BaseClass\n@@ \t def method():\n[3 lines of pre-context]\n- [old_code]\n+ [new_code]\n[3 lines of post-context]\n\nThe full grammar definition is below:\nPatch := Begin { FileOp } End\nBegin := \"*** Begin Patch\" NEWLINE\nEnd := \"*** End Patch\" NEWLINE\nFileOp := AddFile | DeleteFile | UpdateFile\nAddFile := \"*** Add File: \" path NEWLINE { \"+\" line NEWLINE }\nDeleteFile := \"*** Delete File: \" path NEWLINE\nUpdateFile := \"*** Update File: \" path NEWLINE [ MoveTo ] { Hunk }\nMoveTo := \"*** Move to: \" newPath NEWLINE\nHunk := \"@@\" [ header ] NEWLINE { HunkLine } [ \"*** End of File\" NEWLINE ]\nHunkLine := (\" \" | \"-\" | \"+\") text NEWLINE\n\nA full patch can combine several operations:\n\n*** Begin Patch\n*** Add File: hello.txt\n+Hello world\n*** Update File: src/app.py\n*** Move to: src/main.py\n@@ def greet():\n-print(\"Hi\")\n+print(\"Hello, world!\")\n*** Delete File: obsolete.txt\n*** End Patch\n\nIt is important to remember:\n\n- You must include a header with your intended action (Add/Delete/Update)\n- You must prefix new lines with `+` even when creating a new file\n- File references can only be relative, NEVER ABSOLUTE.\n\nYou can invoke apply_patch like:\n\n```\nshell {\"command\":[\"apply_patch\",\"*** Begin Patch\\n*** Add File: hello.txt\\n+Hello, world!\\n*** End Patch\\n\"]}\n```\n````\n\nNotes for implementers:\n\n- The \"You can invoke apply_patch like ...\" footer is only shown when the\n patch command is exposed as a `shell` command (the `codex` fallback path).\n When the freeform tool is registered, the model invokes the tool directly\n and the footer is redundant but harmless.\n- The prompt asserts some things the parser is lenient about in practice:\n - *\"File references can only be relative\"* — the parser accepts absolute\n paths, but the model is instructed to produce relative ones.\n - *\"+ line even when creating a new file\"* — `+` MUST be the first\n character of every content line in an Add File section.\n\n---\n\n## 3. Patch grammar\n\n### 3.1 EBNF (canonical)\n\n```\nPatch := Begin { FileOp }+ End\nBegin := \"*** Begin Patch\" NEWLINE\nEnd := \"*** End Patch\" [NEWLINE]\n\nFileOp := AddFile | DeleteFile | UpdateFile\nAddFile := \"*** Add File: \" path NEWLINE { \"+\" text NEWLINE }+\nDeleteFile := \"*** Delete File: \" path NEWLINE\nUpdateFile := \"*** Update File: \" path NEWLINE\n [ \"*** Move to: \" path NEWLINE ]\n Change?\n\nChange := (ChangeContext | ChangeLine)+ EofLine?\nChangeContext := (\"@@\" | \"@@ \" text) NEWLINE\nChangeLine := (\" \" | \"-\" | \"+\") text NEWLINE\nEofLine := \"*** End of File\" NEWLINE\n```\n\nThe Lark grammar that the OpenAI freeform tool uses to constrain model\noutput is in `codex-rs/tools/src/tool_apply_patch.lark`:\n\n```lark\nstart: begin_patch hunk+ end_patch\nbegin_patch: \"*** Begin Patch\" LF\nend_patch: \"*** End Patch\" LF?\n\nhunk: add_hunk | delete_hunk | update_hunk\nadd_hunk: \"*** Add File: \" filename LF add_line+\ndelete_hunk: \"*** Delete File: \" filename LF\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\n\nfilename: /(.+)/\nadd_line: \"+\" /(.*)/ LF -> line\n\nchange_move: \"*** Move to: \" filename LF\nchange: (change_context | change_line)+ eof_line?\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\neof_line: \"*** End of File\" LF\n\n%import common.LF\n```\n\n### 3.2 Reserved tokens\n\n| Token | Meaning |\n| ---------------------------- | --------------------------------------------------------------- |\n| `*** Begin Patch` | Required first significant line of the envelope. |\n| `*** End Patch` | Required last significant line (trailing LF optional). |\n| `*** Add File: <path>` | Start of an Add File section. |\n| `*** Delete File: <path>` | Standalone Delete File directive. |\n| `*** Update File: <path>` | Start of an Update File section. |\n| `*** Move to: <path>` | Optional rename target, immediately after `*** Update File:`. |\n| `@@` or `@@ <header>` | Starts a chunk inside an Update File. |\n| `*** End of File` | Terminates a chunk; asserts the chunk ended at EOF. |\n| `+<text>` / `-<text>` / ` <text>` | Added / deleted / context line inside a chunk. |\n\nAction headers match **with a trailing space**: the parser uses literal\n`strip_prefix(\"*** Add File: \")` etc. Everything after the space is the\npath; no escaping, no quoting.\n\n### 3.3 Lines and newlines\n\n- Input is split on `\\n` (LF only). CRLF is not supported by the parser —\n producers MUST use LF. (Tool output goes through Rust string handling which\n preserves CRs as literal bytes on the content lines, which then fail to\n match.)\n- Each content line in a hunk starts with exactly one byte (`+`, `-`, or\n space) followed by the line's text and then a newline. An empty ` `-prefixed\n line is representable as a single space followed by LF; a bare `+\\n` is a\n one-character added empty line.\n- A completely blank line (no prefix byte at all) inside an Update File\n section is **skipped**, and is used only for visual separation between\n chunks. This is a deliberate leniency; do not rely on blank lines to carry\n data.\n\n---\n\n## 4. Parser\n\nImplementation: `codex-rs/apply-patch/src/parser.rs`.\n\n### 4.1 Public entry points\n\n- `parse_patch(text: &str) -> Result<ApplyPatchArgs, ParseError>` —\n production parse. Uses `ParseMode::Lenient` (see §4.3).\n- `parse_patch_streaming(text: &str) -> Result<ApplyPatchArgs, ParseError>` —\n same format, but tolerates a missing `*** End Patch` (the harness calls\n this from a streaming response handler to show progress). Its output\n MUST NOT be used to actually apply the patch.\n\nThe result is:\n\n```rust\npub struct ApplyPatchArgs {\n pub patch: String, // canonicalized patch text (heredoc stripped)\n pub hunks: Vec<Hunk>,\n pub workdir: Option<String>, // populated only when parsing a shell\n // invocation that begins with `cd <path> &&`\n}\n```\n\n### 4.2 Lexical canonicalization\n\nBefore parsing, the input is `trim()`ed, then split into lines by `\\n`.\nMarker-line matching is done against `line.trim()`, so arbitrary\nleading/trailing whitespace around the sentinel lines (`*** Begin Patch`,\netc.) is accepted. Content lines (those starting with `+`, `-`, ` `) are\nNOT trimmed — their whitespace is significant.\n\n### 4.3 Parse modes\n\nThe parser operates in one of three modes:\n\n1. **Strict** — requires line 0 = `*** Begin Patch` and the last line to be\n `*** End Patch`. Not used by the harness today (`PARSE_IN_STRICT_MODE =\n false`); retained as a fallback.\n\n2. **Lenient** (default). Tries strict first. If that fails, attempts to\n strip a heredoc wrapper: the first line must be exactly one of\n `<<EOF`, `<<'EOF'`, or `<<\"EOF\"`, and the last line must be `EOF`. The\n inner region is then parsed strictly. This was introduced to handle\n gpt-4.1, which insisted on wrapping the patch in a heredoc body.\n Mismatched quotes (e.g. `<<\"EOF'`) are rejected.\n\n3. **Streaming** — requires `*** Begin Patch` but does NOT require\n `*** End Patch`. Individual hunks are parsed on a best-effort basis; the\n last incomplete hunk is dropped. Used for progress UI only.\n\n### 4.4 Hunk parsing state machine\n\n```\nloop:\n trim line[i]\n if line[i] starts with \"*** End Patch\": break\n match line[i].strip_prefix(...):\n \"*** Add File: \" -> parse AddFile\n \"*** Delete File: \" -> parse DeleteFile\n \"*** Update File: \" -> parse UpdateFile\n otherwise -> InvalidHunkError at line i\n```\n\n**AddFile** consumes subsequent lines while they start with `+`. Each line\nbecomes one element of `contents`, with the leading `+` stripped. The lines\nare joined with `\\n`; an additional `\\n` is appended after the join so the\nresulting file ends with exactly one newline.\n\n**DeleteFile** consumes only the header line; no content follows.\n\n**UpdateFile** consumes the header, optionally a `*** Move to: <path>` line,\nthen zero or more chunks. The UpdateFile section ends when the next line\nstarts with `***` (the next hunk header, or `*** End Patch`) or input is\nexhausted. An UpdateFile section with zero chunks is an error\n(`\"Update file hunk for path '<p>' is empty\"`).\n\n**Chunk** parsing within an UpdateFile:\n\n```\nchunk:\n optional context line:\n \"@@\" -> change_context = None (empty marker)\n \"@@ <header>\" -> change_context = Some(\"<header>\")\n otherwise -> if this is the FIRST chunk of the hunk, fall through\n (context-less first chunk); otherwise error.\n then:\n loop over lines:\n if line starts with \"*\" -> stop (end of chunk; next hunk or end)\n if line == \"\" -> append empty string to BOTH old_lines\n and new_lines (empty context line)\n if line starts with ' ' -> append line[1..] to BOTH old_lines and\n new_lines\n if line starts with '-' -> append line[1..] to old_lines\n if line starts with '+' -> append line[1..] to new_lines\n if line == \"*** End of File\" -> set is_end_of_file = true, stop\n otherwise -> error: unexpected line in update hunk\n```\n\nInvariants enforced by the parser:\n\n- A chunk must contain at least one non-context line (pure context chunks\n are rejected).\n- `*** End of File` cannot be the first line of a chunk.\n- Chunks are stored in order and the applier relies on each chunk's match\n position being ≥ the previous chunk's match position.\n\n### 4.5 Error taxonomy\n\n```rust\npub enum ParseError {\n InvalidPatchError(String), // envelope errors\n InvalidHunkError { message: String, line_number: usize }, // content errors\n}\n```\n\nSpecific messages (implementers SHOULD match these literally so tests and\ndownstream tools keep working):\n\n| Error | Condition |\n| ------------------------------------ | ---------------------------------------------------------------------- |\n| `The first line of the patch must be '*** Begin Patch'` | Missing/wrong `Begin` marker. |\n| `The last line of the patch must be '*** End Patch'` | Missing/wrong `End` marker (strict/lenient only). |\n| `'<line>' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'` | Unknown file-level directive. |\n| `Update file hunk for path '<p>' is empty` | UpdateFile section has zero chunks. |\n| `Expected update hunk to start with a @@ context marker, got: '<line>'` | Missing `@@` when required (non-first chunk). |\n| `Update hunk does not contain any lines` | Chunk with a context but no +/-/space line before EOF/next chunk. |\n| `Unexpected line found in update hunk: '<line>'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)` | Invalid diff prefix. |\n\n---\n\n## 5. Intermediate representation\n\n```rust\npub enum Hunk {\n AddFile { path: PathBuf, contents: String },\n DeleteFile { path: PathBuf },\n UpdateFile {\n path: PathBuf,\n move_path: Option<PathBuf>,\n chunks: Vec<UpdateFileChunk>, // MUST be non-empty\n },\n}\n\npub struct UpdateFileChunk {\n pub change_context: Option<String>, // text after \"@@ \"; None for bare \"@@\"\n pub old_lines: Vec<String>, // lines to match in the file\n pub new_lines: Vec<String>, // replacement lines\n pub is_end_of_file: bool, // \"*** End of File\" present\n}\n```\n\nPaths are stored exactly as the patch wrote them — no canonicalization.\nResolution to an absolute path happens at apply time via\n`AbsolutePathBuf::resolve_path_against_base(path, cwd)`.\n\n---\n\n## 6. Application algorithm\n\nImplementation: `codex-rs/apply-patch/src/lib.rs`.\n\n### 6.1 Top-level flow\n\n```\napply_patch(input, cwd, fs, sandbox):\n args := parse_patch(input)?\n if args.hunks is empty: error \"No files were modified.\"\n affected := { added: [], modified: [], deleted: [] }\n for each hunk in args.hunks:\n apply_hunk(hunk, cwd, fs, sandbox, affected)?\n return affected\n```\n\nHunks are applied **in the order they appear in the patch** and **not\natomically**: if hunk N fails, hunks `0..N-1` have already written to disk\nand are not rolled back. Hunks `N+1..` are skipped. (Test scenario\n`015_failure_after_partial_success_leaves_changes` pins this behavior.)\n\nA reimplementation MAY add transactional semantics, but MUST document the\ndeviation — callers today rely on partial application being observable.\n\n### 6.2 Add File\n\n```\npath_abs := resolve_path_against_base(hunk.path, cwd)\ntry: fs.write_file(path_abs, contents)\n on NotFound: fs.create_directory(parent(path_abs), { recursive: true })\n fs.write_file(path_abs, contents)\nappend hunk.path to affected.added\n```\n\nCharacteristics:\n\n- Parent directories are created on demand (recursive). This is done lazily\n — only after a first write fails with `NotFound`.\n- An existing file at `path_abs` is **silently overwritten** (scenario\n `011_add_overwrites_existing_file`). No confirmation, no diff.\n- `contents` is the literal joined string from the parser — the parser\n already terminates it with `\\n`.\n\n### 6.3 Delete File\n\n```\npath_abs := resolve_path_against_base(hunk.path, cwd)\nmeta := fs.get_metadata(path_abs)\nif meta.is_directory: error \"path is a directory\"\nfs.remove(path_abs, { recursive: false, force: false })\nappend hunk.path to affected.deleted\n```\n\n- If the file does not exist, the metadata call returns `NotFound` and the\n whole `apply_patch` fails with `Failed to delete file <p>: ...`.\n- Deleting directories is explicitly rejected.\n\n### 6.4 Update File\n\n```\napplied := derive_new_contents_from_chunks(path_abs, hunk.chunks, fs)?\nif hunk.move_path.is_some():\n dest_abs := resolve_path_against_base(move_path, cwd)\n write_with_missing_parent_retry(dest_abs, applied.new_contents)\n ensure source isn't a directory; fs.remove(path_abs)\nelse:\n fs.write_file(path_abs, applied.new_contents)\nappend hunk.path to affected.modified\n```\n\nNote: a renamed file is reported as **modified** (`M`) with the original\npath, not as `D` + `A`. This is intentional — it mirrors git's rename\ndetection.\n\n`derive_new_contents_from_chunks`:\n\n```\ntext := fs.read_file_text(path_abs) // utf-8, error if missing\nlines := text.split('\\n') // retains trailing '' if text ends '\\n'\nif lines.last() == \"\": lines.pop() // normalize off trailing-\\n artifact\n\nreplacements := compute_replacements(lines, chunks)?\nnew_lines := apply_replacements(lines, replacements)\nif new_lines.last() != \"\": new_lines.push(\"\") // re-add trailing newline\nreturn new_lines.join('\\n')\n```\n\nPostcondition: **every update produces a file ending in exactly one `\\n`**,\nregardless of whether the input had one. Reimplementations MAY choose to\npreserve \"no trailing newline\" when present — doing so is a deliberate\ndeviation.\n\n### 6.5 Computing replacements\n\n```\nline_index := 0\nreplacements := []\nfor each chunk:\n # 1. If the chunk has a \"@@ ctx\" marker, locate that line first.\n if chunk.change_context.is_some():\n idx := seek_sequence([ctx], lines, start=line_index, eof=false)?\n line_index := idx + 1 # search for old_lines AFTER ctx\n\n # 2. Pure-addition chunks (old_lines empty):\n if chunk.old_lines.is_empty():\n insertion_idx := (if lines.last() == \"\" then lines.len() - 1\n else lines.len())\n replacements.push((insertion_idx, 0, chunk.new_lines))\n continue\n\n # 3. Normal replacement: match old_lines in the file.\n pattern := chunk.old_lines\n new_slc := chunk.new_lines\n found := seek_sequence(lines, pattern, line_index, chunk.is_end_of_file)\n if found.is_none() and pattern.last() == \"\":\n pattern := pattern[..pattern.len()-1] # drop trailing empty sentinel\n if new_slc.last() == \"\":\n new_slc := new_slc[..new_slc.len()-1]\n found := seek_sequence(lines, pattern, line_index, chunk.is_end_of_file)\n match found:\n Some(start): replacements.push((start, pattern.len(), new_slc))\n line_index := start + pattern.len()\n None: error \"Failed to find expected lines in <path>:\\n<old_lines joined>\"\n\nreplacements.sort_by_start_index()\n```\n\nKey invariants:\n\n- Context (`@@ ctx`) is matched by *one line*; it is a locator only and is\n never modified. After a successful context match, the old_lines search\n begins **immediately after** the context line.\n- A chunk's `old_lines` search starts at `line_index` (the cursor after the\n previous chunk), so chunks must appear in file order.\n- Pure-addition chunks (no `-` or ` ` lines, only `+`) append at the end of\n the file. They do NOT honor `line_index`; they always go to the end.\n- The \"trailing empty sentinel\" retry exists because unified-diff-style\n tools often emit a blank line at the end of `old_lines` representing the\n file's terminal newline. Our line-splitting strips that element from the\n file, so a literal match fails; we retry with the sentinel removed.\n\nReplacements are then applied **in reverse order of `start_index`** so\nearlier edits do not shift later edits' indices:\n\n```\napply_replacements(lines, replacements):\n for (start, old_len, new_seg) in replacements.reversed():\n delete lines[start .. start + old_len]\n insert new_seg at position start\n```\n\n### 6.6 `seek_sequence` — the fuzzy matcher\n\nImplementation: `codex-rs/apply-patch/src/seek_sequence.rs`.\n\nSignature:\n\n```rust\nfn seek_sequence(\n lines: &[String], pattern: &[String],\n start: usize, eof: bool,\n) -> Option<usize>\n```\n\nContract:\n\n- Returns the smallest `i ≥ search_start` such that\n `lines[i..i + pattern.len()]` matches `pattern` under one of four match\n predicates (tried in order). `search_start = lines.len() - pattern.len()`\n if `eof` and the pattern fits, else `start`.\n- Empty `pattern` → `Some(start)`.\n- `pattern.len() > lines.len()` → `None` (MUST NOT panic).\n\nThe four match predicates, tried in order (first success wins):\n\n1. **Exact**. `lines[i + k] == pattern[k]` for all k.\n2. **Rstrip**. `lines[i + k].trim_end() == pattern[k].trim_end()`.\n3. **Full trim**. `lines[i + k].trim() == pattern[k].trim()`.\n4. **Unicode-normalized trim**. Trim, then fold common typographic\n punctuation to ASCII, then compare.\n\nNormalization table (MUST be implemented identically):\n\n| Folded to | Source code points |\n| --------- | ------------------ |\n| `-` | U+2010 HYPHEN, U+2011 NON-BREAKING HYPHEN, U+2012 FIGURE DASH, U+2013 EN DASH, U+2014 EM DASH, U+2015 HORIZONTAL BAR, U+2212 MINUS SIGN |\n| `'` | U+2018, U+2019, U+201A, U+201B |\n| `\"` | U+201C, U+201D, U+201E, U+201F |\n| ` ` (space) | U+00A0 NBSP, U+2002, U+2003, U+2004, U+2005, U+2006, U+2007, U+2008, U+2009, U+200A, U+202F, U+205F, U+3000 |\n\nAll other code points are passed through unchanged. This lets the model\nemit ASCII hyphens/quotes/spaces even when the source file contains\ntypographic variants (e.g. an em-dash pasted from a doc).\n\n**Per-chunk EOF hint.** When `eof == true` (set from `is_end_of_file`), the\nmatcher first tries to match at the tail of the file\n(`i = lines.len() - pattern.len()`) before falling through to the normal\nforward search from `start`.\n\n**What's deliberately not supported.**\n\n- No \"floating\" / best-effort match. If all four passes fail, the chunk\n fails; there is no nearest-match heuristic.\n- No matching across non-adjacent lines — the pattern must appear as a\n contiguous block.\n- No multi-match disambiguation: the **first** match wins. Chunks must\n carry enough context (or a `@@ header`) that the first hit at or after\n `line_index` is the intended one.\n\n---\n\n## 7. Path resolution\n\n- `cwd` is the `AbsolutePathBuf` passed to `apply_patch(...)`. In the CLI /\n harness it defaults to the process working directory, possibly further\n qualified by a workdir extracted from a `cd X && apply_patch <<EOF` shell\n invocation (§8.3).\n- `resolve_path_against_base(path, cwd)`:\n - If `path` is absolute → `path` (cwd is ignored).\n - If `path` is relative → `cwd.join(path)`.\n- The patch grammar does not define an escape mechanism. Paths containing\n spaces, tabs, or Unicode are supported as-is (the parser takes the full\n rest of the header line); paths containing literal newlines are\n unrepresentable by construction.\n- A `FileSystemSandboxContext` MAY be passed in; when present, every\n filesystem call is routed through it. All of `read_file_text`,\n `write_file`, `remove`, `get_metadata`, `create_directory` receive the\n sandbox. The sandbox is responsible for enforcing path restrictions —\n the applier does no check of its own.\n\n---\n\n## 8. Invocation forms\n\nThe applier accepts the patch text through several transport layers. A\nreimplementation only strictly needs §8.1 (tool arg) — the others exist for\nhistorical compatibility.\n\n### 8.1 Direct tool argument (freeform or JSON)\n\nThe preferred form. Either the freeform tool's `input` or the JSON tool's\n`input` string is the full patch, e.g.:\n\n```\n*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n```\n\n### 8.2 Heredoc-wrapped\n\nWhen the patch is invoked via `shell` (the legacy path), the model wraps\nthe patch in a heredoc. The parser's lenient mode strips the outermost\nheredoc wrapper:\n\n```\n<<EOF\n*** Begin Patch\n...\n*** End Patch\nEOF\n```\n\nThe opener must be one of `<<EOF`, `<<'EOF'`, `<<\"EOF\"`; the closer must be\n`EOF` on its own line. Mismatched quoting (`<<\"EOF'`) or a missing closer\nis rejected.\n\n### 8.3 Shell script with workdir\n\nThe harness also recognizes a `cd <path> && apply_patch <<'EOF' ... EOF`\nshell invocation (parsed via Tree-sitter in\n`codex-rs/apply-patch/src/invocation.rs`). The `<path>` is extracted into\n`ApplyPatchArgs.workdir` and used to qualify `cwd` before applying hunks.\nAny other pre- or post-commands cause the parse to fail over to \"treat as\na regular shell command\" rather than `apply_patch`.\n\n### 8.4 stdin (standalone executable)\n\n`codex-rs/apply-patch/src/standalone_executable.rs` lets the binary be\ninvoked as `apply_patch` with the patch on argv[1], OR with no args and the\npatch piped on stdin.\n\n---\n\n## 9. Result presentation\n\n### 9.1 Success\n\nAfter a successful apply, the caller renders a git-style summary\n(`codex-rs/apply-patch/src/lib.rs::print_summary`):\n\n```\nSuccess. Updated the following files:\nA <added path 1>\nA <added path 2>\nM <modified or renamed path>\nD <deleted path>\n```\n\n- Sections appear in the order Added / Modified / Deleted.\n- Paths are the ones spelled in the patch (not canonicalized).\n- Renamed files appear under `M` with the **original** path, not the\n destination.\n- Exit status 0.\n\n### 9.2 Failure\n\n- Parse errors: written to stderr as\n `Invalid patch: <message>` or\n `Invalid patch hunk on line <N>: <message>`.\n- Apply errors (context miss / old_lines miss / IO): written to stderr\n with the Rust `anyhow` chain, e.g.:\n - `Failed to find context '<ctx>' in <path>`\n - `Failed to find expected lines in <path>:\\n<block>`\n - `Failed to read file to update <path>: <io err>`\n - `Failed to write file <path>: <io err>`\n - `Failed to delete file <path>: <io err>`\n - `Failed to remove original <path>: <io err>`\n - `Failed to create parent directories for <path>: <io err>`\n- Exit status 1 (apply/parse failure) or 2 (argv usage error).\n\n### 9.3 Harness-side tool call result\n\nWhen invoked through the harness, the handler wraps the above in\n`ExecToolCallOutput { exit_code, stdout, stderr, aggregated_output, duration,\ntimed_out }`. The model sees `aggregated_output`.\n\n### 9.4 Progress events\n\nThe `PatchApplyUpdatedEvent` is emitted to the TUI as each hunk is applied\n(only when the progress feature is on). This is a UX detail and is not part\nof the observable patch semantics.\n\n---\n\n## 10. Edge cases (test-derived)\n\n| Case | Behavior |\n| --------------------------------------------------- | -------- |\n| Patch with zero hunks | Error: `No files were modified.` |\n| Add File overwriting an existing file | Silent overwrite. |\n| Delete File on a directory | Error: `path is a directory`. |\n| Delete File on a nonexistent file | Error propagated from `fs.get_metadata` / `fs.remove`. |\n| Move to an existing destination | Destination overwritten; source removed. |\n| Update File with 0 chunks | Parse error: `Update file hunk for path '<p>' is empty`. |\n| Chunk with only `+` lines (pure addition) | Inserts at end of file (before final empty line if any). |\n| Chunk whose `old_lines` end in an empty string | Retry without the trailing empty; lets EOF edits match. |\n| Patch with `*** End of File` marker | `is_end_of_file = true`; matcher tries tail-of-file first. |\n| Unicode dash/quote/NBSP mismatch between patch and file | Normalized-trim match (4th seek pass) matches. |\n| Leading/trailing whitespace on a sentinel line | Ignored (`line.trim()` before marker compare). |\n| Blank line inside an Update File between chunks | Ignored (used as visual separator). |\n| Heredoc wrapper around the whole patch | Stripped in Lenient mode. |\n| Streaming: `*** End Patch` absent yet | OK in `parse_patch_streaming`; last incomplete hunk is dropped. |\n| First chunk of an Update File lacks `@@` | Allowed (context-less first chunk). |\n| Non-first chunk missing `@@` | Parse error. |\n| Absolute path in patch | Accepted by parser; model is told not to emit these. |\n| Multiple chunks touching the same file | Applied in reverse start-order; must be in file-order in the patch. |\n| One hunk of N fails | Prior hunks remain applied; later hunks skipped. |\n| File with no trailing newline as input | Output gains one (post-condition). |\n\n---\n\n## 11. Reimplementation checklist\n\nTo reimplement this format end-to-end, a conforming implementation MUST:\n\n- [ ] Accept the exact sentinel tokens in §3.2 with the trailing space\n where required; match marker lines after `trim()` only.\n- [ ] Parse the grammar in §3.1 including the context-less first chunk\n allowance, the `*** End of File` terminator, blank-line separation\n between chunks, and the `*** Move to:` renames.\n- [ ] Implement Lenient mode (heredoc strip) and Streaming mode as\n described in §4.3.\n- [ ] Emit the error messages in §4.5 verbatim (test compatibility).\n- [ ] For Update File, read the target with UTF-8, split on `\\n`, drop the\n trailing empty element, apply chunks via the replacement machinery in\n §6.5, and re-add a trailing newline before writing.\n- [ ] Implement `seek_sequence` with the four-pass strictness hierarchy and\n the exact Unicode normalization table in §6.6, including the\n pattern-longer-than-input → `None` guard and the `eof` tail-first\n search.\n- [ ] Apply hunks sequentially and non-atomically; do not rollback on\n partial failure.\n- [ ] Silently overwrite existing destinations for Add File and Move.\n- [ ] Emit the `Success. Updated the following files:` / `A`/`M`/`D`\n summary in §9.1 on success, and stderr messages in §9.2 on failure.\n- [ ] Register both freeform-grammar and JSON-function tool variants with\n `supports_parallel_tool_calls = false`.\n- [ ] Ship the agent prompt in §2 verbatim.\n\nOptional / harness features (not required for correctness):\n\n- Heredoc `cd <dir> && apply_patch <<'EOF' ... EOF` shell-form detection\n with workdir extraction.\n- Streaming progress events to the UI.\n- Unified-diff rendering (`unified_diff_from_chunks`) for displaying a\n user-visible diff after apply.\n",
|
|
7
6
|
"bash-tool-runtime.md": "# Bash tool runtime\n\nThis document describes the **`bash` tool** runtime path used by agent tool calls, from command normalization to execution, truncation/artifacts, and rendering.\n\nIt also calls out where behavior diverges in interactive TUI, print mode, RPC mode, and user-initiated bang (`!`) shell execution.\n\n## Scope and runtime surfaces\n\nThere are two different bash execution surfaces in coding-agent:\n\n1. **Tool-call surface** (`toolName: \"bash\"`): used when the model calls the bash tool.\n - Entry point: `BashTool.execute()`.\n2. **User bang-command surface** (`!cmd` from interactive input or RPC `bash` command): session-level helper path.\n - Entry point: `AgentSession.executeBash()`.\n\nBoth eventually use `executeBash()` in `src/exec/bash-executor.ts` for non-PTY execution, but only the tool-call path runs normalization/interception and tool renderer logic.\n\n## End-to-end tool-call pipeline\n\n## 1) Input normalization and parameter merge\n\n`BashTool.execute()` first normalizes the raw command via `normalizeBashCommand()`:\n\n- extracts trailing `| head -n N`, `| head -N`, `| tail -n N`, `| tail -N` into structured limits,\n- trims trailing/leading whitespace,\n- keeps internal whitespace intact.\n\nThen it merges extracted limits with explicit tool args:\n\n- explicit `head`/`tail` args override extracted values,\n- extracted values are fallback only.\n\n### Caveat\n\n`bash-normalize.ts` comments mention stripping `2>&1`, but current implementation does not remove it. Runtime behavior is still correct (stdout/stderr are already merged), but the normalization behavior is narrower than comments suggest.\n\n## 2) Optional interception (blocked-command path)\n\nIf `bashInterceptor.enabled` is true, `BashTool` loads rules from settings and runs `checkBashInterception()` against the normalized command.\n\nInterception behavior:\n\n- command is blocked **only** when:\n - regex rule matches, and\n - the suggested tool is present in `ctx.toolNames`.\n- invalid regex rules are silently skipped.\n- on block, `BashTool` throws `ToolError` with message:\n - `Blocked: ...`\n - original command included.\n\nDefault rule patterns (defined in code) target common misuses:\n\n- file readers (`cat`, `head`, `tail`, ...)\n- search tools (`grep`, `rg`, ...)\n- file finders (`find`, `fd`, ...)\n- in-place editors (`sed -i`, `perl -i`, `awk -i inplace`)\n- shell redirection writes (`echo ... > file`, heredoc redirection)\n\n### Caveat\n\n`InterceptionResult` includes `suggestedTool`, but `BashTool` currently surfaces only the message text (no structured suggested-tool field in `details`).\n\n## 3) CWD validation and timeout clamping\n\n`cwd` is resolved relative to session cwd (`resolveToCwd`), then validated via `stat`:\n\n- missing path -> `ToolError(\"Working directory does not exist: ...\")`\n- non-directory -> `ToolError(\"Working directory is not a directory: ...\")`\n\nTimeout is clamped to `[1, 3600]` seconds and converted to milliseconds.\n\n## 4) Artifact allocation\n\nBefore execution, the tool allocates an artifact path/id (best-effort) for truncated output storage.\n\n- artifact allocation failure is non-fatal (execution continues without artifact spill file),\n- artifact id/path are passed into execution path for full-output persistence on truncation.\n\n## 5) PTY vs non-PTY execution selection\n\n`BashTool` chooses PTY execution only when all are true:\n\n- `bash.virtualTerminal === \"on\"`\n- `PI_NO_PTY !== \"1\"`\n- tool context has UI (`ctx.hasUI === true` and `ctx.ui` set)\n\nOtherwise it uses non-interactive `executeBash()`.\n\nThat means print mode and non-UI RPC/tool contexts always use non-PTY.\n\n## Non-interactive execution engine (`executeBash`)\n\n## Shell session reuse model\n\n`executeBash()` caches native `Shell` instances in a process-global map keyed by:\n\n- shell path,\n- configured command prefix,\n- snapshot path,\n- serialized shell env,\n- optional agent session key.\n\nFor session-level executions, `AgentSession.executeBash()` passes `sessionKey: this.sessionId`, isolating reuse per session.\n\nTool-call path does **not** pass `sessionKey`, so reuse scope is based on shell config/snapshot/env.\n\n## Shell config and snapshot behavior\n\nAt each call, executor loads settings shell config (`shell`, `env`, optional `prefix`).\n\nIf selected shell includes `bash`, it attempts `getOrCreateSnapshot()`:\n\n- snapshot captures aliases/functions/options from user rc,\n- snapshot creation is best-effort,\n- failure falls back to no snapshot.\n\nIf `prefix` is configured, command becomes:\n\n```text\n<prefix> <command>\n```\n\n## Streaming and cancellation\n\n`Shell.run()` streams chunks to callback. Executor pipes each chunk into `OutputSink` and optional `onChunk` callback.\n\nCancellation:\n\n- aborted signal triggers `shellSession.abort(...)`,\n- timeout from native result is mapped to `cancelled: true` + annotation text,\n- explicit cancellation similarly returns `cancelled: true` + annotation.\n\nNo exception is thrown inside executor for timeout/cancel; it returns structured `BashResult` and lets caller map error semantics.\n\n## Interactive PTY path (`runInteractiveBashPty`)\n\nWhen PTY is enabled, tool runs `runInteractiveBashPty()` which opens an overlay console component and drives a native `PtySession`.\n\nBehavior highlights:\n\n- xterm-headless virtual terminal renders viewport in overlay,\n- keyboard input is normalized (including Kitty sequences and application cursor mode handling),\n- `esc` while running kills the PTY session,\n- terminal resize propagates to PTY (`session.resize(cols, rows)`).\n\nEnvironment hardening defaults are injected for unattended runs:\n\n- pagers disabled (`PAGER=cat`, `GIT_PAGER=cat`, etc.),\n- editor prompts disabled (`GIT_EDITOR=true`, `EDITOR=true`, ...),\n- terminal/auth prompts reduced (`GIT_TERMINAL_PROMPT=0`, `SSH_ASKPASS=/usr/bin/false`, `CI=1`),\n- package-manager/tool automation flags for non-interactive behavior.\n\nPTY output is normalized (`CRLF`/`CR` to `LF`, `sanitizeText`) and written into `OutputSink`, including artifact spill support.\n\nOn PTY startup/runtime error, sink receives `PTY error: ...` line and command finalizes with undefined exit code.\n\n## Output handling: streaming, truncation, artifact spill\n\nBoth PTY and non-PTY paths use `OutputSink`.\n\n## OutputSink semantics\n\n- keeps an in-memory UTF-8-safe tail buffer (`DEFAULT_MAX_BYTES`, currently 50KB),\n- tracks total bytes/lines seen,\n- if artifact path exists and output overflows (or file already active), writes full stream to artifact file,\n- when memory threshold overflows, trims in-memory buffer to tail (UTF-8 boundary safe),\n- marks `truncated` when overflow/file spill occurs.\n\n`dump()` returns:\n\n- `output` (possibly annotated prefix),\n- `truncated`,\n- `totalLines/totalBytes`,\n- `outputLines/outputBytes`,\n- `artifactId` if artifact file was active.\n\n### Long-output caveat\n\nRuntime truncation is byte-threshold based in `OutputSink` (50KB default). It does not enforce a hard 2000-line cap in this code path.\n\n## Live tool updates\n\nFor non-PTY execution, `BashTool` uses a separate `TailBuffer` for partial updates and emits `onUpdate` snapshots while command is running.\n\nFor PTY execution, live rendering is handled by custom UI overlay, not by `onUpdate` text chunks.\n\n## Result shaping, metadata, and error mapping\n\nAfter execution:\n\n1. `cancelled` handling:\n - if abort signal is aborted -> throw `ToolAbortError` (abort semantics),\n - else -> throw `ToolError` (treated as tool failure).\n2. PTY `timedOut` -> throw `ToolError`.\n3. apply head/tail filters to final output text (`applyHeadTail`, head then tail).\n4. empty output becomes `(no output)`.\n5. attach truncation metadata via `toolResult(...).truncationFromSummary(result, { direction: \"tail\" })`.\n6. exit-code mapping:\n - missing exit code -> `ToolError(\"... missing exit status\")`\n - non-zero exit -> `ToolError(\"... Command exited with code N\")`\n - zero exit -> success result.\n\nSuccess payload structure:\n\n- `content`: text output,\n- `details.meta.truncation` when truncated, including:\n - `direction`, `truncatedBy`, total/output line+byte counts,\n - `shownRange`,\n - `artifactId` when available.\n\nBecause built-in tools are wrapped with `wrapToolWithMetaNotice()`, truncation notice text is appended to final text content automatically (for example: `Full: artifact://<id>`).\n\n## Rendering paths\n\n## Tool-call renderer (`bashToolRenderer`)\n\n`bashToolRenderer` is used for tool-call messages (`toolCall` / `toolResult`):\n\n- collapsed mode shows visual-line-truncated preview,\n- expanded mode shows all currently available output text,\n- warning line includes truncation reason and `artifact://<id>` when truncated,\n- timeout value (from args) is shown in footer metadata line.\n\n### Caveat: full artifact expansion\n\n`BashRenderContext` has `isFullOutput`, but current renderer context builder does not set it for bash tool results. Expanded view still uses the text already in result content (tail/truncated output) unless another caller provides full artifact content.\n\n## User bang-command component (`BashExecutionComponent`)\n\n`BashExecutionComponent` is for user `!` commands in interactive mode (not model tool calls):\n\n- streams chunks live,\n- collapsed preview keeps last 20 logical lines,\n- line clamp at 4000 chars per line,\n- shows truncation + artifact warnings when metadata is present,\n- marks cancelled/error/exit state separately.\n\nThis component is wired by `CommandController.handleBashCommand()` and fed from `AgentSession.executeBash()`.\n\n## Mode-specific behavior differences\n\n| Surface | Entry path | PTY eligible | Live output UX | Error surfacing |\n| ------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------ |\n| Interactive tool call | `BashTool.execute` | Yes, when `bash.virtualTerminal=on` and UI exists and `PI_NO_PTY!=1` | PTY overlay (interactive) or streamed tail updates | Tool errors become `toolResult.isError` |\n| Print mode tool call | `BashTool.execute` | No (no UI context) | No TUI overlay; output appears in event stream/final assistant text flow | Same tool error mapping |\n| RPC tool call (agent tooling) | `BashTool.execute` | Usually no UI -> non-PTY | Structured tool events/results | Same tool error mapping |\n| Interactive bang command (`!`) | `AgentSession.executeBash` + `BashExecutionComponent` | No (uses executor directly) | Dedicated bash execution component | Controller catches exceptions and shows UI error |\n| RPC `bash` command | `rpc-mode` -> `session.executeBash` | No | Returns `BashResult` directly | Consumer handles returned fields |\n\n## Operational caveats\n\n- Interceptor only blocks commands when suggested tool is currently available in context.\n- If artifact allocation fails, truncation still occurs but no `artifact://` back-reference is available.\n- Shell session cache has no explicit eviction in this module; lifetime is process-scoped.\n- PTY and non-PTY timeout surfaces differ:\n - PTY exposes explicit `timedOut` result field,\n - non-PTY maps timeout into `cancelled + annotation` summary.\n\n## Implementation files\n\n- [`src/tools/bash.ts`](../packages/coding-agent/src/tools/bash.ts) — tool entrypoint, normalization/interception, PTY/non-PTY selection, result/error mapping, bash tool renderer.\n- [`src/tools/bash-normalize.ts`](../packages/coding-agent/src/tools/bash-normalize.ts) — command normalization and post-run head/tail filtering.\n- [`src/tools/bash-interceptor.ts`](../packages/coding-agent/src/tools/bash-interceptor.ts) — interceptor rule matching and blocked-command messages.\n- [`src/exec/bash-executor.ts`](../packages/coding-agent/src/exec/bash-executor.ts) — non-PTY executor, shell session reuse, cancellation wiring, output sink integration.\n- [`src/tools/bash-interactive.ts`](../packages/coding-agent/src/tools/bash-interactive.ts) — PTY runtime, overlay UI, input normalization, non-interactive env defaults.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/artifact spill and summary metadata.\n- [`src/tools/output-utils.ts`](../packages/coding-agent/src/tools/output-utils.ts) — artifact allocation helpers and streaming tail buffer.\n- [`src/tools/output-meta.ts`](../packages/coding-agent/src/tools/output-meta.ts) — truncation metadata shape + notice injection wrapper.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level `executeBash`, message recording, abort lifecycle.\n- [`src/modes/components/bash-execution.ts`](../packages/coding-agent/src/modes/components/bash-execution.ts) — interactive `!` command execution component.\n- [`src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts) — wiring for interactive `!` command UI stream/update completion.\n- [`src/modes/rpc/rpc-mode.ts`](../packages/coding-agent/src/modes/rpc/rpc-mode.ts) — RPC `bash` and `abort_bash` command surface.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://<id>` resolution.\n",
|
|
8
7
|
"blob-artifact-architecture.md": "# Blob and artifact storage architecture\n\nThis document describes how coding-agent stores large/binary payloads outside session JSONL, how truncated tool output is persisted, and how internal URLs (`artifact://`, `agent://`) resolve back to stored data.\n\n## Why two storage systems exist\n\nThe runtime uses two different persistence mechanisms for different data shapes:\n\n- **Content-addressed blobs** (`blob:sha256:<hash>`): global, binary-oriented storage used to externalize large image base64 payloads from persisted session entries.\n- **Session-scoped artifacts** (files under `<sessionFile-without-.jsonl>/`): per-session text files used for full tool outputs and subagent outputs.\n\nThey are intentionally separate:\n\n- blob storage optimizes deduplication and stable references by content hash,\n- artifact storage optimizes append-only session tooling and human/tool retrieval by local IDs.\n\n## Storage boundaries and on-disk layout\n\n## Blob store boundary (global)\n\n`SessionManager` constructs `BlobStore(getBlobsDir())`, so blob files live in a shared global blob directory (not in a session folder).\n\nBlob file naming:\n\n- file path: `<blobsDir>/<sha256-hex>`\n- no extension\n- reference string stored in entries: `blob:sha256:<sha256-hex>`\n\nImplications:\n\n- same binary content across sessions resolves to the same hash/path,\n- writes are idempotent at the content level,\n- blobs can outlive any individual session file.\n\n## Artifact boundary (session-local)\n\n`ArtifactManager` derives artifact directory from session file path:\n\n- session file: `.../<timestamp>_<sessionId>.jsonl`\n- artifacts directory: `.../<timestamp>_<sessionId>/` (strip `.jsonl`)\n\nArtifact types share this directory:\n\n- truncated tool output files: `<numericId>.<toolType>.log` (for `artifact://`)\n- subagent output files: `<outputId>.md` (for `agent://`)\n\n## ID and name allocation schemes\n\n## Blob IDs: content hash\n\n`BlobStore.put()` computes SHA-256 over raw binary bytes and returns:\n\n- `hash`: hex digest,\n- `path`: `<blobsDir>/<hash>`,\n- `ref`: `blob:sha256:<hash>`.\n\nNo session-local counter is used.\n\n## Artifact IDs: session-local monotonic integer\n\n`ArtifactManager` scans existing `*.log` artifact files on first use to find max existing numeric ID and sets `nextId = max + 1`.\n\nAllocation behavior:\n\n- file format: `{id}.{toolType}.log`\n- IDs are sequential strings (`\"0\"`, `\"1\"`, ...)\n- resume does not overwrite existing artifacts because scan happens before allocation.\n\nIf artifact directory is missing, scanning yields empty list and allocation starts from `0`.\n\n## Agent output IDs (`agent://`)\n\n`AgentOutputManager` allocates IDs for subagent outputs as `<index>-<requestedId>` (optionally nested under parent prefix, e.g. `0-Parent.1-Child`). It scans existing `.md` files on initialization to continue from the next index on resume.\n\n## Persistence dataflow\n\n## 1) Session entry persistence rewrite path\n\nBefore session entries are written (`#rewriteFile` / incremental persist), `SessionManager` calls `prepareEntryForPersistence()` (via `truncateForPersistence`).\n\nKey behaviors:\n\n1. **Large string truncation**: oversized strings are cut and suffixed with `\"[Session persistence truncated large content]\"`.\n2. **Transient field stripping**: `partialJson` and `jsonlEvents` are removed from persisted entries.\n3. **Image externalization to blobs**:\n - only applies to image blocks in `content` arrays,\n - only when `data` is not already a blob ref,\n - only when base64 length is at least threshold (`BLOB_EXTERNALIZE_THRESHOLD = 1024`),\n - replaces inline base64 with `blob:sha256:<hash>`.\n\nThis keeps session JSONL compact while preserving recoverability.\n\n## 2) Session load rehydration path\n\nWhen opening a session (`setSessionFile`), after migrations, `SessionManager` runs `resolveBlobRefsInEntries()`.\n\nFor each message/custom-message image block with `blob:sha256:<hash>`:\n\n- reads blob bytes from blob store,\n- converts bytes back to base64,\n- mutates in-memory entry to inline base64 for runtime consumers.\n\nIf blob is missing:\n\n- `resolveImageData()` logs warning,\n- returns original ref string unchanged,\n- load continues (no hard crash).\n\n## 3) Tool output spill/truncation path\n\n`OutputSink` powers streaming output in bash/python/ssh and related executors.\n\nBehavior:\n\n1. Every chunk is sanitized and appended to in-memory tail buffer.\n2. When in-memory bytes exceed spill threshold (`DEFAULT_MAX_BYTES`, 50KB), sink marks output truncated.\n3. If an artifact path is available, sink opens a file writer and writes:\n - existing buffered content once,\n - all subsequent chunks.\n4. In-memory buffer is always trimmed to tail window for display.\n5. `dump()` returns summary including `artifactId` only when file sink was successfully created.\n\nPractical effect:\n\n- UI/tool return shows truncated tail,\n- full output is preserved in artifact file and referenced as `artifact://<id>`.\n\nIf file sink creation fails (I/O error, missing path, etc.), sink silently falls back to in-memory truncation only; full output is not persisted.\n\n## URL access model\n\n## `blob:` references\n\n`blob:sha256:<hash>` is a persistence reference inside session entry payloads, not an internal URL scheme handled by the router. Resolution is done by `SessionManager` during session load.\n\n## `artifact://<id>`\n\nHandled by `ArtifactProtocolHandler`:\n\n- requires active session artifact directory,\n- ID must be numeric,\n- resolves by matching filename prefix `<id>.`,\n- returns raw text (`text/plain`) from the matched `.log` file,\n- when missing, error includes list of available artifact IDs.\n\nMissing directory behavior:\n\n- if artifacts directory does not exist, throws `No artifacts directory found`.\n\n## `agent://<id>`\n\nHandled by `AgentProtocolHandler` over `<artifactsDir>/<id>.md`:\n\n- plain form returns markdown text,\n- `/path` or `?q=` forms perform JSON extraction,\n- path and query extraction cannot be combined,\n- if extraction requested, file content must parse as JSON.\n\nMissing directory behavior:\n\n- throws `No artifacts directory found`.\n\nMissing output behavior:\n\n- throws `Not found: <id>` with available IDs from existing `.md` files.\n\nRead tool integration:\n\n- `read` supports offset/limit pagination for non-extraction internal URL reads,\n- rejects `offset/limit` when `agent://` extraction is used.\n\n## Resume, fork, and move semantics\n\n## Resume\n\n- `ArtifactManager` scans existing `{id}.*.log` files on first allocation and continues numbering.\n- `AgentOutputManager` scans existing `.md` output IDs and continues numbering.\n- `SessionManager` rehydrates blob refs to base64 on load.\n\n## Fork\n\n`SessionManager.fork()` creates a new session file with new session ID and `parentSession` link, then returns old/new file paths. Artifact copying is handled by `AgentSession.fork()`:\n\n- attempts recursive copy of old artifact directory to new artifact directory,\n- missing old directory is tolerated,\n- non-ENOENT copy errors are logged as warnings and fork still completes.\n\nID implications after fork:\n\n- if copy succeeded, artifact counters in new session continue after max copied ID,\n- if copy failed/skipped, new session artifact IDs start from `0`.\n\nBlob implications after fork:\n\n- blobs are global and content-addressed, so no blob directory copy is required.\n\n## Move to new cwd\n\n`SessionManager.moveTo()` renames both session file and artifact directory to the new default session directory, with rollback logic if a later step fails. This preserves artifact identity while relocating session scope.\n\n## Failure handling and fallback paths\n\n| Case | Behavior |\n| --- | --- |\n| Blob file missing during rehydration | Warn and keep `blob:sha256:` ref string in-memory |\n| Blob read ENOENT via `BlobStore.get` | Returns `null` |\n| Artifact directory missing (`ArtifactManager.listFiles`) | Returns empty list (allocation can start fresh) |\n| Artifact directory missing (`artifact://` / `agent://`) | Throws explicit `No artifacts directory found` |\n| Artifact ID not found | Throws with available IDs listing |\n| OutputSink artifact writer init fails | Continues with tail-only truncation (no full-output artifact) |\n| No session file (some task paths) | Task tool falls back to temp artifacts directory for subagent outputs |\n\n## Binary blob externalization vs text-output artifacts\n\n- **Blob externalization** is for binary image payloads inside persisted session entry content; it replaces inline base64 in JSONL with stable content refs.\n- **Artifacts** are plain text files for execution output and subagent output; they are addressable by session-local IDs through internal URLs.\n\nThe two systems intersect only indirectly (both reduce session JSONL bloat) but have different identity, lifetime, and retrieval paths.\n\n## Implementation files\n\n- [`src/session/blob-store.ts`](../packages/coding-agent/src/session/blob-store.ts) — blob reference format, hashing, put/get, externalize/resolve helpers.\n- [`src/session/artifacts.ts`](../packages/coding-agent/src/session/artifacts.ts) — session artifact directory model and numeric artifact ID allocation.\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts) — `OutputSink` truncation/spill-to-file behavior and summary metadata.\n- [`src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts) — persistence transforms, blob rehydration on load, session fork/move interactions.\n- [`src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — artifact directory copy during interactive fork.\n- [`src/tools/output-utils.ts`](../packages/coding-agent/src/tools/output-utils.ts) — tool artifact manager bootstrap and per-tool artifact path allocation.\n- [`src/internal-urls/artifact-protocol.ts`](../packages/coding-agent/src/internal-urls/artifact-protocol.ts) — `artifact://` resolver.\n- [`src/internal-urls/agent-protocol.ts`](../packages/coding-agent/src/internal-urls/agent-protocol.ts) — `agent://` resolver + JSON extraction.\n- [`src/sdk.ts`](../packages/coding-agent/src/sdk.ts) — internal URL router wiring and artifacts-dir resolver.\n- [`src/task/output-manager.ts`](../packages/coding-agent/src/task/output-manager.ts) — session-scoped agent output ID allocation for `agent://`.\n- [`src/task/executor.ts`](../packages/coding-agent/src/task/executor.ts) — subagent output artifact writes (`<id>.md`) and temp artifact directory fallback.",
|
|
9
8
|
"compaction.md": "# Compaction and Branch Summaries\n\nCompaction and branch summaries are the two mechanisms that keep long sessions usable without losing prior work context.\n\n- **Compaction** rewrites old history into a summary on the current branch.\n- **Branch summary** captures abandoned branch context during `/tree` navigation.\n\nBoth are persisted as session entries and converted back into user-context messages when rebuilding LLM input.\n\n## Key implementation files\n\n- `src/session/compaction/compaction.ts`\n- `src/session/compaction/branch-summarization.ts`\n- `src/session/compaction/pruning.ts`\n- `src/session/compaction/utils.ts`\n- `src/session/session-manager.ts`\n- `src/session/agent-session.ts`\n- `src/session/messages.ts`\n- `src/extensibility/hooks/types.ts`\n- `src/config/settings-schema.ts`\n\n## Session entry model\n\nCompaction and branch summaries are first-class session entries, not plain assistant/user messages.\n\n- `CompactionEntry`\n - `type: \"compaction\"`\n - `summary`, optional `shortSummary`\n - `firstKeptEntryId` (compaction boundary)\n - `tokensBefore`\n - optional `details`, `preserveData`, `fromExtension`\n- `BranchSummaryEntry`\n - `type: \"branch_summary\"`\n - `fromId`, `summary`\n - optional `details`, `fromExtension`\n\nWhen context is rebuilt (`buildSessionContext`):\n\n1. Latest compaction on the active path is converted to one `compactionSummary` message.\n2. Kept entries from `firstKeptEntryId` to the compaction point are re-included.\n3. Later entries on the path are appended.\n4. `branch_summary` entries are converted to `branchSummary` messages.\n5. `custom_message` entries are converted to `custom` messages.\n\nThose custom roles are then transformed into LLM-facing user messages in `convertToLlm()` using the static templates:\n\n- `prompts/compaction/compaction-summary-context.md`\n- `prompts/compaction/branch-summary-context.md`\n\n## Compaction pipeline\n\n### Triggers\n\nCompaction can run in three ways:\n\n1. **Manual**: `/compact [instructions]` calls `AgentSession.compact(...)`.\n2. **Automatic overflow recovery**: after an assistant error that matches context overflow.\n3. **Automatic threshold compaction**: after a successful turn when context exceeds threshold.\n\n### Compaction shape (visual)\n\n```text\nBefore compaction:\n\n entry: 0 1 2 3 4 5 6 7 8 9\n ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┐\n │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │\n └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┘\n └────────┬───────┘ └──────────────┬──────────────┘\n messagesToSummarize kept messages\n ↑\n firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n entry: 0 1 2 3 4 5 6 7 8 9 10\n ┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬──────┬─────┐\n │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool │ cmp │\n └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴──────┴─────┘\n └──────────┬──────┘ └──────────────────────┬───────────────────┘\n not sent to LLM sent to LLM\n ↑\n starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n ↑ ↑ └─────────────────┬────────────────┘\n prompt from cmp messages from firstKeptEntryId\n```\n\n\n### Overflow-retry vs threshold compaction\n\nThe two automatic paths are intentionally different:\n\n- **Overflow-retry compaction**\n - Trigger: current-model assistant error is detected as context overflow.\n - The failing assistant error message is removed from active agent state before retry.\n - Auto compaction runs with `reason: \"overflow\"` and `willRetry: true`.\n - On success, agent auto-continues (`agent.continue()`) after compaction.\n\n- **Threshold compaction**\n - Trigger: `contextTokens > contextWindow - compaction.reserveTokens`.\n - Runs with `reason: \"threshold\"` and `willRetry: false`.\n - On success, if `compaction.autoContinue !== false`, injects a synthetic prompt:\n - `\"Continue if you have next steps.\"`\n\n### Pre-compaction pruning\n\nBefore compaction checks, tool-result pruning may run (`pruneToolOutputs`).\n\nDefault prune policy:\n\n- Protect newest `40_000` tool-output tokens.\n- Require at least `20_000` total estimated savings.\n- Never prune tool results from `skill` or `read`.\n\nPruned tool results are replaced with:\n\n- `[Output truncated - N tokens]`\n\nIf pruning changes entries, session storage is rewritten and agent message state is refreshed before compaction decisions.\n\n### Boundary and cut-point logic\n\n`prepareCompaction()` only considers entries since the last compaction entry (if any).\n\n1. Find previous compaction index.\n2. Compute `boundaryStart = prevCompactionIndex + 1`.\n3. Adapt `keepRecentTokens` using measured usage ratio when available.\n4. Run `findCutPoint()` over the boundary window.\n\nValid cut points include:\n\n- message entries with roles: `user`, `assistant`, `bashExecution`, `hookMessage`, `branchSummary`, `compactionSummary`\n- `custom_message` entries\n- `branch_summary` entries\n\nHard rule: never cut at `toolResult`.\n\nIf there are non-message metadata entries immediately before the cut point (`model_change`, `thinking_level_change`, labels, etc.), they are pulled into the kept region by moving cut index backward until a message or compaction boundary is hit.\n\n### Split-turn handling\n\nIf cut point is not at a user-turn start, compaction treats it as a split turn.\n\nTurn start detection treats these as user-turn boundaries:\n\n- `message.role === \"user\"`\n- `message.role === \"bashExecution\"`\n- `custom_message` entry\n- `branch_summary` entry\n\nSplit-turn compaction generates two summaries:\n\n1. History summary (`messagesToSummarize`)\n2. Turn-prefix summary (`turnPrefixMessages`)\n\nFinal stored summary is merged as:\n\n```markdown\n<history summary>\n\n---\n\n**Turn Context (split turn):**\n\n<turn prefix summary>\n```\n\n### Summary generation\n\n`compact(...)` builds summaries from serialized conversation text:\n\n1. Convert messages via `convertToLlm()`.\n2. Serialize with `serializeConversation()`.\n3. Wrap in `<conversation>...</conversation>`.\n4. Optionally include `<previous-summary>...</previous-summary>`.\n5. Optionally inject hook context as `<additional-context>` list.\n6. Execute summarization prompt with `SUMMARIZATION_SYSTEM_PROMPT`.\n\nPrompt selection:\n\n- first compaction: `compaction-summary.md`\n- iterative compaction with prior summary: `compaction-update-summary.md`\n- split-turn second pass: `compaction-turn-prefix.md`\n- short UI summary: `compaction-short-summary.md`\n\nRemote summarization mode:\n\n- If `compaction.remoteEndpoint` is set, compaction POSTs:\n - `{ systemPrompt, prompt }`\n- Expects JSON containing at least `{ summary }`.\n\n### File-operation context in summaries\n\nCompaction tracks cumulative file activity using assistant tool calls:\n\n- `read(path)` → read set\n- `write(path)` → modified set\n- `edit(path)` → modified set\n\nCumulative behavior:\n\n- Includes prior compaction details only when prior entry is pi-generated (`fromExtension !== true`).\n- In split turns, includes turn-prefix file ops too.\n- `readFiles` excludes files also modified.\n\nSummary text gets file tags appended via prompt template:\n\n```xml\n<read-files>\n...\n</read-files>\n<modified-files>\n...\n</modified-files>\n```\n\n### Persist and reload\n\nAfter summary generation (or hook-provided summary), agent session:\n\n1. Appends `CompactionEntry` with `appendCompaction(...)`.\n2. Rebuilds context via `buildSessionContext()`.\n3. Replaces live agent messages with rebuilt context.\n4. Emits `session_compact` hook event.\n\n## Branch summarization pipeline\n\nBranch summarization is tied to tree navigation, not token overflow.\n\n### Trigger\n\nDuring `navigateTree(...)`:\n\n1. Compute abandoned entries from old leaf to common ancestor using `collectEntriesForBranchSummary(...)`.\n2. If caller requested summary (`options.summarize`), generate summary before switching leaf.\n3. If summary exists, attach it at the navigation target using `branchWithSummary(...)`.\n\nOperationally this is commonly driven by `/tree` flow when `branchSummary.enabled` is enabled.\n\n### Branch switch shape (visual)\n\n```text\nTree before navigation:\n\n ┌─ B ─ C ─ D (old leaf, being abandoned)\n A ───┤\n └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n ┌─ B ─ C ─ D ─ [summary of B,C,D]\n A ───┤\n └─ E ─ F (new leaf)\n```\n\n\n### Preparation and token budget\n\n`generateBranchSummary(...)` computes budget as:\n\n- `tokenBudget = model.contextWindow - branchSummary.reserveTokens`\n\n`prepareBranchEntries(...)` then:\n\n1. First pass: collect cumulative file ops from all summarized entries, including prior pi-generated `branch_summary` details.\n2. Second pass: walk newest → oldest, adding messages until token budget is reached.\n3. Prefer preserving recent context.\n4. May still include large summary entries near budget edge for continuity.\n\nCompaction entries are included as messages (`compactionSummary`) during branch summarization input.\n\n### Summary generation and persistence\n\nBranch summarization:\n\n1. Converts and serializes selected messages.\n2. Wraps in `<conversation>`.\n3. Uses custom instructions if supplied, otherwise `branch-summary.md`.\n4. Calls summarization model with `SUMMARIZATION_SYSTEM_PROMPT`.\n5. Prepends `branch-summary-preamble.md`.\n6. Appends file-operation tags.\n\nResult is stored as `BranchSummaryEntry` with optional details (`readFiles`, `modifiedFiles`).\n\n## Extension and hook touchpoints\n\n### `session_before_compact`\n\nPre-compaction hook.\n\nCan:\n\n- cancel compaction (`{ cancel: true }`)\n- provide full custom compaction payload (`{ compaction: CompactionResult }`)\n\n### `session.compacting`\n\nPrompt/context customization hook for default compaction.\n\nCan return:\n\n- `prompt` (override base summary prompt)\n- `context` (extra context lines injected into `<additional-context>`)\n- `preserveData` (stored on compaction entry)\n\n### `session_compact`\n\nPost-compaction notification with saved `compactionEntry` and `fromExtension` flag.\n\n### `session_before_tree`\n\nRuns on tree navigation before default branch summary generation.\n\nCan:\n\n- cancel navigation\n- provide custom `{ summary: { summary, details } }` used when user requested summarization\n\n### `session_tree`\n\nPost-navigation event exposing new/old leaf and optional summary entry.\n\n## Runtime behavior and failure semantics\n\n- Manual compaction aborts current agent operation first.\n- `abortCompaction()` cancels both manual and auto-compaction controllers.\n- Auto compaction emits start/end session events for UI/state updates.\n- Auto compaction can try multiple model candidates and retry transient failures.\n- Overflow errors are excluded from generic retry path because they are handled by compaction.\n- If auto-compaction fails:\n - overflow path emits `Context overflow recovery failed: ...`\n - threshold path emits `Auto-compaction failed: ...`\n- Branch summarization can be cancelled via abort signal (e.g., Escape), returning canceled/aborted navigation result.\n\n## Settings and defaults\n\nFrom `settings-schema.ts`:\n\n- `compaction.enabled` = `true`\n- `compaction.reserveTokens` = `16384`\n- `compaction.keepRecentTokens` = `20000`\n- `compaction.autoContinue` = `true`\n- `compaction.remoteEndpoint` = `undefined`\n- `branchSummary.enabled` = `false`\n- `branchSummary.reserveTokens` = `16384`\n\nThese values are consumed at runtime by `AgentSession` and compaction/branch summarization modules.",
|