docs: update docs
This commit is contained in:
+14
-12
@@ -28,8 +28,9 @@ All exports live under `@oh-my-pi/pi-ai/utils/schema`:
|
|||||||
OpenAI strict-mode pipeline (sanitize → enforce). All three are exported
|
OpenAI strict-mode pipeline (sanitize → enforce). All three are exported
|
||||||
from `normalize.ts`.
|
from `normalize.ts`.
|
||||||
- `adaptSchemaForStrict(schema, strict)` from `./adapt` — thin composer that
|
- `adaptSchemaForStrict(schema, strict)` from `./adapt` — thin composer that
|
||||||
wraps `tryEnforceStrictSchema` for provider call sites and consults
|
upgrades draft-07 inputs to 2020-12 and wraps `tryEnforceStrictSchema` for
|
||||||
`PI_NO_STRICT` (env `PI_NO_STRICT`) for the global bypass.
|
provider call sites. `./adapt` also exports the `NO_STRICT` global-bypass
|
||||||
|
flag (env `PI_NO_STRICT`) honored by every provider that emits `strict: true`.
|
||||||
|
|
||||||
Removed in the unified-flow refactor:
|
Removed in the unified-flow refactor:
|
||||||
|
|
||||||
@@ -135,15 +136,16 @@ so callers MUST emit `strict: true` only when enforcement actually succeeded.
|
|||||||
|
|
||||||
## Performance: static fingerprint cache
|
## Performance: static fingerprint cache
|
||||||
|
|
||||||
`resolveProviderModels` in `packages/ai/src/model-manager.ts` and
|
`resolveProviderModels` in `packages/catalog/src/model-manager.ts` and
|
||||||
`readModelCache`/`writeModelCache` in `model-cache.ts` cooperate via a
|
`readModelCache`/`writeModelCache` in `packages/catalog/src/model-cache.ts`
|
||||||
schema-v3 `static_fingerprint` column on the `model_cache` SQLite table.
|
cooperate via a `static_fingerprint` column on the `model_cache` SQLite
|
||||||
|
table (current cache schema version 5).
|
||||||
|
|
||||||
- `fingerprintStatic(staticModels)` hashes the static catalog slice
|
- `fingerprintStatic(staticModels)` hashes the static catalog slice
|
||||||
(`Bun.hash(JSON.stringify(models))` in base36) and memoizes the result
|
(`Bun.hash(JSON.stringify(models))` in base36) and memoizes the result
|
||||||
in a per-process `WeakMap` keyed by the array reference. Multiple
|
by tagging the array with a symbol property. Multiple cold-start arms
|
||||||
cold-start arms calling `resolveProviderModels` with the same
|
calling `resolveProviderModels` with the same `staticModels` array pay
|
||||||
`staticModels` array pay the JSON+hash cost once.
|
the JSON+hash cost once.
|
||||||
- On cache read, if the network fetch is being skipped, the cached row is
|
- On cache read, if the network fetch is being skipped, the cached row is
|
||||||
fresh + authoritative, and the cached `static_fingerprint` matches the
|
fresh + authoritative, and the cached `static_fingerprint` matches the
|
||||||
current one, `resolveProviderModels` returns the cached models verbatim
|
current one, `resolveProviderModels` returns the cached models verbatim
|
||||||
@@ -153,10 +155,10 @@ schema-v3 `static_fingerprint` column on the `model_cache` SQLite table.
|
|||||||
empty-source inputs (the common shape after `(static, [])` or for
|
empty-source inputs (the common shape after `(static, [])` or for
|
||||||
providers without a static catalog), avoiding Map churn entirely.
|
providers without a static catalog), avoiding Map churn entirely.
|
||||||
|
|
||||||
Cache rows written before schema v3 are dropped by the cache-version
|
Cache rows written before the current schema version are dropped by the
|
||||||
check; the column defaults to `''` for any row that survives a version
|
cache-version check; the column defaults to `''` for any row that survives
|
||||||
upgrade so the fingerprint-equality check naturally fails closed and the
|
a version upgrade so the fingerprint-equality check naturally fails closed
|
||||||
full merge re-runs.
|
and the full merge re-runs.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ That means print mode and non-UI RPC/tool contexts always use non-PTY.
|
|||||||
- configured command prefix,
|
- configured command prefix,
|
||||||
- snapshot path,
|
- snapshot path,
|
||||||
- serialized shell env,
|
- serialized shell env,
|
||||||
- optional agent session key.
|
- optional agent session key,
|
||||||
|
- minimizer configuration.
|
||||||
|
|
||||||
Session-level bang-command executions pass `sessionKey: this.sessionId`.
|
Session-level bang-command executions pass `sessionKey: this.sessionId`.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ They are intentionally separate:
|
|||||||
Blob file naming:
|
Blob file naming:
|
||||||
|
|
||||||
- file path: `<blobsDir>/<sha256-hex>`
|
- file path: `<blobsDir>/<sha256-hex>`
|
||||||
- no extension
|
- canonical file has no extension; when an extension is supplied (image MIME type), a typed sidecar `<sha256-hex>.<ext>` is hardlinked (or copied) next to it so OS openers can type-detect
|
||||||
- reference string stored in entries: `blob:sha256:<sha256-hex>`
|
- reference string stored in entries: `blob:sha256:<sha256-hex>`
|
||||||
|
|
||||||
Implications:
|
Implications:
|
||||||
@@ -55,6 +55,7 @@ Subagents can adopt the parent `ArtifactManager`; in that case parent and subage
|
|||||||
|
|
||||||
- `hash`: hex digest,
|
- `hash`: hex digest,
|
||||||
- `path`: `<blobsDir>/<hash>`,
|
- `path`: `<blobsDir>/<hash>`,
|
||||||
|
- `displayPath`: `<blobsDir>/<hash>.<ext>` when an extension was supplied, otherwise the canonical path,
|
||||||
- `ref`: `blob:sha256:<hash>`.
|
- `ref`: `blob:sha256:<hash>`.
|
||||||
|
|
||||||
No session-local counter is used.
|
No session-local counter is used.
|
||||||
|
|||||||
+8
-4
@@ -15,13 +15,13 @@ prints
|
|||||||
```
|
```
|
||||||
Collab session started!
|
Collab session started!
|
||||||
• Join from another terminal: omp join "mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30"
|
• Join from another terminal: omp join "mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30"
|
||||||
• or any web browser: relay.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30
|
• or any web browser: my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJUbTovD94ofDaa8YvhY0-ty16w4fn8PgB6PLnoA30
|
||||||
```
|
```
|
||||||
|
|
||||||
The browser line is click-to-join (an OSC 8 hyperlink to the full `https://` deep link): the relay serves the web guest client at `/`, and the room id + key ride in the URL fragment. From another omp (any directory, any machine), either form works:
|
The browser line is click-to-join (an OSC 8 hyperlink to the full `https://` deep link): the relay serves the web guest client at `/`, and the room id + key ride in the URL fragment. From another omp (any directory, any machine), either form works:
|
||||||
|
|
||||||
```
|
```
|
||||||
/join relay.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJU…
|
/join my.omp.sh/#mgAYTZwEnpRQtca0CTgn-Q#gdJU…
|
||||||
```
|
```
|
||||||
|
|
||||||
The guest's previous session is restored on `/leave` (or when the host stops).
|
The guest's previous session is restored on `/leave` (or when the host stops).
|
||||||
@@ -32,6 +32,7 @@ The guest's previous session is restored on `/leave` (or when the host stops).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `/collab` | Start sharing (or re-print the link when already hosting) |
|
| `/collab` | Start sharing (or re-print the link when already hosting) |
|
||||||
| `/collab <relay>` | Start sharing through a specific relay (`relay.example.com`, `ws://localhost:7475`) |
|
| `/collab <relay>` | Start sharing through a specific relay (`relay.example.com`, `ws://localhost:7475`) |
|
||||||
|
| `/collab view` | Print a read-only (view-only) link (starts sharing first if needed) |
|
||||||
| `/collab status` | Show link + participants |
|
| `/collab status` | Show link + participants |
|
||||||
| `/collab stop` | Stop sharing |
|
| `/collab stop` | Stop sharing |
|
||||||
| `/join <link>` | Join a shared session as a guest |
|
| `/join <link>` | Join a shared session as a guest |
|
||||||
@@ -41,7 +42,7 @@ The guest's previous session is restored on `/leave` (or when the host stops).
|
|||||||
|
|
||||||
```
|
```
|
||||||
https://host[:port]/#<link> → browser deep link (printed by /collab; /join accepts it too)
|
https://host[:port]/#<link> → browser deep link (printed by /collab; /join accepts it too)
|
||||||
<roomId>#<key> → default relay (relay.omp.sh)
|
<roomId>#<key> → default relay (my.omp.sh)
|
||||||
host[:port]/r/<roomId>#<key> → custom relay, wss:// inferred
|
host[:port]/r/<roomId>#<key> → custom relay, wss:// inferred
|
||||||
ws://localhost:7475/r/<roomId>#<key> → plain ws, allowed for localhost only
|
ws://localhost:7475/r/<roomId>#<key> → plain ws, allowed for localhost only
|
||||||
```
|
```
|
||||||
@@ -88,8 +89,10 @@ Known v1 limit for guests: a turn already streaming when you join becomes visibl
|
|||||||
|
|
||||||
| Setting | Default | Meaning |
|
| Setting | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `collab.relayUrl` | `wss://relay.omp.sh` | Relay used by `/collab` when no relay is passed inline |
|
| `collab.relayUrl` | `wss://my.omp.sh` | Relay used by `/collab` when no relay is passed inline |
|
||||||
| `collab.displayName` | OS username | Name shown to other participants |
|
| `collab.displayName` | OS username | Name shown to other participants |
|
||||||
|
| `share.serverUrl` | `https://my.omp.sh/s` | Share viewer/upload base used by `/share` (same Go service; links are `<base>/<id>#<key>`) |
|
||||||
|
| `share.redactSecrets` | `true` | Run the secret obfuscator over `/share` snapshots before upload |
|
||||||
|
|
||||||
## Self-hosting the relay
|
## Self-hosting the relay
|
||||||
|
|
||||||
@@ -97,6 +100,7 @@ The relay is a small content-blind Go service (`omp-collab-relay`, in the pi-www
|
|||||||
|
|
||||||
- `GET /` — the static collab-web guest client (target of the `/collab` deep link),
|
- `GET /` — the static collab-web guest client (target of the `/collab` deep link),
|
||||||
- `GET /r/<roomId>?role=host|guest` — WebSocket upgrade,
|
- `GET /r/<roomId>?role=host|guest` — WebSocket upgrade,
|
||||||
|
- `POST /s` / `GET /s/<id>` / `GET /s/<id>/raw` — `/share` blob upload, viewer page, and blob fetch (see the relay README),
|
||||||
- `GET /healthz` — liveness.
|
- `GET /healthz` — liveness.
|
||||||
|
|
||||||
Run it:
|
Run it:
|
||||||
|
|||||||
+4
-3
@@ -44,11 +44,12 @@ When context is rebuilt (`buildSessionContext`):
|
|||||||
4. `branch_summary` entries are converted to `branchSummary` messages.
|
4. `branch_summary` entries are converted to `branchSummary` messages.
|
||||||
5. `custom_message` entries are converted to `custom` messages.
|
5. `custom_message` entries are converted to `custom` messages.
|
||||||
|
|
||||||
Those custom roles are then transformed into LLM-facing user messages in `convertToLlm()` using the static templates:
|
Those custom roles are then transformed into LLM-facing messages in `convertToLlm()`: `compactionSummary` and `branchSummary` become user messages rendered through the static templates
|
||||||
|
|
||||||
- `packages/agent/src/compaction/prompts/compaction-summary-context.md`
|
- `packages/agent/src/compaction/prompts/compaction-summary-context.md`
|
||||||
- `packages/agent/src/compaction/prompts/branch-summary-context.md`
|
- `packages/agent/src/compaction/prompts/branch-summary-context.md`
|
||||||
- `packages/agent/src/compaction/prompts/handoff-document.md`
|
|
||||||
|
while `custom` messages pass through as developer messages with their raw content (no template).
|
||||||
|
|
||||||
## Compaction pipeline
|
## Compaction pipeline
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ Default prune policy:
|
|||||||
|
|
||||||
- Protect newest `40_000` tool-output tokens.
|
- Protect newest `40_000` tool-output tokens.
|
||||||
- Require at least `20_000` total estimated savings.
|
- Require at least `20_000` total estimated savings.
|
||||||
- Never prune tool results from `skill` or `read`.
|
- Never prune `skill` tool results, `read` results of `skill://` paths, or reads of the active plan reference file (added via `AgentSession`'s plan protection).
|
||||||
|
|
||||||
Pruned tool results are replaced with:
|
Pruned tool results are replaced with:
|
||||||
|
|
||||||
|
|||||||
@@ -144,12 +144,11 @@ When `CLAUDE_CODE_USE_FOUNDRY` is enabled, Anthropic requests switch to Foundry
|
|||||||
| `AWS_PROFILE` | Enables named profile auth path |
|
| `AWS_PROFILE` | Enables named profile auth path |
|
||||||
| `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` | Enables IAM key auth path |
|
| `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` | Enables IAM key auth path |
|
||||||
| `AWS_BEARER_TOKEN_BEDROCK` | Highest-precedence bearer token auth path; skips AWS profile/credential-chain lookup when set |
|
| `AWS_BEARER_TOKEN_BEDROCK` | Highest-precedence bearer token auth path; skips AWS profile/credential-chain lookup when set |
|
||||||
| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` / `AWS_CONTAINER_CREDENTIALS_FULL_URI` | Enables ECS task credential path |
|
| `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` / `AWS_CONTAINER_CREDENTIALS_FULL_URI` | Marks Bedrock as available in provider detection (credential resolution itself covers env keys, profiles/SSO/`credential_process`, then IMDSv2) |
|
||||||
| `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Enables web identity auth path |
|
| `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` | Marks Bedrock as available in provider detection (same caveat as the ECS variables above) |
|
||||||
| `AWS_BEDROCK_SKIP_AUTH` | If `1`, injects dummy credentials (proxy/non-auth scenarios) |
|
| `AWS_BEDROCK_SKIP_AUTH` | If `1`, injects dummy credentials (proxy/non-auth scenarios) |
|
||||||
| `AWS_BEDROCK_FORCE_HTTP1` | If `1`, forces Node HTTP/1 request handler |
|
| `HTTPS_PROXY` / `HTTP_PROXY` | Honored via Bun's native fetch proxy support (the provider no longer ships an AWS SDK / proxy-agent transport) |
|
||||||
| `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` | Routes Bedrock runtime and AWS SSO credential calls through the configured proxy using HTTP/1 |
|
| `NO_PROXY` | Excludes matching hosts from Bun's native proxy routing |
|
||||||
| `NO_PROXY` | Excludes matching hosts from proxy routing when a proxy variable is configured |
|
|
||||||
|
|
||||||
Region fallback in provider code: `options.region` → `AWS_REGION` → `AWS_DEFAULT_REGION` → `us-east-1`.
|
Region fallback in provider code: `options.region` → `AWS_REGION` → `AWS_DEFAULT_REGION` → `us-east-1`.
|
||||||
|
|
||||||
@@ -202,7 +201,6 @@ OAuth host chain: `KIMI_CODE_OAUTH_HOST` → `KIMI_OAUTH_HOST` → `https://auth
|
|||||||
| `PI_CODEX_DEBUG` | `1`/`true` enables Codex provider debug logging |
|
| `PI_CODEX_DEBUG` | `1`/`true` enables Codex provider debug logging |
|
||||||
| `PI_CODEX_WEBSOCKET` | `1`/`true` enables websocket transport preference |
|
| `PI_CODEX_WEBSOCKET` | `1`/`true` enables websocket transport preference |
|
||||||
| `PI_OPENAI_STATEFUL` | Overrides the stateful-chaining default for the platform OpenAI Responses API (`previous_response_id`, forces `store: true`): on by default against api.openai.com, off elsewhere |
|
| `PI_OPENAI_STATEFUL` | Overrides the stateful-chaining default for the platform OpenAI Responses API (`previous_response_id`, forces `store: true`): on by default against api.openai.com, off elsewhere |
|
||||||
| `PI_CODEX_WEBSOCKET_V2` | `1`/`true` enables websocket v2 path |
|
|
||||||
| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) |
|
| `PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS` | Positive integer override (default 300000) |
|
||||||
| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) |
|
| `PI_CODEX_WEBSOCKET_RETRY_BUDGET` | Non-negative integer override (default 5) |
|
||||||
| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) |
|
| `PI_CODEX_WEBSOCKET_RETRY_DELAY_MS` | Positive integer base backoff override (default 500) |
|
||||||
@@ -285,7 +283,8 @@ Use `ANTHROPIC_SEARCH_BASE_URL` (optionally with `ANTHROPIC_SEARCH_API_KEY`) to
|
|||||||
|
|
||||||
| Variable | Default / behavior |
|
| Variable | Default / behavior |
|
||||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `PI_PY` | Eval backend override: `0`/`bash`=JavaScript only, `1`/`py`=Python only, `mix`/`both`=both; invalid values ignored |
|
| `PI_PY` | Boolean-like override for the Python eval backend: truthy (`1`/`true`/`yes`/`on`) enables, any other value disables; unset defers to the `eval.py` setting (default enabled) |
|
||||||
|
| `PI_JS` | Same boolean-like override for the JavaScript eval backend; unset defers to the `eval.js` setting (default enabled) |
|
||||||
| `PI_PYTHON_SKIP_CHECK` | If `1`, skips Python interpreter availability checks (subprocess runner still starts on demand) |
|
| `PI_PYTHON_SKIP_CHECK` | If `1`, skips Python interpreter availability checks (subprocess runner still starts on demand) |
|
||||||
| `PI_PYTHON_INTEGRATION` | If `1`, opts gated integration tests in (e.g. `python-runner.integration.test.ts`) into running against real Python |
|
| `PI_PYTHON_INTEGRATION` | If `1`, opts gated integration tests in (e.g. `python-runner.integration.test.ts`) into running against real Python |
|
||||||
| `PI_PYTHON_IPC_TRACE` | If `1`, logs NDJSON frames exchanged with the Python runner subprocess |
|
| `PI_PYTHON_IPC_TRACE` | If `1`, logs NDJSON frames exchanged with the Python runner subprocess |
|
||||||
@@ -378,7 +377,6 @@ These are read as runtime signals; they are usually set by the terminal/OS rathe
|
|||||||
| `COLORTERM`, `TERM`, `WT_SESSION` | Color capability detection (theme color mode) |
|
| `COLORTERM`, `TERM`, `WT_SESSION` | Color capability detection (theme color mode) |
|
||||||
| `COLORFGBG` | Terminal background light/dark auto-detection |
|
| `COLORFGBG` | Terminal background light/dark auto-detection |
|
||||||
| `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `TERMINAL_EMULATOR` | Terminal identity in system prompt/context |
|
| `TERM_PROGRAM`, `TERM_PROGRAM_VERSION`, `TERMINAL_EMULATOR` | Terminal identity in system prompt/context |
|
||||||
| `KDE_FULL_SESSION`, `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`, `XDG_SESSION_DESKTOP`, `GDMSESSION`, `WINDOWMANAGER` | Desktop/window-manager detection in system prompt/context |
|
|
||||||
| `TMUX_PANE`, `CMUX_SURFACE_ID`, `KITTY_WINDOW_ID`, `TERM_SESSION_ID`, `WT_SESSION` | Stable per-terminal session breadcrumb IDs |
|
| `TMUX_PANE`, `CMUX_SURFACE_ID`, `KITTY_WINDOW_ID`, `TERM_SESSION_ID`, `WT_SESSION` | Stable per-terminal session breadcrumb IDs |
|
||||||
| `SHELL`, `ComSpec`, `TERM_PROGRAM`, `TERM` | System info diagnostics |
|
| `SHELL`, `ComSpec`, `TERM_PROGRAM`, `TERM` | System info diagnostics |
|
||||||
| `APPDATA`, `XDG_CONFIG_HOME` | lspmux config path resolution |
|
| `APPDATA`, `XDG_CONFIG_HOME` | lspmux config path resolution |
|
||||||
@@ -393,7 +391,7 @@ These are read as runtime signals; they are usually set by the terminal/OS rathe
|
|||||||
| `PI_NOTIFICATIONS` | `off` / `0` / `false` suppress desktop notifications |
|
| `PI_NOTIFICATIONS` | `off` / `0` / `false` suppress desktop notifications |
|
||||||
| `PI_TUI_WRITE_LOG` | If set, logs TUI writes to file |
|
| `PI_TUI_WRITE_LOG` | If set, logs TUI writes to file |
|
||||||
| `PI_HARDWARE_CURSOR` | If `1`, enables hardware cursor mode |
|
| `PI_HARDWARE_CURSOR` | If `1`, enables hardware cursor mode |
|
||||||
| `PI_NO_SYNC_OUTPUT` | If `1`, disables DEC 2026 synchronized-output wrappers while keeping TUI autowrap guards |
|
| `PI_NO_SYNC_OUTPUT` | If set (any non-empty value), disables DEC 2026 synchronized-output wrappers while keeping TUI autowrap guards |
|
||||||
| `PI_NO_DECCARA` | If set (truthy), disables Kitty DECCARA rectangular-SGR background fills (forces padded-string rendering) |
|
| `PI_NO_DECCARA` | If set (truthy), disables Kitty DECCARA rectangular-SGR background fills (forces padded-string rendering) |
|
||||||
| `PI_DEBUG_REDRAW` | If `1`, enables redraw debug logging |
|
| `PI_DEBUG_REDRAW` | If `1`, enables redraw debug logging |
|
||||||
| `PI_FORCE_IMAGE_PROTOCOL` | Forces terminal image protocol detection (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) |
|
| `PI_FORCE_IMAGE_PROTOCOL` | Forces terminal image protocol detection (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) |
|
||||||
|
|||||||
+6
-7
@@ -138,7 +138,7 @@ Also exposed:
|
|||||||
- `deliverAs: "steer"` (default) — interrupts current run
|
- `deliverAs: "steer"` (default) — interrupts current run
|
||||||
- `deliverAs: "followUp"` — queued to run after current run
|
- `deliverAs: "followUp"` — queued to run after current run
|
||||||
- `deliverAs: "nextTurn"` — stored and injected on the next user prompt
|
- `deliverAs: "nextTurn"` — stored and injected on the next user prompt
|
||||||
- `triggerTurn: true` — starts a turn when idle (`nextTurn` ignores this)
|
- `triggerTurn: true` — starts a turn when idle (also honored with `deliverAs: "nextTurn"`: idle prompts immediately; while streaming the queued message schedules an internal continuation)
|
||||||
|
|
||||||
`pi.sendUserMessage(content, { deliverAs })` always goes through prompt flow; while streaming it queues as steer/follow-up.
|
`pi.sendUserMessage(content, { deliverAs })` always goes through prompt flow; while streaming it queues as steer/follow-up.
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ pi.registerTool({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
`tool_call`/`tool_result` intercept all tools once the registry is wrapped in `sdk.ts`, including built-ins and extension/custom tools. `ToolDefinition` also supports optional `hidden`, `defaultInactive`, `deferrable`, `mcpServerName`, `mcpToolName`, `renderCall`, and `renderResult` fields.
|
`tool_call`/`tool_result` intercept all tools once the registry is wrapped in `sdk.ts`, including built-ins and extension/custom tools. `ToolDefinition` also supports optional `hidden`, `defaultInactive`, `deferrable`, `approval`, `mcpServerName`, `mcpToolName`, `renderCall`, and `renderResult` fields.
|
||||||
|
|
||||||
## UI integration points
|
## UI integration points
|
||||||
|
|
||||||
@@ -297,16 +297,15 @@ Current no-op methods in this controller:
|
|||||||
|
|
||||||
- `setFooter`
|
- `setFooter`
|
||||||
- `setHeader`
|
- `setHeader`
|
||||||
- `setEditorComponent`
|
|
||||||
|
|
||||||
Also note: `setWidget` currently routes to status-line text via `setHookWidget(...)`.
|
`setEditorComponent` is wired to the live editor (`ctx.setEditorComponent(factory)`). `setWidget` renders real widget components above or below the editor via `setHookWidget(...)` (`placement: "aboveEditor" | "belowEditor"`; string-array content capped at 10 lines).
|
||||||
|
|
||||||
### RPC mode (`rpc-mode.ts`)
|
### RPC mode (`rpc-mode.ts`)
|
||||||
|
|
||||||
`ctx.ui` is backed by RPC `extension_ui_request` events:
|
`ctx.ui` is backed by RPC `extension_ui_request` events:
|
||||||
|
|
||||||
- dialog methods (`select`, `confirm`, `input`, `editor`) round-trip to client responses
|
- dialog methods (`select`, `confirm`, `input`, `editor`) round-trip to client responses
|
||||||
- fire-and-forget methods emit requests (`notify`, `setStatus`, `setWidget` for string arrays, `setTitle`, `setEditorText`)
|
- fire-and-forget methods emit requests (`notify`, `setStatus`, `setWidget` for string arrays, `setEditorText`; `setTitle` emits only when `PI_RPC_EMIT_TITLE=1`)
|
||||||
|
|
||||||
Unsupported/no-op in RPC implementation:
|
Unsupported/no-op in RPC implementation:
|
||||||
|
|
||||||
@@ -321,9 +320,9 @@ Unsupported/no-op in RPC implementation:
|
|||||||
|
|
||||||
When no UI context is supplied to runner init, `ctx.hasUI` is `false` and methods are no-op/default-returning.
|
When no UI context is supplied to runner init, `ctx.hasUI` is `false` and methods are no-op/default-returning.
|
||||||
|
|
||||||
### Background interactive mode
|
### ACP mode
|
||||||
|
|
||||||
Background mode installs a non-interactive UI context object. In current implementation, `ctx.hasUI` may still be `true` while interactive dialogs return defaults/no-op behavior.
|
ACP installs an elicitation-bridged UI context (`createAcpExtensionUiContext` in `acp-agent.ts`). `ctx.hasUI` is `true` while only `select`/`confirm`/`input` round-trip (as ACP elicitations; defaults are returned when the client lacks the `elicitation.form` capability). The non-elicitation surface (widgets, editor, theming, terminal input) is stubbed no-op.
|
||||||
|
|
||||||
## Session and state patterns
|
## Session and state patterns
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ Current defaults in native APIs:
|
|||||||
- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, `node_modules` is skipped, `follow_links=true`, minimal detail
|
- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, `node_modules` is skipped, `follow_links=true`, minimal detail
|
||||||
- `grep`: `hidden=true`, `gitignore=true`, `cache=false`; cached directory mode skips `node_modules` unless the glob mentions `node_modules`; minimal detail
|
- `grep`: `hidden=true`, `gitignore=true`, `cache=false`; cached directory mode skips `node_modules` unless the glob mentions `node_modules`; minimal detail
|
||||||
|
|
||||||
Coding-agent callers today:
|
Current callers:
|
||||||
|
|
||||||
- High-volume mention candidate discovery enables cache:
|
- `@`-mention fuzzy file autocomplete enables cache (`fuzzyFind` with `cache: true`):
|
||||||
- `packages/coding-agent/src/utils/file-mentions.ts`
|
- `packages/tui/src/autocomplete.ts`
|
||||||
- Mutation flows invalidate through `packages/coding-agent/src/tools/fs-cache-invalidation.ts`.
|
- Mutation flows invalidate through `packages/coding-agent/src/tools/fs-cache-invalidation.ts`.
|
||||||
- Tool-level search integration (`packages/coding-agent/src/tools/search.ts`) currently calls native `grep` with `cache: false`.
|
- Tool-level search integration (`packages/coding-agent/src/tools/search.ts`) currently calls native `grep` with `cache: false`.
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ Does not cover:
|
|||||||
- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)
|
- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)
|
||||||
- [`packages/agent/src/compaction/compaction.ts`](../packages/agent/src/compaction/compaction.ts)
|
- [`packages/agent/src/compaction/compaction.ts`](../packages/agent/src/compaction/compaction.ts)
|
||||||
- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)
|
- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)
|
||||||
- [`../src/extensibility/slash-commands.ts`](../packages/coding-agent/src/extensibility/slash-commands.ts)
|
- [`../src/slash-commands/builtin-registry.ts`](../packages/coding-agent/src/slash-commands/builtin-registry.ts)
|
||||||
|
|
||||||
## Trigger path
|
## Trigger path
|
||||||
|
|
||||||
1. `/handoff` is declared in builtin slash command metadata (`slash-commands.ts`) with optional inline hint: `[focus instructions]`.
|
1. `/handoff` is declared in builtin slash command metadata (`slash-commands/builtin-registry.ts`) with optional inline hint: `[focus instructions]`.
|
||||||
2. In interactive input handling (`InputController`), submit text matching `/handoff` or `/handoff ...` is intercepted before normal prompt submission.
|
2. In interactive input handling (`InputController`), submit text matching `/handoff` or `/handoff ...` is intercepted before normal prompt submission.
|
||||||
3. The editor is cleared and `handleHandoffCommand(customInstructions?)` is called.
|
3. The editor is cleared and `handleHandoffCommand(customInstructions?)` is called.
|
||||||
4. `CommandController.handleHandoffCommand` performs a preflight guard using current entries:
|
4. `CommandController.handleHandoffCommand` performs a preflight guard using current entries:
|
||||||
@@ -62,10 +62,10 @@ The same minimum-content guard exists again inside `AgentSession.handoff()` and
|
|||||||
|
|
||||||
`generateHandoff(...)` converts the existing `AgentMessage[]` history to real LLM `Message[]` history, then appends one trailing agent-attributed `user` message containing the rendered handoff prompt.
|
`generateHandoff(...)` converts the existing `AgentMessage[]` history to real LLM `Message[]` history, then appends one trailing agent-attributed `user` message containing the rendered handoff prompt.
|
||||||
|
|
||||||
The request uses `completeSimple(...)` directly:
|
The request uses `instrumentedCompleteSimple(...)` (the OTEL-instrumented `completeSimple` oneshot wrapper) directly:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await completeSimple(
|
await instrumentedCompleteSimple(
|
||||||
model,
|
model,
|
||||||
{
|
{
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
@@ -80,6 +80,7 @@ await completeSimple(
|
|||||||
initiatorOverride,
|
initiatorOverride,
|
||||||
metadata,
|
metadata,
|
||||||
},
|
},
|
||||||
|
{ telemetry, oneshotKind: "handoff" },
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ High-level state flow:
|
|||||||
1. Interactive slash command intercepted.
|
1. Interactive slash command intercepted.
|
||||||
2. Preflight message-count guard.
|
2. Preflight message-count guard.
|
||||||
3. `#handoffAbortController` created (`isGeneratingHandoff = true`).
|
3. `#handoffAbortController` created (`isGeneratingHandoff = true`).
|
||||||
4. `generateHandoff(...)` issues one `completeSimple(...)` request with live system prompt, tools, message history, current thinking level, and trailing handoff prompt.
|
4. `generateHandoff(...)` issues one `instrumentedCompleteSimple(...)` request with live system prompt, tools, message history, current thinking level, and trailing handoff prompt.
|
||||||
5. Assistant response text blocks are joined; tool-call blocks are discarded.
|
5. Assistant response text blocks are joined; tool-call blocks are discarded.
|
||||||
6. If missing text → return `undefined`; if aborted → cancellation error path.
|
6. If missing text → return `undefined`; if aborted → cancellation error path.
|
||||||
7. If present:
|
7. If present:
|
||||||
|
|||||||
+1
-1
@@ -234,7 +234,7 @@ Hook status text set via `ctx.ui.setStatus(key, text)` is:
|
|||||||
|
|
||||||
- stored per key
|
- stored per key
|
||||||
- sorted by key name
|
- sorted by key name
|
||||||
- sanitized (`\r`, `\n`, `\t` → spaces; repeated spaces collapsed)
|
- sanitized (ANSI/VT escape sequences stripped; control characters mapped to spaces; repeated spaces collapsed; trimmed)
|
||||||
- joined and width-truncated for display
|
- joined and width-truncated for display
|
||||||
|
|
||||||
## Error propagation and fallback
|
## Error propagation and fallback
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ app.stt.toggle: []
|
|||||||
| Action ID | Default | Meaning |
|
| Action ID | Default | Meaning |
|
||||||
| --------------------------- | -------------------------------------- | --------------------------------------------- |
|
| --------------------------- | -------------------------------------- | --------------------------------------------- |
|
||||||
| `app.model.cycleForward` | `Ctrl+P` | Cycle role models forward |
|
| `app.model.cycleForward` | `Ctrl+P` | Cycle role models forward |
|
||||||
| `app.model.cycleBackward` | `Shift+Ctrl+P` | Cycle role models in temporary mode |
|
| `app.model.cycleBackward` | `Shift+Ctrl+P` | Cycle role models backward |
|
||||||
| `app.model.selectTemporary` | `Alt+P` | Pick a model temporarily for this session |
|
| `app.model.selectTemporary` | `Alt+P` | Pick a model temporarily for this session |
|
||||||
| `app.model.select` | `Alt+M` | Open the model selector and set roles |
|
| `app.model.select` | `Alt+M` | Open the model selector and set roles |
|
||||||
| `app.plan.toggle` | `Alt+Shift+P` | Toggle plan mode |
|
| `app.plan.toggle` | `Alt+Shift+P` | Toggle plan mode |
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ What this means in practice:
|
|||||||
## Required GitHub secrets
|
## Required GitHub secrets
|
||||||
|
|
||||||
Add these under **Settings → Secrets and variables → Actions** (repo secrets).
|
Add these under **Settings → Secrets and variables → Actions** (repo secrets).
|
||||||
Both the cert (`APPLE_CERTIFICATE_P12`) **and** the API key (`APPLE_API_KEY`)
|
All five secrets (cert, password, and API key trio) must be present for
|
||||||
must be present for signing to engage.
|
signing to engage.
|
||||||
|
|
||||||
| Secret | What it is |
|
| Secret | What it is |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ In the TUI, `/marketplace` with no arguments opens the interactive plugin browse
|
|||||||
|
|
||||||
A **marketplace** is a Git repository (or local directory) containing a catalog file at `.claude-plugin/marketplace.json`. The catalog lists available plugins with their sources, descriptions, and metadata.
|
A **marketplace** is a Git repository (or local directory) containing a catalog file at `.claude-plugin/marketplace.json`. The catalog lists available plugins with their sources, descriptions, and metadata.
|
||||||
|
|
||||||
A **plugin** is a directory containing Claude/OMP plugin content such as skills, commands, hooks, tools, MCP servers, LSP servers, rules, prompts, or extension modules. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`).
|
A **plugin** is a directory containing Claude/OMP plugin content such as skills, commands, agents, hooks, tools, MCP servers, or LSP servers. Extension modules (`package.json` `omp.extensions` entry points) are not loaded from marketplace installs — they only load for npm-installed or `omp plugin link`ed plugins. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`).
|
||||||
|
|
||||||
**Scopes**: marketplace plugins can be installed at two scopes:
|
**Scopes**: marketplace plugins can be installed at two scopes:
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -178,7 +178,8 @@ OMP understands two auth-related objects.
|
|||||||
"credentialId": "optional-stored-credential-id",
|
"credentialId": "optional-stored-credential-id",
|
||||||
"tokenUrl": "optional-token-endpoint",
|
"tokenUrl": "optional-token-endpoint",
|
||||||
"clientId": "optional-client-id",
|
"clientId": "optional-client-id",
|
||||||
"clientSecret": "optional-client-secret"
|
"clientSecret": "optional-client-secret",
|
||||||
|
"resource": "optional-mcp-resource-uri"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ If SSE stream ends before matching response, request fails with `No response rec
|
|||||||
|
|
||||||
Client emits JSON-RPC notifications via `transport.notify(...)`.
|
Client emits JSON-RPC notifications via `transport.notify(...)`.
|
||||||
|
|
||||||
- Stdio: writes notification frame to stdin (`jsonrpc`, `method`, optional `params`) plus newline.
|
- Stdio: writes notification frame to stdin (`jsonrpc`, `method`, `params`) plus newline via `writeFrame()`; a failed write closes the transport and throws.
|
||||||
- HTTP: sends POST body without `id`; success accepts `2xx` or `202 Accepted`.
|
- HTTP: sends POST body without `id`; success accepts `2xx` or `202 Accepted`.
|
||||||
|
|
||||||
Server-initiated notifications are surfaced through transport `onNotification`; `MCPManager` consumes known MCP list/update notifications and can forward all notifications through its own callback.
|
Server-initiated notifications are surfaced through transport `onNotification`; `MCPManager` consumes known MCP list/update notifications and can forward all notifications through its own callback.
|
||||||
@@ -112,11 +112,9 @@ Server-initiated notifications are surfaced through transport `onNotification`;
|
|||||||
- start stdout read loop (`readJsonl`)
|
- start stdout read loop (`readJsonl`)
|
||||||
- start stderr loop (read/discard; currently silent)
|
- start stderr loop (read/discard; currently silent)
|
||||||
- `close()`:
|
- `close()`:
|
||||||
- mark disconnected
|
- `#handleClose()`: mark disconnected, reject all pending requests (`Transport closed`), emit `onClose`
|
||||||
- reject all pending requests (`Transport closed`)
|
|
||||||
- kill subprocess
|
- kill subprocess
|
||||||
- await read loop shutdown
|
- await read loop shutdown
|
||||||
- emit `onClose`
|
|
||||||
|
|
||||||
If read loop exits unexpectedly, `finally` triggers `#handleClose()` which performs the same pending-request rejection and close callback.
|
If read loop exits unexpectedly, `finally` triggers `#handleClose()` which performs the same pending-request rejection and close callback.
|
||||||
|
|
||||||
@@ -124,7 +122,7 @@ If read loop exits unexpectedly, `finally` triggers `#handleClose()` which perfo
|
|||||||
|
|
||||||
Per request:
|
Per request:
|
||||||
|
|
||||||
- timeout defaults to `config.timeout ?? 30000`
|
- timeout from `resolveMCPTimeoutMs`: `OMP_MCP_TIMEOUT_MS` env override, else `config.timeout ?? 30000`; `0` disables
|
||||||
- optional `AbortSignal` from caller
|
- optional `AbortSignal` from caller
|
||||||
- abort and timeout both reject the pending promise and clean map entry
|
- abort and timeout both reject the pending promise and clean map entry
|
||||||
|
|
||||||
@@ -150,7 +148,7 @@ When process exits or stream closes:
|
|||||||
|
|
||||||
## Backpressure/streaming notes
|
## Backpressure/streaming notes
|
||||||
|
|
||||||
- Outbound writes use `stdin.write()` + `flush()` without awaiting drain semantics.
|
- `request()` awaits `stdin.write()` + `flush()` so broken-pipe failures reject the request; `notify()` writes through `writeFrame()`, which does not await and neutralizes async EPIPE rejections.
|
||||||
- There is no explicit queue or high-watermark management in transport.
|
- There is no explicit queue or high-watermark management in transport.
|
||||||
- Inbound processing is stream-driven (`for await` over `readJsonl`), one parsed message at a time.
|
- Inbound processing is stream-driven (`for await` over `readJsonl`), one parsed message at a time.
|
||||||
|
|
||||||
@@ -176,13 +174,13 @@ So `connected` means "transport usable", not "persistent stream established".
|
|||||||
|
|
||||||
For `request()`:
|
For `request()`:
|
||||||
|
|
||||||
- timeout uses `AbortController` (`config.timeout ?? 30000`)
|
- timeout uses `AbortController` via `createMCPTimeout` (`OMP_MCP_TIMEOUT_MS` override, else `config.timeout ?? 30000`; `0` disables)
|
||||||
- external signal, if provided, is merged via `AbortSignal.any([...])`
|
- external signal, if provided, is merged via `AbortSignal.any([...])`
|
||||||
- AbortError handling distinguishes caller abort vs timeout
|
- AbortError handling distinguishes caller abort vs timeout
|
||||||
|
|
||||||
For `notify()`:
|
For `notify()`:
|
||||||
|
|
||||||
- timeout uses an internal `AbortController` (`config.timeout ?? 30000`)
|
- timeout uses an internal `AbortController` with the same resolved timeout
|
||||||
- there is no external abort option on the transport interface
|
- there is no external abort option on the transport interface
|
||||||
|
|
||||||
For HTTP OAuth configs managed by `MCPManager`, outbound requests and best-effort server-request responses retry once on `HTTP 401`/`403` if token refresh returns replacement headers.
|
For HTTP OAuth configs managed by `MCPManager`, outbound requests and best-effort server-request responses retry once on `HTTP 401`/`403` if token refresh returns replacement headers.
|
||||||
@@ -230,7 +228,7 @@ SSE JSON parsing errors bubble out of `readSseJson` and reject request/listener.
|
|||||||
Notable differences from `HttpTransport`:
|
Notable differences from `HttpTransport`:
|
||||||
|
|
||||||
- parses entire response text first, then extracts first `data: ` line (`parseSSE`), with JSON fallback
|
- parses entire response text first, then extracts first `data: ` line (`parseSSE`), with JSON fallback
|
||||||
- no request timeout management, no abort API, no session-id handling, no transport lifecycle
|
- optional caller `AbortSignal` (`CallMcpOptions`), with a hard 60s `AbortSignal.timeout` default when none is given; no session-id handling, no transport lifecycle
|
||||||
- returns raw JSON-RPC envelope object
|
- returns raw JSON-RPC envelope object
|
||||||
|
|
||||||
This path is lightweight but less robust than full transport implementation.
|
This path is lightweight but less robust than full transport implementation.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This document describes how MCP servers are discovered, connected, exposed as to
|
|||||||
|
|
||||||
## Lifecycle at a glance
|
## Lifecycle at a glance
|
||||||
|
|
||||||
1. **SDK startup** calls `discoverAndLoadMCPTools()` (unless MCP is disabled).
|
1. **SDK startup** kicks off MCP discovery (unless MCP is disabled): headless/SDK sessions await `discoverAndLoadMCPTools()`; interactive sessions (`hasUI: true`) create the manager up front and defer `discoverAndConnect()` until the session is live.
|
||||||
2. **Discovery** (`loadAllMCPConfigs`) resolves MCP server configs from capability sources, filters disabled/project/Exa entries and browser MCP servers when the built-in browser tool is enabled, and preserves source metadata.
|
2. **Discovery** (`loadAllMCPConfigs`) resolves MCP server configs from capability sources, filters disabled/project/Exa entries and browser MCP servers when the built-in browser tool is enabled, and preserves source metadata.
|
||||||
3. **Manager connect phase** (`MCPManager.connectServers`) starts per-server connect + `tools/list` in parallel.
|
3. **Manager connect phase** (`MCPManager.connectServers`) starts per-server connect + `tools/list` in parallel.
|
||||||
4. **Fast startup gate** waits up to 250ms, then may return:
|
4. **Fast startup gate** waits up to 250ms, then may return:
|
||||||
@@ -20,13 +20,17 @@ This document describes how MCP servers are discovered, connected, exposed as to
|
|||||||
|
|
||||||
### Entry path from SDK
|
### Entry path from SDK
|
||||||
|
|
||||||
`createAgentSession()` in `src/sdk.ts` performs MCP startup when `enableMCP` is true (default):
|
`createAgentSession()` in `src/sdk.ts` performs MCP startup when `enableMCP` is true (default). There are two paths:
|
||||||
|
|
||||||
- calls `discoverAndLoadMCPTools(cwd, { ... })`,
|
- **Headless/SDK** (no UI, no provided manager): awaits `discoverAndLoadMCPTools(cwd, { ... })` and merges the returned tools into the startup `customTools` set.
|
||||||
- passes `authStorage`, cache storage, `mcp.enableProjectConfig`, and browser-MCP filtering based on the `browser.enabled` setting,
|
- **Interactive/TUI** (`hasUI: true`, no provided manager): constructs `MCPManager` immediately (with cache + auth storage), defers `discoverAndConnect()` to a background task started after the session exists, then binds tools via `session.refreshMCPTools(...)` (disposing the manager if the session was torn down mid-connect).
|
||||||
- always sets `filterExa: true`,
|
|
||||||
- logs per-server load/connect errors,
|
Both paths:
|
||||||
- stores returned manager in `toolSession.mcpManager` and session result.
|
|
||||||
|
- pass `authStorage`, cache storage, `mcp.enableProjectConfig`, and browser-MCP filtering based on the `browser.enabled` setting,
|
||||||
|
- always set `filterExa: true`,
|
||||||
|
- log per-server load/connect errors,
|
||||||
|
- store the manager in `toolSession.mcpManager` and the session result.
|
||||||
|
|
||||||
If `enableMCP` is false, MCP discovery is skipped entirely.
|
If `enableMCP` is false, MCP discovery is skipped entirely.
|
||||||
|
|
||||||
@@ -107,9 +111,9 @@ After 250ms:
|
|||||||
- rejected tasks produce per-server errors,
|
- rejected tasks produce per-server errors,
|
||||||
- still-pending tasks:
|
- still-pending tasks:
|
||||||
- use cached tool definitions if available (`MCPToolCache.get`) to create `DeferredMCPTool`s,
|
- use cached tool definitions if available (`MCPToolCache.get`) to create `DeferredMCPTool`s,
|
||||||
- otherwise block until those pending tasks settle.
|
- otherwise contribute no tools at startup; they stay in flight, and the background continuation registers their tools via `#onToolsChanged` once connect/list finishes (a slow server no longer blocks startup — issue #2100).
|
||||||
|
|
||||||
This is a hybrid startup model: fast return when cache is available, correctness wait when cache is not.
|
This is a hybrid startup model: fast return with deferred handles when cache is available, late background registration when it is not.
|
||||||
|
|
||||||
### Background completion behavior
|
### Background completion behavior
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ Current runtime behavior is connection-event driven:
|
|||||||
|
|
||||||
- **No autonomous polling health monitor** in manager/client.
|
- **No autonomous polling health monitor** in manager/client.
|
||||||
- **Automatic reconnect is wired to `transport.onClose`** for managed connections.
|
- **Automatic reconnect is wired to `transport.onClose`** for managed connections.
|
||||||
- Reconnect retries with backoff (`500`, `1000`, `2000`, `4000` ms), reloads tools, and notifies consumers on success.
|
- Reconnect retries with backoff (`500`, `1000`, `2000`, `4000` ms), reloads tools, and notifies consumers on success. A crash-storm circuit breaker suspends automatic reconnects for a server after more than 5 reconnect attempts within 30s; manual `/mcp reconnect` resets that history.
|
||||||
- Tool calls that see retriable connection errors also attempt one reconnect + retry.
|
- Tool calls that see retriable connection errors also attempt one reconnect + retry.
|
||||||
- Reconnect is also explicit via `/mcp reconnect <name>` or broader `/mcp reload`.
|
- Reconnect is also explicit via `/mcp reconnect <name>` or broader `/mcp reload`.
|
||||||
|
|
||||||
@@ -199,7 +203,7 @@ In current wiring, explicit teardown is used in MCP command flows (for reload/re
|
|||||||
| Invalid server config | Server skipped with validation error entry | Best-effort per server |
|
| Invalid server config | Server skipped with validation error entry | Best-effort per server |
|
||||||
| Connect timeout/init failure | Server error recorded; others continue | Best-effort per server |
|
| Connect timeout/init failure | Server error recorded; others continue | Best-effort per server |
|
||||||
| `tools/list` still pending at startup with cache hit | Deferred tools returned immediately | Best-effort fast startup |
|
| `tools/list` still pending at startup with cache hit | Deferred tools returned immediately | Best-effort fast startup |
|
||||||
| `tools/list` still pending at startup without cache | Startup waits for pending to settle | Hard wait for correctness |
|
| `tools/list` still pending at startup without cache | No tools at startup; background continuation registers them via `#onToolsChanged` when ready | Best-effort late registration |
|
||||||
| Late background tool-load failure | Logged after startup gate | Best-effort logging |
|
| Late background tool-load failure | Logged after startup gate | Best-effort logging |
|
||||||
| Runtime dropped transport | Manager attempts reconnect; stale tools remain while reconnecting and future calls may retry once or fail with MCP errors | Best-effort automatic recovery |
|
| Runtime dropped transport | Manager attempts reconnect; stale tools remain while reconnecting and future calls may retry once or fail with MCP errors | Best-effort automatic recovery |
|
||||||
|
|
||||||
|
|||||||
+9
-8
@@ -41,7 +41,7 @@ The agent can read memory files directly using `memory://` URLs with the `read`
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
Local summary memories are built by a background pipeline that runs at startup or when manually triggered via slash command. The pipeline is skipped for subagents and for sessions that are not persisted to a session file.
|
Local summary memories are built by a background pipeline that runs at startup; `/memory enqueue` marks consolidation work that the next startup picks up. The pipeline is skipped for subagents and for sessions that are not persisted to a session file.
|
||||||
|
|
||||||
**Phase 1 — per-session extraction:** For each past session that has changed since it was last processed, a model reads the session history and extracts durable signal: technical decisions, constraints, resolved failures, recurring workflows. Sessions that are too recent, too old, currently active, or beyond the configured scan/age limits are skipped. Each extraction produces a raw memory block and a short synopsis for that session.
|
**Phase 1 — per-session extraction:** For each past session that has changed since it was last processed, a model reads the session history and extracts durable signal: technical decisions, constraints, resolved failures, recurring workflows. Sessions that are too recent, too old, currently active, or beyond the configured scan/age limits are skipped. Each extraction produces a raw memory block and a short synopsis for that session.
|
||||||
|
|
||||||
@@ -59,12 +59,13 @@ Consolidated output is redacted for common secret/token patterns before `MEMORY.
|
|||||||
|
|
||||||
Memory extraction and consolidation behavior is driven by static prompt files in `packages/coding-agent/src/prompts/memories/`.
|
Memory extraction and consolidation behavior is driven by static prompt files in `packages/coding-agent/src/prompts/memories/`.
|
||||||
|
|
||||||
| File | Purpose | Variables |
|
| File | Purpose | Variables |
|
||||||
| --------------------- | ------------------------------------------- | ------------------------------------------- |
|
| ------------------------ | -------------------------------------------- | ------------------------------------------- |
|
||||||
| `stage_one_system.md` | System prompt for per-session extraction | — |
|
| `stage_one_system.md` | System prompt for per-session extraction | — |
|
||||||
| `stage_one_input.md` | User-turn template wrapping session content | `{{thread_id}}`, `{{response_items_json}}` |
|
| `stage_one_input.md` | User-turn template wrapping session content | `{{thread_id}}`, `{{response_items_json}}` |
|
||||||
| `consolidation.md` | Prompt for cross-session consolidation | `{{raw_memories}}`, `{{rollout_summaries}}` |
|
| `consolidation_system.md`| System prompt for cross-session consolidation | — |
|
||||||
| `read_path.md` | Memory guidance injected into live sessions | `{{memory_summary}}` |
|
| `consolidation.md` | User-turn prompt for cross-session consolidation | `{{raw_memories}}`, `{{rollout_summaries}}` |
|
||||||
|
| `read-path.md` | Memory guidance injected into live sessions | `{{memory_summary}}` |
|
||||||
|
|
||||||
### Model selection
|
### Model selection
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ Additional tuning knobs (concurrency, lease durations, token budgets) are availa
|
|||||||
|
|
||||||
## Key files
|
## Key files
|
||||||
|
|
||||||
- `packages/coding-agent/src/memories/index.ts` — pipeline orchestration, injection, slash command handling
|
- `packages/coding-agent/src/memories/index.ts` — pipeline orchestration, injection, clear/enqueue entry points (the `/memory` command routes here via `packages/coding-agent/src/memory-backend/local-backend.ts`)
|
||||||
- `packages/coding-agent/src/memories/storage.ts` — SQLite-backed job queue and thread registry
|
- `packages/coding-agent/src/memories/storage.ts` — SQLite-backed job queue and thread registry
|
||||||
- `packages/coding-agent/src/prompts/memories/` — memory prompt templates
|
- `packages/coding-agent/src/prompts/memories/` — memory prompt templates
|
||||||
- `packages/coding-agent/src/internal-urls/memory-protocol.ts` — `memory://` URL handler
|
- `packages/coding-agent/src/internal-urls/memory-protocol.ts` — `memory://` URL handler
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Recalled memory is background context, not instructions. Current user messages a
|
|||||||
| ------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `memory.backend` | `off` | Set to `mnemopi` to enable this backend. |
|
| `memory.backend` | `off` | Set to `mnemopi` to enable this backend. |
|
||||||
| `mnemopi.dbPath` | agent memories dir | Optional SQLite database path. |
|
| `mnemopi.dbPath` | agent memories dir | Optional SQLite database path. |
|
||||||
| `mnemopi.bank` | project directory name | Base bank name passed to `Mnemopi`; the coding-agent wrapper scopes from this base according to `mnemopi.scoping`. |
|
| `mnemopi.bank` | unset | Optional shared bank base name passed to `Mnemopi`; the coding-agent wrapper scopes from this base according to `mnemopi.scoping`. Unset → shared bank `default`; per-project modes derive a project bank from the project root name plus a stable hash. |
|
||||||
| `mnemopi.scoping` | `per-project` | Memory visibility mode: `global` = one shared bank, `per-project` = isolated project memory, `per-project-tagged` = project-local writes plus global recall visibility. |
|
| `mnemopi.scoping` | `per-project` | Memory visibility mode: `global` = one shared bank, `per-project` = isolated project memory, `per-project-tagged` = project-local writes plus global recall visibility. |
|
||||||
| `mnemopi.autoRecall` | `true` | Recall memory on the first turn of a session. |
|
| `mnemopi.autoRecall` | `true` | Recall memory on the first turn of a session. |
|
||||||
| `mnemopi.autoRetain` | `true` | Retain completed turns automatically. |
|
| `mnemopi.autoRetain` | `true` | Retain completed turns automatically. |
|
||||||
@@ -67,7 +67,7 @@ The combined project-plus-global behavior lives in the wrapper. The `@oh-my-pi/p
|
|||||||
|
|
||||||
## LLM and embeddings
|
## LLM and embeddings
|
||||||
|
|
||||||
The backend passes these settings to the `Mnemopi` constructor; if a setting is omitted, Mnemopi falls back to its `MNEMOPI_*` environment defaults. The backend does not download or run a local GGUF LLM. LLM-dependent paths use a configured pi-ai model, a dynamic completion function, a remote OpenAI-compatible endpoint, or deterministic no-LLM fallbacks.
|
The backend passes these settings to the `Mnemopi` constructor; if a setting is omitted, Mnemopi falls back to its `MNEMOPI_*` environment defaults. The backend does not download or run a local GGUF LLM. LLM-dependent paths use a configured pi-ai model, an opt-in local on-device memory model (`providers.memoryModel`, ONNX — overrides `smol`/`remote` when set to a local model), a dynamic completion function, a remote OpenAI-compatible endpoint, or deterministic no-LLM fallbacks.
|
||||||
|
|
||||||
FTS-only:
|
FTS-only:
|
||||||
|
|
||||||
|
|||||||
+16
-9
@@ -10,7 +10,7 @@ Primary implementation files:
|
|||||||
- `src/config/model-resolver.ts` — parses model patterns and selects initial/smol/slow models
|
- `src/config/model-resolver.ts` — parses model patterns and selects initial/smol/slow models
|
||||||
- `src/config/settings-schema.ts` — model-related settings (`modelRoles`, provider transport preferences)
|
- `src/config/settings-schema.ts` — model-related settings (`modelRoles`, provider transport preferences)
|
||||||
- `src/session/auth-storage.ts` — API key + OAuth resolution order
|
- `src/session/auth-storage.ts` — API key + OAuth resolution order
|
||||||
- `packages/ai/src/models.ts` and `packages/ai/src/types.ts` — built-in providers/models and `Model`/`compat` types
|
- `packages/catalog/src/models.ts` and `packages/catalog/src/types.ts` — built-in providers/models (`getBundledModels` / `getBundledProviders`) and `Model`/`compat` types
|
||||||
|
|
||||||
## Config file location and legacy behavior
|
## Config file location and legacy behavior
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ Must define at least one of:
|
|||||||
|
|
||||||
### Discovery
|
### Discovery
|
||||||
|
|
||||||
- `discovery` requires provider-level `api`.
|
- `discovery` requires provider-level `api`, except `discovery.type: proxy` (per-model wire auto-detected).
|
||||||
|
|
||||||
### Model value checks
|
### Model value checks
|
||||||
|
|
||||||
@@ -167,13 +167,13 @@ ModelRegistry pipeline (on refresh):
|
|||||||
### Provider-model cache and static fingerprint
|
### Provider-model cache and static fingerprint
|
||||||
|
|
||||||
Cached per-provider model lists are persisted in the model-cache SQLite
|
Cached per-provider model lists are persisted in the model-cache SQLite
|
||||||
database (schema v3) with a `static_fingerprint` column that hashes the
|
database (current schema version 5) with a `static_fingerprint` column that
|
||||||
static catalog slice merged into the row. When `resolveProviderModels`
|
hashes the static catalog slice merged into the row. When `resolveProviderModels`
|
||||||
skips the network fetch and the fingerprint of the in-memory static
|
skips the network fetch and the fingerprint of the in-memory static
|
||||||
catalog matches the cached one, the cached rows are returned verbatim —
|
catalog matches the cached one, the cached rows are returned verbatim —
|
||||||
the static + dynamic merge is bypassed entirely. The fingerprint is
|
the static + dynamic merge is bypassed entirely. The fingerprint is
|
||||||
memoized per process via a WeakMap keyed by the static-models array
|
memoized per process by tagging the static-models array with a symbol
|
||||||
reference, so repeated cold-start calls do not re-hash.
|
property, so repeated cold-start calls do not re-hash.
|
||||||
|
|
||||||
## Canonical model equivalence and coalescing
|
## Canonical model equivalence and coalescing
|
||||||
|
|
||||||
@@ -525,7 +525,7 @@ The built-in model policy currently links OpenAI `codex-spark` variants to `gpt-
|
|||||||
|
|
||||||
## Compatibility and routing fields
|
## Compatibility and routing fields
|
||||||
|
|
||||||
The `compat` block on a provider or model overrides the URL-based auto-detection in `packages/ai/src/providers/openai-completions-compat.ts`. It is validated by `OpenAICompatSchema` in `packages/coding-agent/src/config/models-config-schema.ts` and consumed by every `openai-completions` transport (`packages/ai/src/providers/openai-completions.ts`). The canonical type is `OpenAICompat` in `packages/ai/src/types.ts`.
|
The `compat` block on a provider or model overrides the URL-based auto-detection in `packages/catalog/src/compat/openai.ts` (`buildOpenAICompat`). It is validated by `OpenAICompatSchema` in `packages/coding-agent/src/config/models-config-schema.ts` and consumed by every `openai-completions` transport (`packages/ai/src/providers/openai-completions.ts`). The canonical type is `OpenAICompat` in `packages/catalog/src/types.ts`.
|
||||||
|
|
||||||
`models.yml` accepts the following keys (all optional; unset falls back to URL detection):
|
`models.yml` accepts the following keys (all optional; unset falls back to URL detection):
|
||||||
|
|
||||||
@@ -539,17 +539,24 @@ Request shaping:
|
|||||||
- `supportsToolChoice` — emit the `tool_choice` parameter when the caller forces a specific tool. Default: `true`. Set `false` for endpoints that 400 on `tool_choice` (e.g. DeepSeek when reasoning is on).
|
- `supportsToolChoice` — emit the `tool_choice` parameter when the caller forces a specific tool. Default: `true`. Set `false` for endpoints that 400 on `tool_choice` (e.g. DeepSeek when reasoning is on).
|
||||||
- `disableReasoningOnForcedToolChoice` — drop `reasoning_effort` / OpenRouter `reasoning` whenever `tool_choice` forces a call. Default: auto (Kimi/Anthropic-fronted endpoints).
|
- `disableReasoningOnForcedToolChoice` — drop `reasoning_effort` / OpenRouter `reasoning` whenever `tool_choice` forces a call. Default: auto (Kimi/Anthropic-fronted endpoints).
|
||||||
- `disableReasoningOnToolChoice` — drop reasoning fields whenever any `tool_choice` is sent. Default: auto (DeepSeek reasoning models).
|
- `disableReasoningOnToolChoice` — drop reasoning fields whenever any `tool_choice` is sent. Default: auto (DeepSeek reasoning models).
|
||||||
|
- `alwaysSendMaxTokens` — always send a max-token field when the caller did not provide one. Default: auto (Kimi-family models derive TPM limits from `max_tokens`).
|
||||||
|
- `strictResponsesPairing` — Responses-API tool-call/result history must be strictly paired. Default: auto (Azure OpenAI, GitHub Copilot).
|
||||||
|
- `streamIdleTimeoutMs` — stream-watchdog idle-timeout floor in ms for slow reasoning hosts. Default: auto (GLM coding-plan hosts, direct DeepSeek reasoning).
|
||||||
|
- `cacheControlFormat` — `"anthropic"` to include Anthropic-style prompt-cache markers in chat-completions payloads. Default: auto (OpenRouter `anthropic/*` models).
|
||||||
|
- `supportsLongPromptCacheRetention` — host honors `prompt_cache_retention: "24h"` on the Responses API. Default: auto (api.openai.com).
|
||||||
- `extraBody` — extra top-level fields merged into every request body (gateway hints, controller selectors, etc.).
|
- `extraBody` — extra top-level fields merged into every request body (gateway hints, controller selectors, etc.).
|
||||||
|
|
||||||
Reasoning / thinking:
|
Reasoning / thinking:
|
||||||
|
|
||||||
- `supportsReasoningEffort` — accept `reasoning_effort`. Default: auto (off for Grok and zAI).
|
- `supportsReasoningEffort` — accept `reasoning_effort`. Default: auto (off for Grok, Z.ai/Zhipu, and Xiaomi MiMo).
|
||||||
|
- `supportsReasoningParams` — whether request shaping may send reasoning params at all. Default: auto (off for GitHub Copilot chat-completions).
|
||||||
- `reasoningEffortMap` — partial map from internal effort levels (`minimal|low|medium|high|xhigh`) to provider-specific strings (e.g. DeepSeek maps `xhigh -> "max"`).
|
- `reasoningEffortMap` — partial map from internal effort levels (`minimal|low|medium|high|xhigh`) to provider-specific strings (e.g. DeepSeek maps `xhigh -> "max"`).
|
||||||
- `thinkingFormat` — request shape for thinking: `"openai"` (`reasoning_effort`), `"openrouter"` (`reasoning: { effort }`), `"zai"` (`thinking: { type: "enabled" }`), `"qwen"` (top-level `enable_thinking`), or `"qwen-chat-template"` (`chat_template_kwargs.enable_thinking`). Default: `"openai"`.
|
- `thinkingFormat` — request shape for thinking: `"openai"` (`reasoning_effort`), `"openrouter"` (`reasoning: { effort }`), `"zai"` (`thinking: { type: "enabled" }`), `"qwen"` (top-level `enable_thinking`), or `"qwen-chat-template"` (`chat_template_kwargs.enable_thinking`). Default: `"openai"`.
|
||||||
- `reasoningContentField` — assistant field carrying chain-of-thought: `"reasoning_content"`, `"reasoning"`, or `"reasoning_text"`. Default: auto.
|
- `reasoningContentField` — assistant field carrying chain-of-thought: `"reasoning_content"`, `"reasoning"`, or `"reasoning_text"`. Default: auto.
|
||||||
- `requiresReasoningContentForToolCalls` — assistant tool-call turns must round-trip the reasoning field (DeepSeek-R1, Kimi, OpenRouter when reasoning is on). Default: `false`.
|
- `requiresReasoningContentForToolCalls` — assistant tool-call turns must round-trip the reasoning field (DeepSeek-R1, Kimi, OpenRouter when reasoning is on). Default: `false`.
|
||||||
- `allowsSyntheticReasoningContentForToolCalls` — allow a placeholder reasoning field when a prior assistant tool-call turn lacks provider reasoning content. Default: `true`; set `false` for providers that validate the exact reasoning value.
|
- `allowsSyntheticReasoningContentForToolCalls` — allow a placeholder reasoning field when a prior assistant tool-call turn lacks provider reasoning content. Default: `true`; set `false` for providers that validate the exact reasoning value.
|
||||||
- `requiresAssistantContentForToolCalls` — assistant tool-call turns must include non-empty text content (Kimi). Default: `false`.
|
- `requiresAssistantContentForToolCalls` — assistant tool-call turns must include non-empty text content (Kimi). Default: `false`.
|
||||||
|
- `whenThinking` — partial compat overrides applied only when a request actually engages thinking mode (deep-merged over the baseline compat).
|
||||||
|
|
||||||
Tool / message normalization:
|
Tool / message normalization:
|
||||||
|
|
||||||
@@ -569,7 +576,7 @@ Provider-level `compat` is the baseline; per-model `compat` is deep-merged on to
|
|||||||
|
|
||||||
### Anthropic compatibility (`anthropic-messages`)
|
### Anthropic compatibility (`anthropic-messages`)
|
||||||
|
|
||||||
For `anthropic-messages` models the runtime uses a separate `AnthropicCompat` shape (`packages/ai/src/types.ts`). The `models.yml` schema currently exposes only the strict-tools opt-out as a top-level provider field (see below); the remaining Anthropic-side knobs (`disableAdaptiveThinking`, `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`, `supportsMidConversationSystem`) are set by built-in catalog metadata and are not user-configurable from `models.yml`.
|
For `anthropic-messages` models the runtime uses a separate `AnthropicCompat` shape (`packages/catalog/src/types.ts`). The `models.yml` schema exposes the strict-tools opt-out as a top-level provider field (see below) plus two Anthropic-side flags in the same `compat` slot — `requiresToolResultId` (non-standard `id` alias on `tool_result` blocks for Z.AI-style proxies) and `replayUnsignedThinking` (replay unsigned thinking blocks as native thinking instead of demoting them to text); the remaining Anthropic-side knobs (`disableAdaptiveThinking`, `supportsEagerToolInputStreaming`, `supportsLongCacheRetention`, `supportsMidConversationSystem`, `supportsForcedToolChoice`, `supportsSamplingParams`) are set by built-in catalog metadata and are not user-configurable from `models.yml`.
|
||||||
|
|
||||||
### Strict tool schemas (`disableStrictTools`)
|
### Strict tool schemas (`disableStrictTools`)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ At module initialization, `native/index.js` computes:
|
|||||||
- **Platform tag**: `${process.platform}-${process.arch}` (for example `darwin-arm64`).
|
- **Platform tag**: `${process.platform}-${process.arch}` (for example `darwin-arm64`).
|
||||||
- **Package version**: from `packages/natives/package.json`.
|
- **Package version**: from `packages/natives/package.json`.
|
||||||
- **Core directories**:
|
- **Core directories**:
|
||||||
- `leafPackageDir`: directory of the platform leaf package, resolved via `require.resolve("@oh-my-pi/pi-natives-<tag>/package.json")`; `null` when no leaf is installed (e.g. local dev).
|
- `leafPackageDir`: directory of the platform leaf package, resolved via `require.resolve("@oh-my-pi/pi-natives-<tag>/package.json")`; `null` when no leaf is installed (e.g. local dev) and forced to `null` in compiled-binary mode.
|
||||||
- `nativeDir`: package-local `packages/natives/native`.
|
- `nativeDir`: package-local `packages/natives/native`.
|
||||||
- `execDir`: directory containing `process.execPath`.
|
- `execDir`: directory containing `process.execPath`.
|
||||||
- `versionedDir`: `<getNativesDir()>/<packageVersion>`.
|
- `versionedDir`: `<getNativesDir()>/<packageVersion>`.
|
||||||
@@ -95,11 +95,10 @@ The default unsuffixed fallback remains part of the x64 candidate list.
|
|||||||
|
|
||||||
### Non-compiled runtime
|
### Non-compiled runtime
|
||||||
|
|
||||||
For each filename, candidates are, in order:
|
Candidates are grouped by directory class, in order:
|
||||||
|
|
||||||
1. `<leafPackageDir>/<filename>` (omitted when `leafPackageDir` is `null`)
|
1. `<leafPackageDir>/<filename>` for every filename (omitted when `leafPackageDir` is `null`)
|
||||||
2. `<nativeDir>/<filename>`
|
2. `<nativeDir>/<filename>` then `<execDir>/<filename>`, per filename
|
||||||
3. `<execDir>/<filename>`
|
|
||||||
|
|
||||||
The leaf package dir comes first so the optional-dependency binary published with the release is preferred over any `.node` left in the core package's `native/` (e.g. a stale local-dev build).
|
The leaf package dir comes first so the optional-dependency binary published with the release is preferred over any `.node` left in the core package's `native/` (e.g. a stale local-dev build).
|
||||||
|
|
||||||
@@ -107,12 +106,10 @@ On Windows installs where `nativeDir` is inside a `node_modules` segment (`shoul
|
|||||||
|
|
||||||
### Compiled runtime
|
### Compiled runtime
|
||||||
|
|
||||||
For each filename, candidates are:
|
Candidates are grouped, in order:
|
||||||
|
|
||||||
1. `<versionedDir>/<filename>`
|
1. `<versionedDir>/<filename>` then `<userDataDir>/<filename>`, per filename
|
||||||
2. `<userDataDir>/<filename>`
|
2. `<nativeDir>/<filename>` then `<execDir>/<filename>`, per filename
|
||||||
3. `<nativeDir>/<filename>`
|
|
||||||
4. `<execDir>/<filename>`
|
|
||||||
|
|
||||||
At load time, an extracted embedded candidate, or a staged Windows candidate when no embedded candidate exists, is prepended ahead of these de-duplicated candidates.
|
At load time, an extracted embedded candidate, or a staged Windows candidate when no embedded candidate exists, is prepended ahead of these de-duplicated candidates.
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ Loader failures are explicit:
|
|||||||
- `ast`
|
- `ast`
|
||||||
- `block`
|
- `block`
|
||||||
- `clipboard`
|
- `clipboard`
|
||||||
|
- `crash_handler`
|
||||||
- `fd`
|
- `fd`
|
||||||
- `fs_cache`
|
- `fs_cache`
|
||||||
- `glob`
|
- `glob`
|
||||||
@@ -123,6 +124,7 @@ Loader failures are explicit:
|
|||||||
- `pty`
|
- `pty`
|
||||||
- `shell`
|
- `shell`
|
||||||
- `sixel`
|
- `sixel`
|
||||||
|
- `snapcompact`
|
||||||
- `summary`
|
- `summary`
|
||||||
- `task`
|
- `task`
|
||||||
- `text`
|
- `text`
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ Changing sync ↔ async for an existing export is a breaking public API change b
|
|||||||
`#[napi(object)]` Rust structs become TS interfaces, for example:
|
`#[napi(object)]` Rust structs become TS interfaces, for example:
|
||||||
|
|
||||||
- `GrepResult`, `SearchResult`, `GlobResult`, `FuzzyFindResult`
|
- `GrepResult`, `SearchResult`, `GlobResult`, `FuzzyFindResult`
|
||||||
- `ShellRunResult`, `ShellExecuteResult`, `PtyRunResult`, `MinimizerResult`
|
- `ShellRunResult`, `PtyRunResult`, `MinimizerResult`
|
||||||
- `AstFindResult`, `AstReplaceResult`, `BlockRange`, `SummaryResult`
|
- `AstFindResult`, `AstReplaceResult`, `BlockRange`, `SummaryResult`
|
||||||
- `System`/media/isolation payloads such as `ClipboardImage`, `WorkProfile`, `ParsedKittyResult`, `IsoResolveResult`
|
- `System`/media/isolation payloads such as `ClipboardImage`, `WorkProfile`, `ParsedKittyResult`, `IsoResolveResult`
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ It follows the architecture terms from `docs/natives-architecture.md`:
|
|||||||
|
|
||||||
- `bun scripts/build-native.ts` (`build`) → N-API build, addon install, generated declarations install, explicit ESM export and enum runtime patch.
|
- `bun scripts/build-native.ts` (`build`) → N-API build, addon install, generated declarations install, explicit ESM export and enum runtime patch.
|
||||||
- `bun scripts/embed-native.ts` (`embed:native`) → generate `native/embedded-addon.js` plus `native/embedded-addons.<tag>.tar.gz` from built files.
|
- `bun scripts/embed-native.ts` (`embed:native`) → generate `native/embedded-addon.js` plus `native/embedded-addons.<tag>.tar.gz` from built files.
|
||||||
|
- `bun scripts/gen-npm-packages.ts` (`gen:npm`) → generate per-platform npm leaf packages (`@oh-my-pi/pi-natives-<platform>-<arch>`, installed as optional dependencies of the core package) under `npm/` from built addon files.
|
||||||
|
|
||||||
Root scripts include `build:native` as `bun --cwd=packages/natives run build`.
|
Root scripts include `build:native` as `bun --cwd=packages/natives run build`.
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ Root scripts include `build:native` as `bun --cwd=packages/natives run build`.
|
|||||||
- `--no-js`
|
- `--no-js`
|
||||||
- `--dts index.d.ts`
|
- `--dts index.d.ts`
|
||||||
- `--profile local` for non-CI local native builds, otherwise `--profile ci`
|
- `--profile local` for non-CI local native builds, otherwise `--profile ci`
|
||||||
- optional `--target <CROSS_TARGET>`
|
- `-o <isolated temp output dir>`
|
||||||
|
- optional `--target <CROSS_TARGET>` plus `--cross-compile` (napi picks the `cargo-zigbuild` or `cargo-xwin` backend from the target) for cross builds
|
||||||
|
|
||||||
`crates/pi-natives/Cargo.toml` declares `crate-type = ["cdylib"]`; napi-rs emits `.node` artifacts plus generated `index.d.ts` in an isolated temporary output directory under `packages/natives/native/.build/`.
|
`crates/pi-natives/Cargo.toml` declares `crate-type = ["cdylib"]`; napi-rs emits `.node` artifacts plus generated `index.d.ts` in an isolated temporary output directory under `packages/natives/native/.build/`.
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ Failure exits have explicit error text for invalid variants, failed napi build,
|
|||||||
4. **Generate archive + manifest**: write `native/embedded-addons.<platform>-<arch>.tar.gz` containing all available target addon files and `native/embedded-addon.js` with package version, archive metadata, and file sizes.
|
4. **Generate archive + manifest**: write `native/embedded-addons.<platform>-<arch>.tar.gz` containing all available target addon files and `native/embedded-addon.js` with package version, archive metadata, and file sizes.
|
||||||
5. **Runtime extraction ready** for compiled mode.
|
5. **Runtime extraction ready** for compiled mode.
|
||||||
|
|
||||||
`--reset` writes the null manifest stub (`embeddedAddon = null`) without validating addon availability.
|
`--reset` writes the null manifest stub (`embeddedAddon = null`) without validating addon availability, and deletes any existing `embedded-addons.*.tar.gz` archives from `native/`.
|
||||||
|
|
||||||
## Dev workflow vs shipped/compiled behavior
|
## Dev workflow vs shipped/compiled behavior
|
||||||
|
|
||||||
@@ -140,7 +142,7 @@ Failure exits have explicit error text for invalid variants, failed napi build,
|
|||||||
Typical local loop:
|
Typical local loop:
|
||||||
|
|
||||||
1. Build addon: `bun --cwd=packages/natives run build`.
|
1. Build addon: `bun --cwd=packages/natives run build`.
|
||||||
2. Loader resolves package-local `native/` candidates, then executable-dir fallback candidates.
|
2. Loader resolves platform npm leaf-package candidates (`@oh-my-pi/pi-natives-<platform>-<arch>`, when resolvable), then package-local `native/` and executable-dir fallback candidates.
|
||||||
3. Generated declarations in `native/index.d.ts` describe the public TS API.
|
3. Generated declarations in `native/index.d.ts` describe the public TS API.
|
||||||
|
|
||||||
## Shipped/compiled binary workflow
|
## Shipped/compiled binary workflow
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Conversion behavior:
|
|||||||
|
|
||||||
### Clipboard (`clipboard`)
|
### Clipboard (`clipboard`)
|
||||||
|
|
||||||
- `copyToClipboard(text)` is a synchronous native call using `arboard::Clipboard::set_text`.
|
- `copyToClipboard(text)` is a synchronous native call using `arboard::Clipboard::set_text`. On Linux a single process-lifetime `Clipboard` instance is kept alive (X11/Wayland selection ownership); macOS/Windows use a transient instance per call.
|
||||||
- `readImageFromClipboard()` runs in `task::blocking("clipboard.read_image", (), ...)`.
|
- `readImageFromClipboard()` runs in `task::blocking("clipboard.read_image", (), ...)`.
|
||||||
- Image read returns `null`/`undefined` when `arboard` reports `ContentNotAvailable`.
|
- Image read returns `null`/`undefined` when `arboard` reports `ContentNotAvailable`.
|
||||||
- Successful image read converts clipboard RGBA data into PNG bytes and returns `{ data: Uint8Array, mimeType: "image/png" }`.
|
- Successful image read converts clipboard RGBA data into PNG bytes and returns `{ data: Uint8Array, mimeType: "image/png" }`.
|
||||||
@@ -112,7 +112,7 @@ Failure transitions:
|
|||||||
|
|
||||||
### Clipboard lifecycle
|
### Clipboard lifecycle
|
||||||
|
|
||||||
- Text copy constructs an `arboard::Clipboard` and calls `set_text` synchronously.
|
- Text copy calls `set_text` synchronously; macOS/Windows construct a transient `arboard::Clipboard` per call, while Linux initializes one process-lifetime instance on first copy and reuses it.
|
||||||
- Image read constructs an `arboard::Clipboard`, calls `get_image`, encodes PNG on success, maps `ContentNotAvailable` to `None`, and rejects other errors.
|
- Image read constructs an `arboard::Clipboard`, calls `get_image`, encodes PNG on success, maps `ContentNotAvailable` to `None`, and rejects other errors.
|
||||||
|
|
||||||
### Work profiling lifecycle
|
### Work profiling lifecycle
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Rust creates `brush_core::Shell` with:
|
|||||||
- inherited environment disabled (`do_not_inherit_env: true`), followed by explicit environment reconstruction from host env,
|
- inherited environment disabled (`do_not_inherit_env: true`), followed by explicit environment reconstruction from host env,
|
||||||
- profile and rc loading skipped,
|
- profile and rc loading skipped,
|
||||||
- bash-mode builtins, with `exec` and `suspend` disabled,
|
- bash-mode builtins, with `exec` and `suspend` disabled,
|
||||||
- native `sleep` and `timeout` builtins registered,
|
- native `sleep`, `timeout`, and `nohup` builtins registered,
|
||||||
- skip-list for shell-sensitive vars (`PS1`, `PWD`, `SHLVL`, bash function exports, etc.),
|
- skip-list for shell-sensitive vars (`PS1`, `PWD`, `SHLVL`, bash function exports, etc.),
|
||||||
- a non-exported `env="$env"` fallback so PowerShell-style `$env:NAME` survives brush parameter expansion unless the user shadows `env`.
|
- a non-exported `env="$env"` fallback so PowerShell-style `$env:NAME` survives brush parameter expansion unless the user shadows `env`.
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ The parser combines:
|
|||||||
|
|
||||||
Modifier handling:
|
Modifier handling:
|
||||||
|
|
||||||
- only shift/alt/ctrl bits are compared for key matching,
|
- only shift/alt/ctrl/super bits are compared for key matching,
|
||||||
- lock bits are masked out before comparisons.
|
- lock bits are masked out before comparisons.
|
||||||
|
|
||||||
Layout behavior:
|
Layout behavior:
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ So: overload/rate/server/network-style failures use this retry policy; context-w
|
|||||||
- assistant `stopReason === "error"`
|
- assistant `stopReason === "error"`
|
||||||
- `errorMessage` exists
|
- `errorMessage` exists
|
||||||
- message is **not** context overflow
|
- message is **not** context overflow
|
||||||
- `errorMessage` matches transient transport/envelope patterns or `isUsageLimitError(...)`
|
- one of:
|
||||||
|
- the stop is a classifier refusal (`stopDetails.type` is `"refusal"` or `"sensitive"`)
|
||||||
|
- the error is a stale OpenAI Responses replay failure (`Item with id '…' not found`, or an invalid/expired/not-found `previous_response`)
|
||||||
|
- `errorMessage` matches transient transport/envelope patterns or `isUsageLimitError(...)`
|
||||||
|
|
||||||
Current retryable inputs are regex/string-classified:
|
Current retryable inputs are regex/string-classified:
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ Current retryable inputs are regex/string-classified:
|
|||||||
- provider-suggested retry wording, including OpenAI `retry your request` failures
|
- provider-suggested retry wording, including OpenAI `retry your request` failures
|
||||||
- network/connection/socket failures, refused/closed connections, upstream connect/reset-before-headers, socket hang up, timeout/timed out, fetch failed, terminated, retry delay wording, and unexpected socket close messages
|
- network/connection/socket failures, refused/closed connections, upstream connect/reset-before-headers, socket hang up, timeout/timed out, fetch failed, terminated, retry delay wording, and unexpected socket close messages
|
||||||
|
|
||||||
This is string-pattern classification, not typed provider error codes.
|
Transport classification is regex text matching, not typed provider error codes; classifier refusals are the exception, detected from the typed `stopDetails` field.
|
||||||
|
|
||||||
## Retry lifecycle and state transitions
|
## Retry lifecycle and state transitions
|
||||||
|
|
||||||
@@ -62,9 +65,9 @@ Flow (`#handleRetryableError`):
|
|||||||
3. Increment `#retryAttempt`.
|
3. Increment `#retryAttempt`.
|
||||||
4. Create `#retryPromise` once (first attempt in a chain).
|
4. Create `#retryPromise` once (first attempt in a chain).
|
||||||
5. If attempt exceeded `retry.maxRetries`, emit final failure event and stop.
|
5. If attempt exceeded `retry.maxRetries`, emit final failure event and stop.
|
||||||
6. Compute capped jittered local delay: `min(retry.baseDelayMs * 2^(attempt-1), 8000ms) * (75–100% jitter)`.
|
6. Compute capped jittered local delay: `min(retry.baseDelayMs * 2^(attempt-1), 8000ms) * (75–100% jitter)`. Stale OpenAI Responses replay errors skip the backoff entirely (delay `0`) after resetting the cached provider session.
|
||||||
7. For usage-limit errors, parse retry hints and call auth storage (`markUsageLimitReached(...)`); if credential switching succeeds, force delay to `0`. Otherwise wait for whichever comes first — the provider's retry-after/backoff hint, or the earliest moment a temporarily blocked sibling credential frees up (`retryAtMs` + 1s buffer) so the next attempt can pick it up.
|
7. For usage-limit errors, parse retry hints and call auth storage (`markUsageLimitReached(...)`); if credential switching succeeds — including spending a banked Codex reset via the opt-in auto-redeem — force delay to `0`. Otherwise wait for whichever comes first — the provider's retry-after/backoff hint, or the earliest moment a temporarily blocked sibling credential frees up (`retryAtMs` + 1s buffer) so the next attempt can pick it up.
|
||||||
8. If no credential switch occurred, suppress the current model selector for cooldown, try configured retry model fallback chains, and force delay to `0` on model switch.
|
8. If no credential switch occurred and `retry.modelFallback` is enabled, suppress the current model selector for cooldown and try configured retry model fallback chains, forcing delay to `0` on model switch. Classifier refusals skip the cooldown and only proceed when a fallback model was actually applied (pinned); with no fallback, the chain ends without an `auto_retry_start`.
|
||||||
9. If the final delay exceeds `retry.maxDelayMs` and no credential/model switch happened, emit final failure and do not sleep.
|
9. If the final delay exceeds `retry.maxDelayMs` and no credential/model switch happened, emit final failure and do not sleep.
|
||||||
10. Emit `auto_retry_start`.
|
10. Emit `auto_retry_start`.
|
||||||
11. Remove the trailing assistant error message from agent runtime state (kept in persisted session history).
|
11. Remove the trailing assistant error message from agent runtime state (kept in persisted session history).
|
||||||
@@ -79,8 +82,9 @@ Flow (`#handleRetryableError`):
|
|||||||
- retry cancellation during backoff sleep
|
- retry cancellation during backoff sleep
|
||||||
- max retries exceeded path
|
- max retries exceeded path
|
||||||
- max delay exceeded path
|
- max delay exceeded path
|
||||||
|
- classifier refusal with no fallback model applied (chain ends silently, no retry started)
|
||||||
|
|
||||||
`#retryPromise` resolves/clears when retry chain ends (success, cancellation, max-exceeded, or max-delay failure), via `#resolveRetry()`.
|
`#retryPromise` resolves/clears when retry chain ends (success, cancellation, max-exceeded, max-delay failure, or classifier-refusal stop), via `#resolveRetry()`.
|
||||||
|
|
||||||
## Backoff and max-attempt semantics
|
## Backoff and max-attempt semantics
|
||||||
|
|
||||||
@@ -138,7 +142,7 @@ On `auto_retry_end`, it restores prior `Esc` handler and clears loader state.
|
|||||||
|
|
||||||
## Streaming and prompt completion behavior
|
## Streaming and prompt completion behavior
|
||||||
|
|
||||||
`prompt()` ultimately waits on `#waitForRetry()` after `agent.prompt(...)` returns.
|
`prompt()` ultimately waits on `#waitForPostPromptRecovery()` after `agent.prompt(...)` returns; that loop awaits the retry lifecycle promise alongside TTSR resume and deferred post-prompt tasks.
|
||||||
|
|
||||||
Effect:
|
Effect:
|
||||||
|
|
||||||
@@ -157,6 +161,7 @@ Defined in settings schema under retry group:
|
|||||||
- `retry.maxRetries`
|
- `retry.maxRetries`
|
||||||
- `retry.baseDelayMs`
|
- `retry.baseDelayMs`
|
||||||
- `retry.maxDelayMs`
|
- `retry.maxDelayMs`
|
||||||
|
- `retry.modelFallback` (default `true`; gates retry model-fallback switching)
|
||||||
- `retry.fallbackChains`
|
- `retry.fallbackChains`
|
||||||
- `retry.fallbackRevertPolicy` (`"cooldown-expiry"` by default; `"never"` disables automatic restoration)
|
- `retry.fallbackRevertPolicy` (`"cooldown-expiry"` by default; `"never"` disables automatic restoration)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ Kernel semantics are implemented in `executePython` / `PythonKernel` and apply t
|
|||||||
`PythonKernelMode`:
|
`PythonKernelMode`:
|
||||||
|
|
||||||
- `session` (default)
|
- `session` (default)
|
||||||
- kernels are cached by `(session id, cwd)`
|
- kernels are cached by `(session id, cwd, interpreter)`
|
||||||
- multiple owners can share a retained kernel for the same key
|
- multiple owners can share a retained kernel for the same key
|
||||||
- execution is serialized by the tool's exclusive concurrency and backend execution path
|
- execution is serialized by the tool's exclusive concurrency and backend execution path
|
||||||
- dead kernels are replaced before execution
|
- dead kernels are replaced before execution
|
||||||
@@ -103,7 +103,7 @@ In session mode:
|
|||||||
|
|
||||||
- if the retained subprocess is not alive before execution, it is replaced
|
- if the retained subprocess is not alive before execution, it is replaced
|
||||||
- if execution fails because the subprocess died, the kernel is replaced and the code is retried once
|
- if execution fails because the subprocess died, the kernel is replaced and the code is retried once
|
||||||
- explicit `reset` is rejected while another reset for the same session key is already in progress
|
- concurrent resets for the same session key coalesce: a reset already in flight is awaited instead of starting another, and runs queued behind it proceed on the freshly-restarted kernel
|
||||||
|
|
||||||
## 4) Environment/session variable injection
|
## 4) Environment/session variable injection
|
||||||
|
|
||||||
@@ -114,6 +114,7 @@ Kernel startup and per-execution environment patching can receive:
|
|||||||
- `PI_TOOL_BRIDGE_URL`
|
- `PI_TOOL_BRIDGE_URL`
|
||||||
- `PI_TOOL_BRIDGE_TOKEN`
|
- `PI_TOOL_BRIDGE_TOKEN`
|
||||||
- `PI_TOOL_BRIDGE_SESSION`
|
- `PI_TOOL_BRIDGE_SESSION`
|
||||||
|
- `PI_EVAL_LOCAL_ROOTS`
|
||||||
|
|
||||||
The runner initializes process state so code executes in the requested cwd, managed env entries are reflected in `os.environ`, and cwd is available on `sys.path`.
|
The runner initializes process state so code executes in the requested cwd, managed env entries are reflected in `os.environ`, and cwd is available on `sys.path`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Plugin manager and installer plumbing
|
# Plugin manager and installer plumbing
|
||||||
|
|
||||||
This document describes how `omp plugin` npm/link operations mutate plugin state on disk and how installed npm/link plugins become runtime capabilities (tools and extensions today, hooks/commands path resolution available). Marketplace installs use separate marketplace registries and cache plumbing; see `docs/marketplace.md`.
|
This document describes how `omp plugin` npm/git/link operations mutate plugin state on disk and how installed npm/git/link plugins become runtime capabilities (tools and extensions today, hooks/commands path resolution available). Marketplace installs use separate marketplace registries and cache plumbing; see `docs/marketplace.md`.
|
||||||
|
|
||||||
## Scope and architecture
|
## Scope and architecture
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ There are two plugin-management implementations in the codebase:
|
|||||||
1. **Active path used by CLI commands**: `PluginManager` (`src/extensibility/plugins/manager.ts`)
|
1. **Active path used by CLI commands**: `PluginManager` (`src/extensibility/plugins/manager.ts`)
|
||||||
2. **Legacy helper module**: installer functions (`src/extensibility/plugins/installer.ts`)
|
2. **Legacy helper module**: installer functions (`src/extensibility/plugins/installer.ts`)
|
||||||
|
|
||||||
`omp plugin` npm/link actions go through `PluginManager`; marketplace actions go through `MarketplaceManager`.
|
`omp plugin` npm/git/link actions go through `PluginManager`; marketplace actions go through `MarketplaceManager`. `install` classifies each target (`classifyInstallTarget` in `cli/classify-install-target.ts`): `name@marketplace` routes to the marketplace manager, local paths route to `PluginManager.link()`, git and npm specs to `PluginManager.install()`.
|
||||||
|
|
||||||
`installer.ts` still documents important safety checks and filesystem behavior, but it is not the path used by `src/commands/plugin.ts` + `src/cli/plugin-cli.ts`.
|
`installer.ts` still documents important safety checks and filesystem behavior, but it is not the path used by `src/commands/plugin.ts` + `src/cli/plugin-cli.ts`.
|
||||||
|
|
||||||
@@ -75,6 +75,8 @@ Marketplace registries live separately:
|
|||||||
- `pkg[a,b]` -> enable named features
|
- `pkg[a,b]` -> enable named features
|
||||||
- `@scope/pkg@1.2.3[feat]` -> scoped + versioned package with explicit feature selection
|
- `@scope/pkg@1.2.3[feat]` -> scoped + versioned package with explicit feature selection
|
||||||
|
|
||||||
|
`PluginManager.install` also accepts git sources (validated by `validateGitSpec` instead of the npm regex): namespaced shorthands `github:user/repo[#ref]`, `gitlab:`, `bitbucket:`, `codeberg:`, `sourcehut:`/`srht:`, and full git URLs (`https://github.com/user/repo`, `git@github.com:user/repo`, `ssh://…`, `git+https://…`). Git specs do not encode the package name, so install diffs `plugins/package.json#dependencies` before/after `bun install` to resolve it.
|
||||||
|
|
||||||
`extractPackageName` strips version suffix for on-disk path lookup after install.
|
`extractPackageName` strips version suffix for on-disk path lookup after install.
|
||||||
|
|
||||||
## Manifest source and required fields
|
## Manifest source and required fields
|
||||||
@@ -97,10 +99,10 @@ Malformed `package.json` JSON is a hard failure at read time; malformed manifest
|
|||||||
## Install/update flow (`PluginManager.install`)
|
## Install/update flow (`PluginManager.install`)
|
||||||
|
|
||||||
1. Parse feature bracket syntax from install spec.
|
1. Parse feature bracket syntax from install spec.
|
||||||
2. Validate package name against regex + shell-metacharacter denylist.
|
2. Validate the spec: git specs via `validateGitSpec`; npm specs against the package-name regex + shell-metacharacter denylist.
|
||||||
3. Ensure plugin `package.json` exists (`omp-plugins`, private dependencies map).
|
3. Ensure plugin `package.json` exists (`omp-plugins`, private dependencies map).
|
||||||
4. Run `bun install <packageSpec>` in `~/.omp/plugins`.
|
4. Run `bun install <packageSpec>` in `~/.omp/plugins`.
|
||||||
5. Read installed package `node_modules/<name>/package.json`.
|
5. Resolve the installed package name (npm: strip version via `extractPackageName`; git: diff `dependencies` before/after) and read `node_modules/<name>/package.json`.
|
||||||
6. Resolve manifest and compute `enabledFeatures`:
|
6. Resolve manifest and compute `enabledFeatures`:
|
||||||
- `[*]`: all declared features (or `null` if no feature map)
|
- `[*]`: all declared features (or `null` if no feature map)
|
||||||
- `[a,b]`: validates each feature exists in manifest features map
|
- `[a,b]`: validates each feature exists in manifest features map
|
||||||
@@ -162,7 +164,7 @@ Caveat: current `PluginManager.link` does not enforce the `cwd` path-boundary ch
|
|||||||
|
|
||||||
`getEnabledPlugins(cwd)` (`plugins/loader.ts`) reads:
|
`getEnabledPlugins(cwd)` (`plugins/loader.ts`) reads:
|
||||||
|
|
||||||
- plugin dependency manifest (`package.json`)
|
- plugin dependency manifest (`package.json`), unioned with lockfile plugin entries so `plugin link`-only plugins without a dependency entry are still discovered
|
||||||
- lockfile runtime state
|
- lockfile runtime state
|
||||||
- project overrides via `getConfigDirPaths("plugin-overrides.json", { user: false, cwd })`
|
- project overrides via `getConfigDirPaths("plugin-overrides.json", { user: false, cwd })`
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ Each resolver includes base entries plus feature entries:
|
|||||||
- explicit feature list -> only selected features
|
- explicit feature list -> only selected features
|
||||||
- `enabledFeatures === null` -> enable features marked `default: true`
|
- `enabledFeatures === null` -> enable features marked `default: true`
|
||||||
|
|
||||||
Manifest entries may point to a file or to a directory containing `index.ts`, `index.js`, `index.mjs`, or `index.cjs`. Missing files are silently skipped (`existsSync` guard).
|
Manifest entries may point to a file or to a directory containing `index.ts`, `index.js`, `index.mjs`, or `index.cjs`. Missing files are silently skipped (`statSync`/`existsSync` guard).
|
||||||
|
|
||||||
## Current runtime wiring differences
|
## Current runtime wiring differences
|
||||||
|
|
||||||
@@ -218,8 +220,9 @@ No cross-process locking or merge strategy exists; concurrent writers can overwr
|
|||||||
|
|
||||||
Active manager path enforces package-name validation:
|
Active manager path enforces package-name validation:
|
||||||
|
|
||||||
- regex for scoped/unscoped package specs (optionally with version)
|
- npm specs: regex for scoped/unscoped package specs (optionally with version)
|
||||||
- explicit shell metacharacter denylist (`[;&|`$(){}[]<>\\]`)
|
- shell metacharacter denylist: `;`, `&`, `|`, backtick, `$`, `(`, `)`, `{`, `}`, `<`, `>`, `\`, newline, CR, tab (`[`/`]` are allowed for feature brackets)
|
||||||
|
- git specs: `validateGitSpec` (permits `:`, `/`, `#`, `+`, `.`, `-`, `_`) instead of the npm regex
|
||||||
|
|
||||||
This limits command-injection risk when invoking `bun install/uninstall`.
|
This limits command-injection risk when invoking `bun install/uninstall`.
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ This document explains how token/tool streaming is normalized in `@oh-my-pi/pi-a
|
|||||||
## End-to-end flow
|
## End-to-end flow
|
||||||
|
|
||||||
1. `streamSimple()` (`packages/ai/src/stream.ts`) maps generic options and dispatches to a provider stream function.
|
1. `streamSimple()` (`packages/ai/src/stream.ts`) maps generic options and dispatches to a provider stream function.
|
||||||
2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, pi-native gateway transport, plus GitLab Duo/Kimi/Synthetic wrappers and extension-registered custom APIs.
|
2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, pi-native gateway transport, plus GitLab Duo/Kimi/Synthetic/xAI-Grok-Responses wrappers and extension-registered custom APIs.
|
||||||
3. Each provider pushes events into `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`), which throttles delta events and exposes:
|
3. Each provider pushes events into `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`), which exposes:
|
||||||
- async iteration for incremental updates
|
- async iteration for incremental updates
|
||||||
- `result()` for final `AssistantMessage`
|
- `result()` for final `AssistantMessage`
|
||||||
4. `agentLoop` (`packages/agent/src/agent-loop.ts`) consumes those events, mutates in-flight assistant state, and emits `message_update` events carrying the raw `assistantMessageEvent`.
|
4. `agentLoop` (`packages/agent/src/agent-loop.ts`) consumes those events, mutates in-flight assistant state, and emits `message_update` events carrying the raw `assistantMessageEvent`.
|
||||||
@@ -28,18 +28,13 @@ All providers emit the same shape (`AssistantMessageEvent` in `packages/ai/src/t
|
|||||||
`AssistantMessageEventStream` guarantees:
|
`AssistantMessageEventStream` guarantees:
|
||||||
|
|
||||||
- final result is resolved by terminal event (`done` or `error`)
|
- final result is resolved by terminal event (`done` or `error`)
|
||||||
- deltas are batched/throttled (~50ms)
|
- events are delivered to consumers immediately, in push order (no batching or merging)
|
||||||
- buffered deltas are flushed before non-delta events and before completion
|
|
||||||
|
|
||||||
## Delta throttling and harmonization behavior
|
## Delta throttling behavior
|
||||||
|
|
||||||
`AssistantMessageEventStream` treats `text_delta`, `thinking_delta`, and `toolcall_delta` as mergeable events:
|
`AssistantMessageEventStream` itself no longer throttles or merges delta events — every provider event is delivered as pushed. The per-delta cost control moved into tool-call argument parsing: providers accumulate partial JSON and re-parse it via `parseStreamingJsonThrottled()` (`packages/ai/src/utils/json-parse.ts`), which skips the re-parse until at least `STREAMING_JSON_PARSE_MIN_GROWTH` (256) new bytes have arrived, bounding mid-stream parse cost from quadratic to linear. The final `toolcall_end` parse is always unconditional and authoritative.
|
||||||
|
|
||||||
- buffered deltas are merged only when **type + contentIndex** match
|
There is no provider backpressure: providers still produce at full speed, while the local stream queues.
|
||||||
- merge keeps the latest `partial` snapshot
|
|
||||||
- non-delta events force immediate flush
|
|
||||||
|
|
||||||
This smooths high-frequency provider streams for TUI/event consumers, but is not provider backpressure: providers still produce at full speed, while the local stream buffers.
|
|
||||||
|
|
||||||
## Provider normalization details
|
## Provider normalization details
|
||||||
|
|
||||||
@@ -63,7 +58,7 @@ Tool-call argument streaming:
|
|||||||
|
|
||||||
- each tool block carries internal `partialJson`
|
- each tool block carries internal `partialJson`
|
||||||
- every JSON delta appends to `partialJson`
|
- every JSON delta appends to `partialJson`
|
||||||
- `arguments` are reparsed on each delta via `parseStreamingJson()`
|
- `arguments` are reparsed on appended deltas via `parseStreamingJsonThrottled()` (re-parse only after ≥256 new bytes)
|
||||||
- `toolcall_end` reparses once more, then strips `partialJson`
|
- `toolcall_end` reparses once more, then strips `partialJson`
|
||||||
|
|
||||||
## OpenAI Responses family (`openai-responses`, `openai-codex-responses`, `azure-openai-responses`)
|
## OpenAI Responses family (`openai-responses`, `openai-codex-responses`, `azure-openai-responses`)
|
||||||
@@ -88,7 +83,7 @@ Tool-call argument streaming:
|
|||||||
|
|
||||||
## Google Generative AI (`google-generative-ai`)
|
## Google Generative AI (`google-generative-ai`)
|
||||||
|
|
||||||
Source: `packages/ai/src/providers/google.ts`
|
Source: `packages/ai/src/providers/google.ts` (thin request wrapper) and `google-shared.ts` (`streamGoogleGenAI`, shared chunk-to-block translation)
|
||||||
|
|
||||||
Normalization points:
|
Normalization points:
|
||||||
|
|
||||||
@@ -106,17 +101,17 @@ Tool-call argument streaming:
|
|||||||
|
|
||||||
## Partial tool-call JSON accumulation and recovery
|
## Partial tool-call JSON accumulation and recovery
|
||||||
|
|
||||||
Shared behavior for Anthropic/OpenAI Responses uses `parseStreamingJson()` (`packages/ai/src/utils/json-parse.ts`):
|
Shared behavior for Anthropic/OpenAI Responses uses `parseStreamingJson()` / `parseStreamingJsonThrottled()` (`packages/ai/src/utils/json-parse.ts`):
|
||||||
|
|
||||||
1. try `JSON.parse`
|
1. try `JSON.parse`
|
||||||
2. fallback to `partial-json` parser for incomplete fragments
|
2. fallback to `repairJson()` + the `partial-json` parser for incomplete fragments
|
||||||
3. if both fail, return `{}`
|
3. if both fail, return `{}`
|
||||||
|
|
||||||
Implications:
|
Implications:
|
||||||
|
|
||||||
- malformed or truncated argument deltas do not crash stream processing immediately
|
- malformed or truncated argument deltas do not crash stream processing immediately
|
||||||
- in-progress `arguments` may temporarily be `{}`
|
- in-progress `arguments` may temporarily be `{}`
|
||||||
- later valid deltas can recover structured arguments because parsing is retried on every append
|
- later valid deltas can recover structured arguments because parsing is retried as the buffer grows (throttled to ≥256-byte growth steps mid-stream)
|
||||||
- final `toolcall_end` performs one more parse attempt before emission
|
- final `toolcall_end` performs one more parse attempt before emission
|
||||||
|
|
||||||
## Stop reasons vs transport/runtime errors
|
## Stop reasons vs transport/runtime errors
|
||||||
@@ -140,7 +135,7 @@ If provider stream throws or signals failure, each provider wrapper catches and
|
|||||||
|
|
||||||
## Malformed chunk / SSE parse failure behavior
|
## Malformed chunk / SSE parse failure behavior
|
||||||
|
|
||||||
Most provider paths delegate chunk/SSE framing to vendor SDK streams (Anthropic SDK, OpenAI SDK, Google SDK). The Codex SSE fallback uses `readSseJson()` directly, and websocket Codex frames are normalized through the same event handler.
|
The OpenAI Completions/Responses paths delegate chunk/SSE framing to the `openai` SDK stream. Anthropic uses the in-repo `AnthropicMessagesClient` (`packages/ai/src/providers/anthropic-client.ts`); the Google paths and the Codex SSE fallback read SSE via `readSseJson()` directly, and websocket Codex frames are normalized through the same event handler.
|
||||||
|
|
||||||
Observed behavior in current implementation:
|
Observed behavior in current implementation:
|
||||||
|
|
||||||
@@ -169,7 +164,7 @@ Tool execution cancellation is separate from model stream cancellation:
|
|||||||
There is no hard backpressure mechanism between provider SDK stream and downstream consumers:
|
There is no hard backpressure mechanism between provider SDK stream and downstream consumers:
|
||||||
|
|
||||||
- `EventStream` uses in-memory queues with no max size
|
- `EventStream` uses in-memory queues with no max size
|
||||||
- throttling reduces UI update rate but does not slow provider intake
|
- the throttled partial-JSON re-parse reduces per-delta CPU cost but does not slow provider intake
|
||||||
- if consumers lag significantly, queued events can grow until completion
|
- if consumers lag significantly, queued events can grow until completion
|
||||||
|
|
||||||
Current design favors responsiveness and simple ordering over bounded-buffer flow control.
|
Current design favors responsiveness and simple ordering over bounded-buffer flow control.
|
||||||
@@ -195,7 +190,7 @@ Unified (common contract):
|
|||||||
|
|
||||||
- event shape (`AssistantMessageEvent`)
|
- event shape (`AssistantMessageEvent`)
|
||||||
- final result extraction (`done`/`error`)
|
- final result extraction (`done`/`error`)
|
||||||
- delta throttling + merge rules
|
- immediate in-order event delivery
|
||||||
- agent/session event propagation model
|
- agent/session event propagation model
|
||||||
|
|
||||||
Provider-specific (not fully abstracted):
|
Provider-specific (not fully abstracted):
|
||||||
@@ -210,7 +205,7 @@ Provider-specific (not fully abstracted):
|
|||||||
## Implementation files
|
## Implementation files
|
||||||
|
|
||||||
- [`../../ai/src/stream.ts`](../packages/ai/src/stream.ts) — provider dispatch, option mapping, API key/session plumbing, custom API dispatch, and provider-specific credential handling.
|
- [`../../ai/src/stream.ts`](../packages/ai/src/stream.ts) — provider dispatch, option mapping, API key/session plumbing, custom API dispatch, and provider-specific credential handling.
|
||||||
- [`../../ai/src/utils/event-stream.ts`](../packages/ai/src/utils/event-stream.ts) — generic stream queue + assistant delta throttling.
|
- [`../../ai/src/utils/event-stream.ts`](../packages/ai/src/utils/event-stream.ts) — generic stream queue + final-result resolution.
|
||||||
- [`../../ai/src/utils/json-parse.ts`](../packages/ai/src/utils/json-parse.ts) — partial JSON parsing for streamed tool arguments.
|
- [`../../ai/src/utils/json-parse.ts`](../packages/ai/src/utils/json-parse.ts) — partial JSON parsing for streamed tool arguments.
|
||||||
- [`../../ai/src/providers/anthropic.ts`](../packages/ai/src/providers/anthropic.ts) — Anthropic event translation and tool JSON delta accumulation.
|
- [`../../ai/src/providers/anthropic.ts`](../packages/ai/src/providers/anthropic.ts) — Anthropic event translation and tool JSON delta accumulation.
|
||||||
- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-responses-shared.ts`](../packages/ai/src/providers/openai-responses-shared.ts), [`openai-codex-responses.ts`](../packages/ai/src/providers/openai-codex-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping.
|
- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-responses-shared.ts`](../packages/ai/src/providers/openai-responses-shared.ts), [`openai-codex-responses.ts`](../packages/ai/src/providers/openai-codex-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping.
|
||||||
|
|||||||
+10
-10
@@ -59,7 +59,7 @@ One JSON object per line, UTF-8, `\n` terminated.
|
|||||||
Host → runner:
|
Host → runner:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{"id": "<reqId>", "code": "<source>", "silent": false, "storeHistory": true}
|
{"id": "<reqId>", "code": "<source>", "silent": false, "storeHistory": true, "cwd": "<optional>", "env": {"KEY": "VAL"}}
|
||||||
{"type": "exit"}
|
{"type": "exit"}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Unknown magic names raise `NameError: UsageError: ...` inside the cell.
|
|||||||
`python.kernelMode` controls retained kernel reuse:
|
`python.kernelMode` controls retained kernel reuse:
|
||||||
|
|
||||||
- `session` (default)
|
- `session` (default)
|
||||||
- Reuses kernel sessions keyed by namespaced eval session id plus cwd.
|
- Reuses kernel sessions keyed by namespaced eval session id plus normalized cwd and interpreter.
|
||||||
- Multiple owners can share the same retained kernel for that key.
|
- Multiple owners can share the same retained kernel for that key.
|
||||||
- Calls through the tool are exclusive, so tool invocations do not overlap.
|
- Calls through the tool are exclusive, so tool invocations do not overlap.
|
||||||
- A dead retained subprocess is replaced before execution.
|
- A dead retained subprocess is replaced before execution.
|
||||||
@@ -138,9 +138,9 @@ Environment is filtered before launching the runner:
|
|||||||
- Allow-prefixes: `LC_`, `XDG_`, `PI_`
|
- Allow-prefixes: `LC_`, `XDG_`, `PI_`
|
||||||
- Denylist strips common API keys (OpenAI/Anthropic/Gemini/etc.)
|
- Denylist strips common API keys (OpenAI/Anthropic/Gemini/etc.)
|
||||||
|
|
||||||
Runtime selection order:
|
Runtime selection order (skipped entirely when the `python.interpreter` setting names an explicit executable):
|
||||||
|
|
||||||
1. Active/located venv (`VIRTUAL_ENV`, then `<cwd>/.venv`, `<cwd>/venv`)
|
1. Active/located venv (`VIRTUAL_ENV`, then `CONDA_PREFIX`, then `<cwd>/.venv`, `<cwd>/venv`)
|
||||||
2. Managed venv at `~/.omp/python-env`
|
2. Managed venv at `~/.omp/python-env`
|
||||||
3. `python` or `python3` on PATH
|
3. `python` or `python3` on PATH
|
||||||
|
|
||||||
@@ -156,19 +156,19 @@ The runner additionally receives `PYTHONUNBUFFERED=1` and `PYTHONIOENCODING=utf-
|
|||||||
- JavaScript backend only (`eval.py=false`, `eval.js=true`, or `PI_PY=0 PI_JS=1`)
|
- JavaScript backend only (`eval.py=false`, `eval.js=true`, or `PI_PY=0 PI_JS=1`)
|
||||||
- both backends (`eval.py=true`, `eval.js=true`, or `PI_PY=1 PI_JS=1`)
|
- both backends (`eval.py=true`, `eval.js=true`, or `PI_PY=1 PI_JS=1`)
|
||||||
|
|
||||||
`PI_PY` and `PI_JS` use normal boolean flag parsing. If either env var is set, the env pair overrides the per-key settings; an unset member of the pair defaults to enabled.
|
`PI_PY` and `PI_JS` use normal boolean flag parsing. Each flag, when set, overrides only its own setting; an unset flag falls back to its setting (`eval.py` / `eval.js`, both default `true`).
|
||||||
|
|
||||||
If Python preflight fails and `eval.js` is enabled, `eval` remains available for `js` cells; `py` cells fail with a Python-backend availability error.
|
If Python preflight fails and `eval.js` is enabled, `eval` remains available for `js` cells; `py` cells fail with a Python-backend availability error.
|
||||||
|
|
||||||
Python prelude helpers include `agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None)`. It synchronously calls the host bridge, runs one subagent through the task executor, and returns the final text. When `schema` is supplied, the helper parses the subagent's JSON output and returns the object.
|
Python prelude helpers include `agent(prompt, *, agent_type="task", model=None, label=None, schema=None)`. It synchronously calls the host bridge, runs one subagent through the task executor, and returns the final text. When `schema` is supplied, the helper parses the subagent's JSON output and returns the object.
|
||||||
|
|
||||||
## Execution flow and cancellation/timeout
|
## Execution flow and cancellation/timeout
|
||||||
|
|
||||||
### Cell timeout
|
### Cell timeout
|
||||||
|
|
||||||
Each eval cell `timeout` is in seconds, defaults to 30, and is clamped to `1..600`. It is a **wall-clock budget on the cell's own work** that the watchdog (`IdleTimeout`, `src/eval/idle-timeout.ts`) enforces, **but it is paused while a host-side `agent()`/`parallel()`/`completion()` bridge call is in flight**: those calls pump a heartbeat (`withBridgeHeartbeat`, `src/eval/heartbeat.ts`) that re-arms the watchdog, so a long fanout or a slow completion runs to completion instead of being killed mid-stream.
|
Each eval cell `timeout` is in seconds, defaults to 30, and is clamped to `1..3600`. It is a **wall-clock budget on the cell's own work** that the watchdog (`IdleTimeout`, `src/eval/idle-timeout.ts`) enforces, **but it is suspended while a host-side `agent()`/`parallel()`/`completion()` bridge call is in flight**: those calls emit synthetic pause/resume timeout-control status events (`withBridgeTimeoutPause`, `src/eval/bridge-timeout.ts`) that pause the watchdog entirely and start a fresh timeout window when control returns to the runtime, so a long fanout or a slow completion runs to completion instead of being killed mid-stream. Pause is reference-counted because `parallel()` can have multiple bridge calls in flight at once.
|
||||||
|
|
||||||
The heartbeat is the **sole** signal that extends the budget. Everything else the cell does — compute, `stdout`/`stderr`, `log()`/`phase()`, and ordinary (non-agent) tool calls — counts against `timeout`, so a cell that is not delegating to an agent/completion is bounded by a plain wall-clock timeout. The tool combines the caller abort signal, the session abort signal, and the watchdog's signal with `AbortSignal.any(...)`; no wall-clock deadline is passed to the backend, so neither runtime arms a competing fixed timer.
|
The pause/resume events are the **sole** mechanism that suspends the budget. Everything else the cell does — compute, `stdout`/`stderr`, `log()`/`phase()`, and ordinary (non-agent) tool calls — counts against `timeout`, so a cell that is not delegating to an agent/completion is bounded by a plain wall-clock timeout. The tool combines the caller abort signal, the session abort signal, and the watchdog's signal with `AbortSignal.any(...)`; no wall-clock deadline is passed to the backend, so neither runtime arms a competing fixed timer.
|
||||||
|
|
||||||
### Kernel execution cancellation
|
### Kernel execution cancellation
|
||||||
|
|
||||||
@@ -176,10 +176,10 @@ On abort/timeout:
|
|||||||
|
|
||||||
- The host sends `kill("SIGINT")` to the runner subprocess.
|
- The host sends `kill("SIGINT")` to the runner subprocess.
|
||||||
- The runner's exec-time signal handler raises `KeyboardInterrupt` inside the user code.
|
- The runner's exec-time signal handler raises `KeyboardInterrupt` inside the user code.
|
||||||
- Result includes `cancelled=true`; the timeout path annotates output as `Command timed out after <n> seconds`.
|
- Result includes `cancelled=true`; a kernel timeout is annotated as `eval cell timed out after <n>s; kernel interrupted but remains running. Reset the kernel via { reset: true } if state appears corrupted.`
|
||||||
- Between requests the runner installs `SIG_IGN` for SIGINT so a stray cancel does not tear down the kernel.
|
- Between requests the runner installs `SIG_IGN` for SIGINT so a stray cancel does not tear down the kernel.
|
||||||
|
|
||||||
If a second cancel is required (runner stuck in C code), the host escalates to `SIGTERM` and the session restarts on the next call.
|
If the runner does not emit `done` within 5s of the interrupt (`INTERRUPT_ESCALATION_MS` — e.g. stuck in C code holding the GIL), the host shuts the subprocess down (escalating `exit` → `SIGTERM` → `SIGKILL`), the cell is annotated as kernel-killed, and the kernel is recreated on the next call.
|
||||||
|
|
||||||
### stdin behavior
|
### stdin behavior
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ This document explains how preview/apply workflows are modeled in coding-agent a
|
|||||||
- `action: "discard"` invokes `reject(reason, extra)` if provided; otherwise returns `Discarded: <label>. Reason: <reason>`.
|
- `action: "discard"` invokes `reject(reason, extra)` if provided; otherwise returns `Discarded: <label>. Reason: <reason>`.
|
||||||
- `extra` is optional free-form metadata. Queue handlers receive it; producers decide whether it has meaning.
|
- `extra` is optional free-form metadata. Queue handlers receive it; producers decide whether it has meaning.
|
||||||
|
|
||||||
If no pending action exists, `resolve` fails with:
|
If no pending action exists, `resolve(action="apply")` fails with:
|
||||||
|
|
||||||
- `No pending action to resolve. Nothing to apply or discard.`
|
- `No pending action to resolve. Nothing to apply or discard.`
|
||||||
|
|
||||||
|
`resolve(action="discard")` with no pending action succeeds instead, returning `Nothing to discard; no pending action remains.` — the desired end-state (no staged change) already holds.
|
||||||
|
|
||||||
## Pending actions use the tool-choice queue
|
## Pending actions use the tool-choice queue
|
||||||
|
|
||||||
Preview producers call `queueResolveHandler(...)`, which pushes a one-shot forced `resolve` directive onto the session tool-choice queue and adds a `resolve-reminder` steering message.
|
Preview producers call `queueResolveHandler(...)`, which pushes a one-shot forced `resolve` directive onto the session tool-choice queue and adds a `resolve-reminder` steering message.
|
||||||
|
|||||||
+9
-2
@@ -23,10 +23,10 @@ Behavior notes:
|
|||||||
|
|
||||||
- `@file` CLI arguments are rejected in RPC mode.
|
- `@file` CLI arguments are rejected in RPC mode.
|
||||||
- RPC mode disables automatic session title generation by default to avoid an extra model call.
|
- RPC mode disables automatic session title generation by default to avoid an extra model call.
|
||||||
- RPC mode resets workflow-altering `todo.*`, `task.*`, `async.*`, and `bash.autoBackground.*` settings to their built-in defaults instead of inheriting user overrides.
|
- RPC mode resets workflow-altering `todo.*`, `task.*`, `memory.backend`/`memories.enabled`, `async.*`, and `bash.autoBackground.*` settings to their built-in defaults instead of inheriting user overrides.
|
||||||
- The process reads stdin as JSONL (`readJsonl(Bun.stdin.stream())`).
|
- The process reads stdin as JSONL (`readJsonl(Bun.stdin.stream())`).
|
||||||
- At startup it writes `{ "type": "ready" }` before processing commands.
|
- At startup it writes `{ "type": "ready" }` before processing commands.
|
||||||
- When stdin closes, pending host-tool calls are rejected and the process exits with code `0`.
|
- When stdin closes, pending host-tool calls and host-URI requests are rejected and the process exits with code `0`.
|
||||||
- Responses/events are written as one JSON object per line.
|
- Responses/events are written as one JSON object per line.
|
||||||
|
|
||||||
## Transport and Framing
|
## Transport and Framing
|
||||||
@@ -44,6 +44,9 @@ There is no envelope beyond the object shape itself.
|
|||||||
5. Host tool requests/cancellations (`host_tool_call`, `host_tool_cancel`)
|
5. Host tool requests/cancellations (`host_tool_call`, `host_tool_cancel`)
|
||||||
6. Host URI requests/cancellations (`host_uri_request`, `host_uri_cancel`)
|
6. Host URI requests/cancellations (`host_uri_request`, `host_uri_cancel`)
|
||||||
7. Extension errors (`{ type: "extension_error", extensionPath, event, error }`)
|
7. Extension errors (`{ type: "extension_error", extensionPath, event, error }`)
|
||||||
|
8. Available-commands updates (`{ type: "available_commands_update", commands }`), emitted at startup and whenever command metadata changes
|
||||||
|
9. Subagent frames (`subagent_lifecycle`, `subagent_progress`, `subagent_event`), gated by `set_subagent_subscription`
|
||||||
|
10. Builtin slash-command side channels (`command_output`, `session_info_update`, `config_update`)
|
||||||
|
|
||||||
### Inbound frame categories (stdin)
|
### Inbound frame categories (stdin)
|
||||||
|
|
||||||
@@ -81,9 +84,13 @@ Important edge behavior from runtime:
|
|||||||
### State
|
### State
|
||||||
|
|
||||||
- `{ id?, type: "get_state" }`
|
- `{ id?, type: "get_state" }`
|
||||||
|
- `{ id?, type: "get_available_commands" }`
|
||||||
- `{ id?, type: "set_todos", phases: TodoPhase[] }`
|
- `{ id?, type: "set_todos", phases: TodoPhase[] }`
|
||||||
- `{ id?, type: "set_host_tools", tools: RpcHostToolDefinition[] }`
|
- `{ id?, type: "set_host_tools", tools: RpcHostToolDefinition[] }`
|
||||||
- `{ id?, type: "set_host_uri_schemes", schemes: RpcHostUriSchemeDefinition[] }`
|
- `{ id?, type: "set_host_uri_schemes", schemes: RpcHostUriSchemeDefinition[] }`
|
||||||
|
- `{ id?, type: "set_subagent_subscription", level: "off" | "progress" | "events" }`
|
||||||
|
- `{ id?, type: "get_subagents" }`
|
||||||
|
- `{ id?, type: "get_subagent_messages", subagentId?: string, sessionFile?: string, fromByte?: number }`
|
||||||
|
|
||||||
### Model
|
### Model
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ Consequence: precedence and deduplication are **name-based only**. Two different
|
|||||||
`src/discovery/index.ts` auto-registers providers. For `rules`, current providers are:
|
`src/discovery/index.ts` auto-registers providers. For `rules`, current providers are:
|
||||||
|
|
||||||
- `native` (priority `100`)
|
- `native` (priority `100`)
|
||||||
|
- `omp-plugins` (priority `90`) — `rules/*.{md,mdc}` inside configured extension package roots, normalized via the shared `buildRuleFromMarkdown` path
|
||||||
- `agents` (priority `70`)
|
- `agents` (priority `70`)
|
||||||
- `cursor` (priority `50`)
|
- `cursor` (priority `50`)
|
||||||
- `windsurf` (priority `50`)
|
- `windsurf` (priority `50`)
|
||||||
@@ -98,7 +99,7 @@ Loads from:
|
|||||||
Normalization (`transformMDCRule`):
|
Normalization (`transformMDCRule`):
|
||||||
|
|
||||||
- `description`: kept only if string
|
- `description`: kept only if string
|
||||||
- `alwaysApply`: only `true` is preserved (`false` becomes `undefined`)
|
- `alwaysApply`: normalized to a boolean — `true` only when frontmatter has `alwaysApply: true` (anything else becomes `false`)
|
||||||
- `globs`: accepts array (string elements only) or single string
|
- `globs`: accepts array (string elements only) or single string
|
||||||
- `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` are parsed by shared rule helpers
|
- `condition`/legacy `ttsr_trigger`, `scope`, and `interruptMode` are parsed by shared rule helpers
|
||||||
- `name` from filename without extension
|
- `name` from filename without extension
|
||||||
@@ -137,13 +138,13 @@ All providers use `parseFrontmatter` (`utils/frontmatter.ts`) with these semanti
|
|||||||
2. Body is trimmed after frontmatter extraction.
|
2. Body is trimmed after frontmatter extraction.
|
||||||
3. If YAML parse fails:
|
3. If YAML parse fails:
|
||||||
- warning is logged,
|
- warning is logged,
|
||||||
- parser falls back to simple `key: value` line parsing (`^(\w+):\s*(.*)$`).
|
- parser falls back to simple `key: value` line parsing (`^([\w-]+):\s*(.*)$`).
|
||||||
|
|
||||||
Ambiguity consequences:
|
Ambiguity consequences:
|
||||||
|
|
||||||
- Fallback parser does not support arrays, nested objects, quoting rules, or hyphenated keys.
|
- Fallback parser does not support arrays, nested objects, or quoting rules.
|
||||||
- Fallback values become strings (for example `alwaysApply: true` becomes string `"true"`), so providers requiring boolean/string types may drop metadata.
|
- Fallback values become strings (for example `alwaysApply: true` becomes string `"true"`), so providers requiring boolean/string types may drop metadata.
|
||||||
- `ttsr_trigger` works in fallback (underscore key); keys like `thinking-level` would not.
|
- `ttsr_trigger` works in fallback (underscore key); hyphenated keys like `thinking-level` also parse and are normalized to camelCase (`thinkingLevel`) — key normalization applies to the YAML path too.
|
||||||
- Files without valid frontmatter still load as rules with empty metadata and full content body.
|
- Files without valid frontmatter still load as rules with empty metadata and full content body.
|
||||||
|
|
||||||
## 4. Provider precedence and deduplication
|
## 4. Provider precedence and deduplication
|
||||||
@@ -159,11 +160,12 @@ Ambiguity consequences:
|
|||||||
Effective rule provider order is currently:
|
Effective rule provider order is currently:
|
||||||
|
|
||||||
1. `native` (100)
|
1. `native` (100)
|
||||||
2. `agents` (70)
|
2. `omp-plugins` (90)
|
||||||
3. `cursor` (50)
|
3. `agents` (70)
|
||||||
4. `windsurf` (50)
|
4. `cursor` (50)
|
||||||
5. `cline` (40)
|
5. `windsurf` (50)
|
||||||
6. `builtin-defaults` (1)
|
6. `cline` (40)
|
||||||
|
7. `builtin-defaults` (1)
|
||||||
|
|
||||||
### Intra-provider ordering caveat
|
### Intra-provider ordering caveat
|
||||||
|
|
||||||
@@ -172,6 +174,7 @@ Within a provider, item order comes from `loadFilesFromDir` glob result ordering
|
|||||||
Notable source-order differences:
|
Notable source-order differences:
|
||||||
|
|
||||||
- `native` appends project `.omp/rules`, user `~/.omp/agent/rules`, user `RULES.md`, then nearest project `RULES.md`.
|
- `native` appends project `.omp/rules`, user `~/.omp/agent/rules`, user `RULES.md`, then nearest project `RULES.md`.
|
||||||
|
- `omp-plugins` appends `rules/` results per configured extension package root.
|
||||||
- `agents` appends project-walk `.agent`/`.agents` rule dirs before user home dirs.
|
- `agents` appends project-walk `.agent`/`.agents` rule dirs before user home dirs.
|
||||||
- `cursor` appends user then project results.
|
- `cursor` appends user then project results.
|
||||||
- `windsurf` appends user `global_rules` first, then project rules.
|
- `windsurf` appends user `global_rules` first, then project rules.
|
||||||
@@ -201,13 +204,13 @@ After rule discovery in `createAgentSession` (`sdk.ts`), `bucketRules(...)` appl
|
|||||||
### `description`
|
### `description`
|
||||||
|
|
||||||
- Required for inclusion in rulebook.
|
- Required for inclusion in rulebook.
|
||||||
- Rendered in system prompt `<rules>` block.
|
- Rendered in the system prompt rulebook block (`<domain-rules>` in the default template, `<rules>` in the custom-prompt template).
|
||||||
- Missing description means rule is not available via `rule://` and not listed in system prompt rules.
|
- Missing description keeps the rule out of the rulebook listing; unless it is always-apply or an accepted TTSR rule, it is also not addressable via `rule://`.
|
||||||
|
|
||||||
### `globs`
|
### `globs`
|
||||||
|
|
||||||
- Carried through on `Rule`.
|
- Carried through on `Rule`.
|
||||||
- Rendered as `<glob>...</glob>` entries in the system prompt rules block.
|
- Rendered inline in the default prompt's rulebook listing (`- <name> (<glob>, ...): <description>`); the custom-prompt template renders them as `<glob>...</glob>` entries.
|
||||||
- Exposed in rules UI state (`extensions` mode list).
|
- Exposed in rules UI state (`extensions` mode list).
|
||||||
- Used by TTSR as a global path gate: if a TTSR rule has globs, the match context must include at least one matching file path.
|
- Used by TTSR as a global path gate: if a TTSR rule has globs, the match context must include at least one matching file path.
|
||||||
- Not used to automatically select rulebook rules for `rule://`; rulebook matching remains advisory prompt behavior.
|
- Not used to automatically select rulebook rules for `rule://`; rulebook matching remains advisory prompt behavior.
|
||||||
@@ -231,12 +234,9 @@ After rule discovery in `createAgentSession` (`sdk.ts`), `bucketRules(...)` appl
|
|||||||
|
|
||||||
`buildSystemPromptInternal` receives both `rules` (rulebook) and `alwaysApplyRules`.
|
`buildSystemPromptInternal` receives both `rules` (rulebook) and `alwaysApplyRules`.
|
||||||
|
|
||||||
Always-apply rules are rendered first, injecting their raw content directly into the prompt.
|
Always-apply rules are deduped against custom prompt sources (`dedupeAlwaysApplyRules` drops a rule whose content already appears in the SYSTEM/APPEND_SYSTEM customization) and rendered first, injecting their raw content directly into the prompt (inside a `<generic-rules>` block in the default template).
|
||||||
|
|
||||||
Rulebook rules are rendered in a `# Rules` section with:
|
Rulebook rules are rendered in a `<domain-rules>` block as `- <name> (<globs>): <description>` lines; the URL list in the prompt documents `rule://<name>` and the workflow section tells the model to read relevant rules first. The custom-prompt template (`custom-system-prompt.md`) instead renders `<rule name="...">` entries with `<glob>` children under an explicit "You MUST read `rule://<name>`" instruction.
|
||||||
|
|
||||||
- `Read rule://<name> when working in matching domain`
|
|
||||||
- Each rule's `name`, `description`, and optional `<glob>` list
|
|
||||||
|
|
||||||
This is advisory/contextual: prompt text asks the model to read applicable rules, but code does not enforce glob applicability.
|
This is advisory/contextual: prompt text asks the model to read applicable rules, but code does not enforce glob applicability.
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ Implications:
|
|||||||
|
|
||||||
## 9. Known partial / non-enforced semantics
|
## 9. Known partial / non-enforced semantics
|
||||||
|
|
||||||
1. The rule providers currently loaded for `rules` are `native`, `agents`, `cursor`, `windsurf`, `cline`, and embedded `builtin-defaults`; provider files for other tools may parse other config formats but do not register rule loaders.
|
1. The rule providers currently loaded for `rules` are `native`, `omp-plugins`, `agents`, `cursor`, `windsurf`, `cline`, and embedded `builtin-defaults`; provider files for other tools may parse other config formats but do not register rule loaders.
|
||||||
2. `globs` metadata is surfaced to prompt/UI and is used as a global path gate for TTSR matching, but it is not used to automatically select rulebook rules for `rule://`.
|
2. `globs` metadata is surfaced to prompt/UI and is used as a global path gate for TTSR matching, but it is not used to automatically select rulebook rules for `rule://`.
|
||||||
3. Rule selection for `rule://` includes rulebook, always-apply, and registered TTSR rules (so a triggered TTSR rule can be re-read), but not rules that registered no condition and carry neither a description nor `alwaysApply`.
|
3. Rule selection for `rule://` includes rulebook, always-apply, and registered TTSR rules (so a triggered TTSR rule can be re-read), but not rules that registered no condition and carry neither a description nor `alwaysApply`.
|
||||||
4. Discovery warnings (`loadCapability("rules").warnings`) are produced but `createAgentSession` does not currently surface/log them in this path.
|
4. Discovery warnings (`loadCapability("rules").warnings`) are produced but `createAgentSession` does not currently surface/log them in this path.
|
||||||
|
|||||||
+1
-1
@@ -300,7 +300,7 @@ type CreateAgentSessionResult = {
|
|||||||
modelFallbackMessage?: string;
|
modelFallbackMessage?: string;
|
||||||
lspServers?: Array<{
|
lspServers?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
status: "ready" | "error";
|
status: "connecting" | "ready" | "error" | "available";
|
||||||
fileTypes: string[];
|
fileTypes: string[];
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ This document describes operator-visible behavior for session export/share/fork/
|
|||||||
| `/dump` | Interactive slash command | No | No | Clipboard text |
|
| `/dump` | Interactive slash command | No | No | Clipboard text |
|
||||||
| `/export [path]` | Interactive slash command | No | No | HTML file |
|
| `/export [path]` | Interactive slash command | No | No | HTML file |
|
||||||
| `--export <session.jsonl> [outputPath]` | CLI startup fast-path | No runtime session mutation | No active session; reads target file | HTML file |
|
| `--export <session.jsonl> [outputPath]` | CLI startup fast-path | No runtime session mutation | No active session; reads target file | HTML file |
|
||||||
| `/share` | Interactive slash command | No | No | Temp HTML + share URL/gist |
|
| `/share` | Interactive slash command | No | No | Encrypted share link (gist or share server); temp HTML only for custom handlers |
|
||||||
| `/fresh` | Interactive slash command | Yes (provider-facing in-memory id/state only) | No; keeps current session file/header | None |
|
| `/fresh` | Interactive slash command | Yes (provider-facing in-memory id/state only) | No; keeps current session file/header | None |
|
||||||
| `/fork` | Interactive slash command | Yes (active session identity changes) | Creates new session file and switches current session to it (persistent mode only) | Copies artifact directory to new session namespace when present |
|
| `/fork` | Interactive slash command | Yes (active session identity changes) | Creates new session file and switches current session to it (persistent mode only) | Copies artifact directory to new session namespace when present |
|
||||||
| `--fork <id\|path>` | CLI startup | Yes after session creation | Creates a new session fork from the selected source into current cwd/session dir | None |
|
| `--fork <id\|path>` | CLI startup | Yes after session creation | Creates a new session fork from the selected source into current cwd/session dir | None |
|
||||||
@@ -33,7 +33,7 @@ This document describes operator-visible behavior for session export/share/fork/
|
|||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
|
|
||||||
1. `InputController` routes `/export...` to `CommandController.handleExportCommand`.
|
1. The builtin slash-command registry (`src/slash-commands/builtin-registry.ts`) routes `/export...` to `CommandController.handleExportCommand` in the TUI.
|
||||||
2. The command splits on whitespace and uses only the first argument after `/export` as `outputPath`.
|
2. The command splits on whitespace and uses only the first argument after `/export` as `outputPath`.
|
||||||
3. `AgentSession.exportToHtml()` calls `exportSessionToHtml(sessionManager, state, { outputPath, themeName })`.
|
3. `AgentSession.exportToHtml()` calls `exportSessionToHtml(sessionManager, state, { outputPath, themeName })`.
|
||||||
4. On success, UI shows path and opens the file in browser.
|
4. On success, UI shows path and opens the file in browser.
|
||||||
@@ -84,15 +84,10 @@ No session persistence changes are made by dumping.
|
|||||||
|
|
||||||
## Share
|
## Share
|
||||||
|
|
||||||
`/share` is interactive-only and always starts by exporting current session to a temp HTML file.
|
`/share` publishes an end-to-end encrypted snapshot of the session and prints
|
||||||
|
a viewer link. Implementation: [`../packages/coding-agent/src/export/share.ts`](../packages/coding-agent/src/export/share.ts).
|
||||||
|
|
||||||
### Phase 1: temp export
|
### Phase 1: custom share handler (if present)
|
||||||
|
|
||||||
- Temp file path: `${os.tmpdir()}/${Snowflake.next()}.html`
|
|
||||||
- Uses `session.exportToHtml(tmpFile)`
|
|
||||||
- If export fails (notably in-memory sessions), share ends with error.
|
|
||||||
|
|
||||||
### Phase 2: custom share handler (if present)
|
|
||||||
|
|
||||||
`loadCustomShare()` checks `~/.omp/agent` for first existing candidate:
|
`loadCustomShare()` checks `~/.omp/agent` for first existing candidate:
|
||||||
|
|
||||||
@@ -104,36 +99,57 @@ Requirements:
|
|||||||
|
|
||||||
- Module must default-export a function `(htmlPath) => Promise<CustomShareResult | string | undefined>`.
|
- Module must default-export a function `(htmlPath) => Promise<CustomShareResult | string | undefined>`.
|
||||||
|
|
||||||
If present and valid:
|
If present and valid, the legacy contract is preserved: the session is
|
||||||
|
exported to a temp HTML file (`${os.tmpdir()}/${Snowflake.next()}.html`),
|
||||||
|
the handler receives its path, and the temp file is removed afterwards.
|
||||||
|
Handler result interpretation:
|
||||||
|
|
||||||
- UI enters `Sharing...` loader state.
|
- string => treated as URL, shown and opened
|
||||||
- Handler result interpretation:
|
- object => `url` and/or `message` shown; `url` opened
|
||||||
- string => treated as URL, shown and opened
|
- `undefined`/falsy => generic `Session shared`
|
||||||
- object => `url` and/or `message` shown; `url` opened
|
|
||||||
- `undefined`/falsy => generic `Session shared`
|
|
||||||
- Temp file is removed after completion.
|
|
||||||
|
|
||||||
Critical fallback behavior:
|
Critical fallback behavior:
|
||||||
|
|
||||||
- If custom handler exists but loading fails, command errors and returns.
|
- If custom handler exists but loading fails, command errors and returns.
|
||||||
- If custom handler executes and throws, command errors and returns.
|
- If custom handler executes and throws, command errors and returns.
|
||||||
- In both failure cases, it **does not** fall back to GitHub gist.
|
- In both failure cases, it **does not** fall back to the default flow.
|
||||||
- Gist fallback happens only when no custom share script exists.
|
- The default flow runs only when no custom share script exists.
|
||||||
|
|
||||||
### Phase 3: default gist fallback
|
### Phase 2: default encrypted share
|
||||||
|
|
||||||
Only when no custom share handler is found:
|
Only when no custom share handler is found (`shareSession()`):
|
||||||
|
|
||||||
1. Validates `gh auth status`.
|
1. Builds the session snapshot (`header`, `entries`, `leafId`, plus current
|
||||||
2. Shows `Creating gist...` loader.
|
`systemPrompt` and tool descriptions from agent state).
|
||||||
3. Runs `gh gist create --public=false <tmpFile>`.
|
2. If `share.redactSecrets` is enabled (default) and secrets are configured
|
||||||
4. Parses gist URL, derives gist id, builds preview URL `https://gistpreview.github.io/?<id>`.
|
(`secrets.*`), the secret obfuscator deep-walks every string in the
|
||||||
5. Shows both preview and gist URLs; opens preview.
|
snapshot, replacing configured/discovered secrets with placeholders.
|
||||||
|
3. The JSON is gzipped and sealed with a fresh AES-256-GCM key
|
||||||
|
(`[12B IV][ciphertext+tag]`).
|
||||||
|
4. Upload, first match wins:
|
||||||
|
- **Secret gist** — when `gh` is installed and authenticated, the sealed
|
||||||
|
blob is pushed base64-encoded as `session.ompshare.txt` (budget 5 MB
|
||||||
|
sealed; gist raw fetches cap at 10 MB).
|
||||||
|
- **Share server** — `POST <share.serverUrl>` (default
|
||||||
|
`https://my.omp.sh/s`) with the raw blob, capped at 1 MB. Oversized
|
||||||
|
snapshots are trimmed until they fit: inline images first, then long
|
||||||
|
strings (32 KB → 8 KB → 2 KB → 512 B caps), then oldest entries.
|
||||||
|
5. The link is `<share.serverUrl>/<id>#<base64url key>` in both cases. The
|
||||||
|
viewer page served there fetches the blob (hex ids via the GitHub gist
|
||||||
|
API, anything else from the server's blob store) and decrypts it
|
||||||
|
client-side; the key lives only in the URL fragment and never appears in
|
||||||
|
any HTTP request.
|
||||||
|
|
||||||
|
The UI reports the share URL (plus the underlying gist URL and a truncation
|
||||||
|
note when applicable). Headless `/share` prints the same lines. Unlike
|
||||||
|
`/export`, `/share` works for in-memory (`--no-session`) sessions: the
|
||||||
|
snapshot is built from live entries, no session file required.
|
||||||
|
|
||||||
Cancellation/abort semantics in share:
|
Cancellation/abort semantics in share:
|
||||||
|
|
||||||
- Loader has `onAbort` hook that restores editor UI and reports `Share cancelled`.
|
- Loader has `onAbort` hook that restores editor UI and reports `Share cancelled`.
|
||||||
- The underlying `gh gist create` command is not passed an abort signal in this code path; cancellation is UI-level and checked after command returns.
|
- The upload itself is not aborted mid-flight; cancellation is UI-level and
|
||||||
|
checked after the upload returns.
|
||||||
|
|
||||||
## Fork
|
## Fork
|
||||||
|
|
||||||
@@ -187,21 +203,20 @@ Startup `--fork` is resolved before normal session creation:
|
|||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
|
|
||||||
1. Opens session selector populated via `SessionManager.list(currentCwd, currentSessionDir)`.
|
1. Opens session selector populated via `SessionManager.list(currentCwd, currentSessionDir)`. If the current folder has no sessions, `SessionManager.listAll()` is preloaded and the picker opens directly in all-projects scope.
|
||||||
2. On selection, `SelectorController.handleResumeSession(sessionPath)` calls `session.switchSession(sessionPath)`.
|
2. On selection, `SelectorController.handleResumeSession(sessionPath)` calls `session.switchSession(sessionPath)`.
|
||||||
3. UI clears/rebuilds chat and todos, then reports `Resumed session`.
|
3. UI clears/rebuilds chat and todos, then reports `Resumed session` (or `Resumed session in <dir>` when the resumed session belongs to another project, in which case the process cwd and cwd-derived caches are re-pointed via `applyCwdChange`).
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- This picker only lists sessions in the current session directory scope.
|
- The picker starts in current-folder scope; Tab toggles to all-projects scope (lazily loading `SessionManager.listAll()` on first toggle, cached afterwards).
|
||||||
- It does not use global cross-project search.
|
|
||||||
|
|
||||||
## CLI `--resume`
|
## CLI `--resume`
|
||||||
|
|
||||||
### `--resume` (no value)
|
### `--resume` (no value)
|
||||||
|
|
||||||
- `main.ts` lists sessions for current cwd/sessionDir and opens picker.
|
- `main.ts` lists sessions for current cwd/sessionDir and opens picker. When the current folder is empty, it falls back to `SessionManager.listAll()` and opens the picker in all-projects scope; `No sessions found` is printed only when the global list is also empty.
|
||||||
- Selected path is opened with `SessionManager.open(selectedPath)` before session creation.
|
- Selected path is opened with `SessionManager.open(selectedPath)` before session creation. Selecting a session from another project first switches the process into that project's directory and reloads cwd-scoped settings/caches.
|
||||||
|
|
||||||
### `--resume <value>`
|
### `--resume <value>`
|
||||||
|
|
||||||
@@ -253,6 +268,7 @@ This is startup-only behavior; there is no interactive `/continue` slash command
|
|||||||
12. Restore model (if available in current registry).
|
12. Restore model (if available in current registry).
|
||||||
13. Restore or initialize thinking level and service tier.
|
13. Restore or initialize thinking level and service tier.
|
||||||
14. Reconnect agent event subscription.
|
14. Reconnect agent event subscription.
|
||||||
|
15. Run the registered session-switch reconciler, if any (interactive mode registers `#reconcileModeFromSession()` via `setSessionSwitchReconciler` to re-enter persisted modes such as plan); reconciler errors are logged, not fatal.
|
||||||
|
|
||||||
If any step after the capture fails, `switchSession()` restores the captured state and reconnects the previous agent subscription before rethrowing.
|
If any step after the capture fails, `switchSession()` restores the captured state and reconnects the previous agent subscription before rethrowing.
|
||||||
|
|
||||||
@@ -290,14 +306,14 @@ These callbacks are observational; they do not cancel switch/fork.
|
|||||||
- `/fork` is blocked while streaming (user must wait/abort current response first).
|
- `/fork` is blocked while streaming (user must wait/abort current response first).
|
||||||
- `/resume` selector can be cancelled by user closing selector.
|
- `/resume` selector can be cancelled by user closing selector.
|
||||||
- Cross-project `--resume <id>` can be cancelled by declining fork prompt.
|
- Cross-project `--resume <id>` can be cancelled by declining fork prompt.
|
||||||
- `/share` has UI abort path (`Share cancelled`) for gist flow; it does not wire process-kill semantics for `gh gist create` in this code path.
|
- `/share` has a UI abort path (`Share cancelled`); the upload itself is not killed mid-flight.
|
||||||
|
|
||||||
## Non-persistent (in-memory) session behavior
|
## Non-persistent (in-memory) session behavior
|
||||||
|
|
||||||
When session manager is created with `SessionManager.inMemory()` (`--no-session`):
|
When session manager is created with `SessionManager.inMemory()` (`--no-session`):
|
||||||
|
|
||||||
- Session file path is absent.
|
- Session file path is absent.
|
||||||
- `/export` and `/share` fail with `Cannot export in-memory session to HTML` (propagated to command error UI).
|
- `/export` fails with `Cannot export in-memory session to HTML` (propagated to command error UI). `/share` still works: the snapshot is built from live entries.
|
||||||
- `/fork` fails because `SessionManager.fork()` requires persistence.
|
- `/fork` fails because `SessionManager.fork()` requires persistence.
|
||||||
- `/dump` still works because it serializes in-memory agent state.
|
- `/dump` still works because it serializes in-memory agent state.
|
||||||
- CLI resume/continue semantics are bypassed if `--no-session` is set, because manager creation returns in-memory immediately.
|
- CLI resume/continue semantics are bypassed if `--no-session` is set, because manager creation returns in-memory immediately.
|
||||||
@@ -305,5 +321,5 @@ When session manager is created with `SessionManager.inMemory()` (`--no-session`
|
|||||||
## Known implementation caveats (as of current code)
|
## Known implementation caveats (as of current code)
|
||||||
|
|
||||||
- `SelectorController.handleResumeSession()` does not check the boolean result from `session.switchSession(...)`; a hook-cancelled switch can still proceed through UI "Resumed session" repaint/status path.
|
- `SelectorController.handleResumeSession()` does not check the boolean result from `session.switchSession(...)`; a hook-cancelled switch can still proceed through UI "Resumed session" repaint/status path.
|
||||||
- `/share` custom-share failures do not degrade to default gist fallback; they terminate the command with error.
|
- `/share` custom-share failures do not degrade to the default encrypted share flow; they terminate the command with error.
|
||||||
- `/export` argument tokenization is simplistic and does not preserve quoted paths with spaces.
|
- `/export` argument tokenization is simplistic and does not preserve quoted paths with spaces.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ It focuses on current implementation behavior, including fallback paths and cave
|
|||||||
|
|
||||||
`SessionManager` stores sessions under a cwd-scoped directory by default:
|
`SessionManager` stores sessions under a cwd-scoped directory by default:
|
||||||
|
|
||||||
- `~/.omp/agent/sessions/--<cwd-encoded>--/*.jsonl`
|
- `~/.omp/agent/sessions/<dir-encoded>/*.jsonl` (home-relative `-<rel>` names, `-tmp-<rel>` for temp paths, legacy `--<abs>--` otherwise)
|
||||||
|
|
||||||
`SessionManager.list(cwd, sessionDir?)` reads only that directory unless an explicit `sessionDir` is provided.
|
`SessionManager.list(cwd, sessionDir?)` reads only that directory unless an explicit `sessionDir` is provided.
|
||||||
|
|
||||||
@@ -62,10 +62,10 @@ For `SessionInfo` list entries:
|
|||||||
1. Read terminal-scoped breadcrumb (`~/.omp/agent/terminal-sessions/<terminal-id>`)
|
1. Read terminal-scoped breadcrumb (`~/.omp/agent/terminal-sessions/<terminal-id>`)
|
||||||
2. Validate breadcrumb:
|
2. Validate breadcrumb:
|
||||||
- current terminal can be identified
|
- current terminal can be identified
|
||||||
- breadcrumb cwd matches current cwd (resolved path compare)
|
|
||||||
- referenced file still exists
|
- referenced file still exists
|
||||||
3. If breadcrumb is invalid/missing, fall back to newest file by mtime in the session dir (`findMostRecentSession`)
|
3. If the breadcrumb's cwd differs from the current cwd, that cwd no longer exists (moved/renamed dir), and the current directory has no sessions of its own, the breadcrumb session is re-rooted into the current directory (`SessionManager.open` + `moveTo`) instead of starting fresh
|
||||||
4. If none found, create a new session
|
4. Otherwise, if the breadcrumb cwd matches the current cwd (resolved path compare), use the breadcrumb session; else fall back to newest file by mtime in the session dir (`findMostRecentSession`)
|
||||||
|
5. If none found, create a new session
|
||||||
|
|
||||||
Terminal ID derivation prefers TTY path and falls back to env-based identifiers (`TMUX_PANE`, `CMUX_SURFACE_ID`, `KITTY_WINDOW_ID`, `TERM_SESSION_ID`, `WT_SESSION`).
|
Terminal ID derivation prefers TTY path and falls back to env-based identifiers (`TMUX_PANE`, `CMUX_SURFACE_ID`, `KITTY_WINDOW_ID`, `TERM_SESSION_ID`, `WT_SESSION`).
|
||||||
|
|
||||||
@@ -87,9 +87,11 @@ Breadcrumb writes are best-effort and non-fatal.
|
|||||||
|
|
||||||
Cross-project match behavior:
|
Cross-project match behavior:
|
||||||
|
|
||||||
- if matched session cwd differs from current cwd, CLI prompts whether to fork into current project
|
- if the matched session's recorded cwd no longer exists (moved/renamed dir), CLI prompts `Move (re-root) it into the current directory? [Y/n]`; yes opens the session and `moveTo(cwd)` re-roots it (this also applies to local-scope matches whose recorded cwd is gone)
|
||||||
- yes -> `SessionManager.forkFrom(...)`
|
- otherwise, if a global match's cwd differs from the current cwd, CLI prompts `Fork into current directory? [y/N]`
|
||||||
- no -> throws error (`Session "..." is in another project (...)`)
|
- fork accepted -> `SessionManager.forkFrom(...)`
|
||||||
|
- either prompt declined -> command cancels (`Resume cancelled: session is in another project.`)
|
||||||
|
- non-TTY -> throws `SessionResolutionError` instead of prompting
|
||||||
|
|
||||||
No match -> throws error (`Session "..." not found.`).
|
No match -> throws error (`Session "..." not found.`).
|
||||||
|
|
||||||
@@ -98,10 +100,10 @@ No match -> throws error (`Session "..." not found.`).
|
|||||||
Handled after initial session-manager construction:
|
Handled after initial session-manager construction:
|
||||||
|
|
||||||
1. list local sessions with `SessionManager.list(cwd, parsed.sessionDir)`
|
1. list local sessions with `SessionManager.list(cwd, parsed.sessionDir)`
|
||||||
2. if empty: print `No sessions found` and exit early
|
2. if empty: preload `SessionManager.listAll()` and open the picker in all-projects scope; print `No sessions found` and exit early only when the global list is also empty
|
||||||
3. open TUI picker (`selectSession`)
|
3. open TUI picker (`selectSession`, with optional preloaded `allSessions`/`startInAllScope`)
|
||||||
4. if canceled: print `No session selected` and exit early
|
4. if canceled: print `No session selected` and exit early
|
||||||
5. if selected: `SessionManager.open(selectedPath)`
|
5. if selected: when the session belongs to another project, switch the process into that project's directory (`setProjectDir`, cache resets, settings reload) first; then `SessionManager.open(selected.path)`
|
||||||
|
|
||||||
### `--continue`
|
### `--continue`
|
||||||
|
|
||||||
@@ -111,18 +113,20 @@ Uses `SessionManager.continueRecent(...)` directly (breadcrumb-first behavior ab
|
|||||||
|
|
||||||
## CLI picker (`src/cli/session-picker.ts`)
|
## CLI picker (`src/cli/session-picker.ts`)
|
||||||
|
|
||||||
`selectSession(sessions)` creates a standalone TUI with `SessionSelectorComponent` and resolves exactly once:
|
`selectSession(sessions, { allSessions?, startInAllScope? })` creates a standalone TUI with `SessionSelectorComponent` and resolves exactly once:
|
||||||
|
|
||||||
- selection -> resolves selected path
|
- selection -> resolves selected `SessionInfo` (caller uses `.path` / `.cwd`)
|
||||||
- cancel (Esc) -> resolves `null`
|
- cancel (Esc) -> resolves `null`
|
||||||
- hard exit (Ctrl+C path) -> stops TUI and `process.exit(0)`
|
- hard exit (Ctrl+C path) -> stops TUI and `process.exit(0)`
|
||||||
|
- Tab toggles current-folder / all-projects scope; the all-projects list is loaded lazily via `SessionManager.listAll` (or preloaded via `allSessions`)
|
||||||
|
- search ranking is augmented with prompt-history matches from `history.db` (`HistoryStorage.matchingSessionIds`) when available
|
||||||
|
|
||||||
## Interactive in-session picker (`SelectorController.showSessionSelector`)
|
## Interactive in-session picker (`SelectorController.showSessionSelector`)
|
||||||
|
|
||||||
Flow:
|
Flow:
|
||||||
|
|
||||||
1. fetch sessions from current session dir via `SessionManager.list(currentCwd, currentSessionDir)`
|
1. fetch sessions from current session dir via `SessionManager.list(currentCwd, currentSessionDir)`; if empty, preload `SessionManager.listAll()` and open in all-projects scope
|
||||||
2. mount `SessionSelectorComponent` in editor area using `showSelector(...)`
|
2. mount `SessionSelectorComponent` in editor area using `showSelector(...)`, wired with `loadAllSessions: () => SessionManager.listAll()` and a `history.db` prompt matcher
|
||||||
3. callbacks:
|
3. callbacks:
|
||||||
- select -> close selector and call `handleResumeSession(sessionPath)`
|
- select -> close selector and call `handleResumeSession(sessionPath)`
|
||||||
- cancel -> restore editor and rerender
|
- cancel -> restore editor and rerender
|
||||||
@@ -137,16 +141,15 @@ Flow:
|
|||||||
- Delete to delete after confirmation
|
- Delete to delete after confirmation
|
||||||
- Esc to cancel
|
- Esc to cancel
|
||||||
- Ctrl+C to exit
|
- Ctrl+C to exit
|
||||||
- fuzzy search across session id/title/cwd/first message/all messages/path
|
- Tab to toggle current-folder / all-projects scope
|
||||||
|
- ranked fuzzy search across session id/title/cwd/first message/all messages/path, merged with prompt-history matches from `history.db`
|
||||||
|
|
||||||
Empty-list render behavior:
|
Empty-list render behavior:
|
||||||
|
|
||||||
- renders `No sessions in current folder. Press Tab to view all.`
|
- current-folder scope renders `No sessions in current folder. Press Tab to view all.`; all-projects scope renders `No sessions found`
|
||||||
- Enter/Delete on empty do nothing (no callback)
|
- Enter/Delete on empty do nothing (no callback)
|
||||||
- Esc/Ctrl+C still work
|
- Esc/Ctrl+C still work
|
||||||
|
|
||||||
Caveat: the empty-state UI mentions Tab, but this component currently has no Tab handler and current wiring only lists current-scope sessions.
|
|
||||||
|
|
||||||
## Runtime switch execution (`AgentSession.switchSession`)
|
## Runtime switch execution (`AgentSession.switchSession`)
|
||||||
|
|
||||||
`switchSession(sessionPath)` is the core in-process switch path.
|
`switchSession(sessionPath)` is the core in-process switch path.
|
||||||
@@ -158,8 +161,8 @@ Lifecycle/state transition:
|
|||||||
3. if canceled -> return `false` with no switch
|
3. if canceled -> return `false` with no switch
|
||||||
4. disconnect from current agent event stream
|
4. disconnect from current agent event stream
|
||||||
5. abort active generation/tool flow
|
5. abort active generation/tool flow
|
||||||
6. clear queued steering/follow-up/next-turn message buffers
|
6. flush session writer (`sessionManager.flush()`) to persist pending writes, then capture rollback state
|
||||||
7. flush session writer (`sessionManager.flush()`) to persist pending writes
|
7. clear queued steering/follow-up/next-turn message buffers
|
||||||
8. `sessionManager.setSessionFile(sessionPath)`
|
8. `sessionManager.setSessionFile(sessionPath)`
|
||||||
- updates session file pointer
|
- updates session file pointer
|
||||||
- writes terminal breadcrumb
|
- writes terminal breadcrumb
|
||||||
@@ -171,11 +174,11 @@ Lifecycle/state transition:
|
|||||||
12. emit `session_switch` hook event (`reason: "resume"`, `previousSessionFile`)
|
12. emit `session_switch` hook event (`reason: "resume"`, `previousSessionFile`)
|
||||||
13. replace agent messages with rebuilt context and sync todos
|
13. replace agent messages with rebuilt context and sync todos
|
||||||
14. close provider sessions when switching to a different session or when same-session reload changed replay messages
|
14. close provider sessions when switching to a different session or when same-session reload changed replay messages
|
||||||
15. restore default model from `sessionContext.models.default` if available and present in model registry
|
15. restore model via `getRestorableSessionModels(sessionContext.models, lastModelChangeRole)` — tries the recorded models in fallback order and uses the first one present in the model registry
|
||||||
16. restore thinking level and service tier:
|
16. restore thinking level and service tier:
|
||||||
- thinking uses persisted `thinking_level_change`, otherwise the configured default clamped to model capability
|
- thinking uses persisted `thinking_level_change`, otherwise the configured default clamped to model capability
|
||||||
- service tier uses persisted `service_tier_change`, otherwise the configured `serviceTier` setting (`"none"` becomes unset)
|
- service tier uses persisted `service_tier_change`, otherwise the configured `serviceTier` setting (`"none"` becomes unset)
|
||||||
17. reconnect agent listeners and return `true`
|
17. reconnect agent listeners, run the registered session-switch reconciler if any (interactive mode re-enters persisted modes; errors logged, not fatal), and return `true`
|
||||||
|
|
||||||
## UI state rebuild after interactive switch
|
## UI state rebuild after interactive switch
|
||||||
|
|
||||||
@@ -186,9 +189,10 @@ Lifecycle/state transition:
|
|||||||
- clear pending-message UI and pending tool map
|
- clear pending-message UI and pending tool map
|
||||||
- reset streaming component/message references
|
- reset streaming component/message references
|
||||||
- call `session.switchSession(...)`
|
- call `session.switchSession(...)`
|
||||||
|
- if the resumed session's cwd differs from the previous one, re-point the process and cwd-derived caches at it (`applyCwdChange`)
|
||||||
- clear chat container and rerender from session context (`renderInitialMessages`)
|
- clear chat container and rerender from session context (`renderInitialMessages`)
|
||||||
- reload todos from new session artifacts
|
- reload todos from new session artifacts
|
||||||
- show `Resumed session`
|
- show `Resumed session` (or `Resumed session in <dir>` for a cross-project resume)
|
||||||
|
|
||||||
So visible conversation/todo state is rebuilt from the new session file.
|
So visible conversation/todo state is rebuilt from the new session file.
|
||||||
|
|
||||||
@@ -200,7 +204,7 @@ So visible conversation/todo state is rebuilt from the new session file.
|
|||||||
- `sdk.ts` builds `existingSession = sessionManager.buildSessionContext()`.
|
- `sdk.ts` builds `existingSession = sessionManager.buildSessionContext()`.
|
||||||
- Agent messages are restored once during session creation.
|
- Agent messages are restored once during session creation.
|
||||||
- Model/thinking are selected during creation (including restore/fallback logic).
|
- Model/thinking are selected during creation (including restore/fallback logic).
|
||||||
- Interactive mode then runs `#restoreModeFromSession()` to re-enter persisted mode state (currently plan/plan_paused).
|
- Interactive mode then runs `#reconcileModeFromSession()` to re-enter persisted mode state (e.g. plan mode).
|
||||||
|
|
||||||
### In-session switch (`/resume`-style selector path)
|
### In-session switch (`/resume`-style selector path)
|
||||||
|
|
||||||
@@ -208,7 +212,7 @@ So visible conversation/todo state is rebuilt from the new session file.
|
|||||||
- Messages/model/thinking are rebuilt immediately in place.
|
- Messages/model/thinking are rebuilt immediately in place.
|
||||||
- Hook `session_before_switch`/`session_switch` events are emitted.
|
- Hook `session_before_switch`/`session_switch` events are emitted.
|
||||||
- UI chat/todos are refreshed.
|
- UI chat/todos are refreshed.
|
||||||
- No dedicated post-switch mode restore call is made in selector flow; mode re-entry behavior is not symmetric with startup `#restoreModeFromSession()`.
|
- Mode re-entry is symmetric with startup: interactive mode registers `#reconcileModeFromSession()` as the session-switch reconciler (`setSessionSwitchReconciler`), and `switchSession()` invokes it after reconnecting.
|
||||||
|
|
||||||
## Failure and edge-case behavior
|
## Failure and edge-case behavior
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ Key files:
|
|||||||
- `src/session/agent-session.ts` — `/tree` navigation flow, summarization, hook/event emission
|
- `src/session/agent-session.ts` — `/tree` navigation flow, summarization, hook/event emission
|
||||||
- `src/modes/components/tree-selector.ts` — interactive tree UI behavior and filtering
|
- `src/modes/components/tree-selector.ts` — interactive tree UI behavior and filtering
|
||||||
- `src/modes/controllers/selector-controller.ts` — selector orchestration for `/tree` and `/branch`
|
- `src/modes/controllers/selector-controller.ts` — selector orchestration for `/tree` and `/branch`
|
||||||
- `src/modes/controllers/input-controller.ts` — command routing (`/tree`, `/branch`, double-escape behavior)
|
- `src/slash-commands/builtin-registry.ts` — command routing (`/tree`, `/branch`)
|
||||||
|
- `src/modes/controllers/input-controller.ts` — double-escape behavior and `app.session.tree`/`app.session.fork` keybinding wiring
|
||||||
- `src/session/messages.ts` — conversion of `branch_summary`, `compaction`, and `custom_message` entries into LLM context messages
|
- `src/session/messages.ts` — conversion of `branch_summary`, `compaction`, and `custom_message` entries into LLM context messages
|
||||||
|
|
||||||
## Tree data model in `SessionManager`
|
## Tree data model in `SessionManager`
|
||||||
@@ -118,7 +119,7 @@ User-facing `/branch` flow (`SelectorController.showUserMessageSelector` → `Ag
|
|||||||
`session/messages.ts` then maps these message types for model input:
|
`session/messages.ts` then maps these message types for model input:
|
||||||
|
|
||||||
- `branchSummary` and `compactionSummary` become user-role templated context messages
|
- `branchSummary` and `compactionSummary` become user-role templated context messages
|
||||||
- `custom`/`hookMessage` become user-role content messages
|
- `custom`/`hookMessage` become developer-role content messages (via agent-core's `convertMessageToLlm`)
|
||||||
|
|
||||||
So tree movement changes context by changing the active leaf path, not by mutating old entries.
|
So tree movement changes context by changing the active leaf path, not by mutating old entries.
|
||||||
|
|
||||||
|
|||||||
+14
-8
@@ -28,10 +28,16 @@ Does not cover `/tree` UI rendering behavior beyond semantics that affect sessio
|
|||||||
Default session file location:
|
Default session file location:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/.omp/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl
|
~/.omp/agent/sessions/<dir-encoded>/<timestamp>_<sessionId>.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
`<cwd-encoded>` is derived from the working directory by stripping leading slash and replacing `/`, `\\`, and `:` with `-`.
|
`<dir-encoded>` depends on where the canonicalized cwd lives:
|
||||||
|
|
||||||
|
- inside the home directory: `-<relative-path>` with `/`, `\\`, and `:` replaced by `-` (bare `-` for home itself)
|
||||||
|
- inside the OS temp root: `-tmp-<relative-path>` with the same replacement
|
||||||
|
- anywhere else: legacy absolute form `--<cwd-without-leading-slash-with-same-replacement>--`
|
||||||
|
|
||||||
|
Old `--<home-encoded>-*--` directories are migrated to the new home-relative names once per sessions root on first access (best-effort).
|
||||||
|
|
||||||
Blob store location:
|
Blob store location:
|
||||||
|
|
||||||
@@ -370,7 +376,7 @@ The underlying model is append-only tree + mutable leaf pointer:
|
|||||||
|
|
||||||
## Context Reconstruction (`buildSessionContext`)
|
## Context Reconstruction (`buildSessionContext`)
|
||||||
|
|
||||||
`buildSessionContext(entries, leafId, byId?)` resolves what is sent to the model.
|
`buildSessionContext(entries, leafId?, byId?, options?)` resolves what is sent to the model. Passing `options.transcript: true` instead builds the full-history display transcript (compactions emitted inline at the position they fired) — display-only, never sent to a provider.
|
||||||
|
|
||||||
Algorithm:
|
Algorithm:
|
||||||
|
|
||||||
@@ -421,7 +427,7 @@ Rationale in code: avoid persisting sessions that never produced an assistant re
|
|||||||
|
|
||||||
- `flush()` flushes writer and calls `fsync()`.
|
- `flush()` flushes writer and calls `fsync()`.
|
||||||
- Atomic full rewrites (`#rewriteFile`) write to temp file, flush+fsync, close, then rename over target.
|
- Atomic full rewrites (`#rewriteFile`) write to temp file, flush+fsync, close, then rename over target.
|
||||||
- Used for migrations, `setSessionName`, `rewriteEntries`, move operations, and tool-call arg rewrites.
|
- Used for migrations, `setSessionName`, `rewriteEntries` (tool-output pruning/supersede passes), and move/fork operations.
|
||||||
|
|
||||||
### Error behavior
|
### Error behavior
|
||||||
|
|
||||||
@@ -448,14 +454,14 @@ On load, blob refs are resolved back to base64 for message/custom_message image
|
|||||||
`SessionStorage` interface provides all filesystem operations used by `SessionManager`:
|
`SessionStorage` interface provides all filesystem operations used by `SessionManager`:
|
||||||
|
|
||||||
- sync: `ensureDirSync`, `existsSync`, `writeTextSync`, `statSync`, `listFilesSync`
|
- sync: `ensureDirSync`, `existsSync`, `writeTextSync`, `statSync`, `listFilesSync`
|
||||||
- async: `exists`, `readText`, `readTextSlices`, `writeText`, `rename`, `unlink`, `openWriter`
|
- async: `exists`, `readText`, `readTextSlices`, `writeText`, `rename`, `unlink`, `deleteSessionWithArtifacts`, `openWriter`
|
||||||
|
|
||||||
Implementations:
|
Implementations:
|
||||||
|
|
||||||
- `FileSessionStorage`: real filesystem (Bun + node fs)
|
- `FileSessionStorage`: real filesystem (Bun + node fs)
|
||||||
- `MemorySessionStorage`: map-backed in-memory implementation for tests/non-persistent sessions
|
- `MemorySessionStorage`: map-backed in-memory implementation for tests/non-persistent sessions
|
||||||
|
|
||||||
`SessionStorageWriter` exposes `writeLine`, `flush`, `fsync`, `close`, `getError`.
|
`SessionStorageWriter` exposes `writeLine`, `writeLineSync`, `flush`, `fsync`, `close`, `getError`.
|
||||||
|
|
||||||
## Session Discovery Utilities
|
## Session Discovery Utilities
|
||||||
|
|
||||||
@@ -474,9 +480,9 @@ Metadata extraction for `getRecentSessions` reads a prefix via `readTextSlices(.
|
|||||||
`HistoryStorage` (`history-storage.ts`) is a separate SQLite subsystem for prompt recall/search, not session replay.
|
`HistoryStorage` (`history-storage.ts`) is a separate SQLite subsystem for prompt recall/search, not session replay.
|
||||||
|
|
||||||
- DB: `~/.omp/agent/history.db`
|
- DB: `~/.omp/agent/history.db`
|
||||||
- Table: `history(id, prompt, created_at, cwd)`
|
- Table: `history(id, prompt, created_at, cwd, session_id)`
|
||||||
- FTS5 index: `history_fts` with trigger-maintained sync
|
- FTS5 index: `history_fts` with trigger-maintained sync
|
||||||
- Deduplicates consecutive identical prompts using in-memory last-prompt cache
|
- Deduplicates consecutive identical prompts using in-memory last-prompt cache
|
||||||
- Async insertion (`setImmediate`) so prompt capture does not block turn execution
|
- Inserts are batched through an async drain queue (~100 ms delay) so prompt capture does not block turn execution
|
||||||
|
|
||||||
Use session files for conversation graph/state replay; use `HistoryStorage` for prompt history UX.
|
Use session files for conversation graph/state replay; use `HistoryStorage` for prompt history UX.
|
||||||
|
|||||||
+11
-9
@@ -56,6 +56,7 @@ Supported frontmatter fields on the skill type:
|
|||||||
- `globs?: string[]`
|
- `globs?: string[]`
|
||||||
- `alwaysApply?: boolean`
|
- `alwaysApply?: boolean`
|
||||||
- `hide?: boolean`
|
- `hide?: boolean`
|
||||||
|
- `disableModelInvocation?: boolean` (Agent Skills equivalent of `hide`; normalized from kebab-case `disable-model-invocation`)
|
||||||
- additional keys are preserved as unknown metadata
|
- additional keys are preserved as unknown metadata
|
||||||
|
|
||||||
Current runtime behavior:
|
Current runtime behavior:
|
||||||
@@ -63,12 +64,13 @@ Current runtime behavior:
|
|||||||
- `name` defaults to the skill directory name
|
- `name` defaults to the skill directory name
|
||||||
- `description` is required for:
|
- `description` is required for:
|
||||||
- native `.omp` provider skill discovery (`requireDescription: true`)
|
- native `.omp` provider skill discovery (`requireDescription: true`)
|
||||||
|
- `omp-plugins` extension-package skills and the `github` provider (`.github/skills/`), which also pass `requireDescription: true`
|
||||||
- `skills.customDirectories` scans via `scanSkillsFromDir` in `src/discovery/helpers.ts` (non-recursive)
|
- `skills.customDirectories` scans via `scanSkillsFromDir` in `src/discovery/helpers.ts` (non-recursive)
|
||||||
- non-native providers can load skills without description
|
- the claude/codex/agents/opencode/claude-plugins providers can load skills without description
|
||||||
|
|
||||||
## Discovery pipeline
|
## Discovery pipeline
|
||||||
|
|
||||||
`discoverSkills()` in `src/extensibility/skills.ts` does two passes:
|
`loadSkills()` in `src/extensibility/skills.ts` does two passes:
|
||||||
|
|
||||||
1. **Capability providers** via `loadCapability("skills")`
|
1. **Capability providers** via `loadCapability("skills")`
|
||||||
2. **Custom directories** via `scanSkillsFromDir(..., { requireDescription: true })` (one-level directory enumeration)
|
2. **Custom directories** via `scanSkillsFromDir(..., { requireDescription: true })` (one-level directory enumeration)
|
||||||
@@ -82,7 +84,7 @@ Provider ordering is priority-first (higher wins), then registration order for t
|
|||||||
Current registered skill providers:
|
Current registered skill providers:
|
||||||
|
|
||||||
1. `native` (priority 100) — `.omp` user/project skills via `src/discovery/builtin.ts`
|
1. `native` (priority 100) — `.omp` user/project skills via `src/discovery/builtin.ts`
|
||||||
2. `omp-plugins` (priority 90) — `skills/` bundled next to extension packages loaded through `extensions:` or `--extension`/`-e`
|
2. `omp-plugins` (priority 90) — `skills/` bundled next to extension packages loaded through `extensions:`, `--extension`/`-e`, or installed plugins under `~/.omp/plugins/node_modules`
|
||||||
3. `claude` (priority 80)
|
3. `claude` (priority 80)
|
||||||
4. priority 70 group (in registration order):
|
4. priority 70 group (in registration order):
|
||||||
- `claude-plugins`
|
- `claude-plugins`
|
||||||
@@ -95,12 +97,12 @@ Dedup key is skill name. First item with a given name wins.
|
|||||||
|
|
||||||
### Source toggles and filtering
|
### Source toggles and filtering
|
||||||
|
|
||||||
`discoverSkills()` applies these controls:
|
`loadSkills()` applies these controls:
|
||||||
|
|
||||||
- source toggles: `enableCodexUser`, `enableClaudeUser`, `enableClaudeProject`, `enablePiUser`, `enablePiProject`
|
- source toggles: `enableCodexUser`, `enableClaudeUser`, `enableClaudeProject`, `enablePiUser`, `enablePiProject`
|
||||||
- `disabledExtensions` entries with `skill:<name>`
|
- `disabledExtensions` entries with `skill:<name>`
|
||||||
- `ignoredSkills` (exclude)
|
- `ignoredSkills` (exclude; glob patterns)
|
||||||
- `includeSkills` (include allowlist; empty means include all)
|
- `includeSkills` (include allowlist; glob patterns; empty means include all)
|
||||||
|
|
||||||
Filter order is:
|
Filter order is:
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ Filter order is:
|
|||||||
- `extensibility/skills.ts` additionally:
|
- `extensibility/skills.ts` additionally:
|
||||||
- de-duplicates identical files by `realpath` (symlink-safe)
|
- de-duplicates identical files by `realpath` (symlink-safe)
|
||||||
- emits collision warnings when a later skill name conflicts
|
- emits collision warnings when a later skill name conflicts
|
||||||
- keeps the convenience `discoverSkillsFromDir({ dir, source })` API as a thin adapter over `scanSkillsFromDir`
|
- keeps the convenience `loadSkillsFromDir({ dir, source })` API as a thin adapter over `scanSkillsFromDir`
|
||||||
- Custom-directory skills are merged after provider skills and follow the same collision behavior
|
- Custom-directory skills are merged after provider skills and follow the same collision behavior
|
||||||
|
|
||||||
## Runtime usage behavior
|
## Runtime usage behavior
|
||||||
@@ -148,7 +150,7 @@ If `skills.enableSkillCommands` is true, interactive mode registers one slash co
|
|||||||
- **Ctrl+Enter** (`app.message.followUp`) → invokes the skill on the `followUp` queue while streaming, or as a normal idle prompt when the agent is not streaming
|
- **Ctrl+Enter** (`app.message.followUp`) → invokes the skill on the `followUp` queue while streaming, or as a normal idle prompt when the agent is not streaming
|
||||||
- appends metadata (`Skill: <path>`, optional `User: <args>`)
|
- appends metadata (`Skill: <path>`, optional `User: <args>`)
|
||||||
|
|
||||||
There is no flag, mode-selector, or frontmatter knob to override this — the keybinding _is_ the choice, identical to how free text is routed during streaming (`input-controller.ts:243-249` for Enter, `input-controller.ts:462-500` for Ctrl+Enter; both dispatch through `#invokeSkillCommand`).
|
There is no flag, mode-selector, or frontmatter knob to override this — the keybinding _is_ the choice, identical to how free text is routed during streaming (`input-controller.ts:436-442` for Enter, `input-controller.ts:770-775` for Ctrl+Enter; both dispatch through `#invokeSkillCommand`).
|
||||||
|
|
||||||
## `skill://` URL behavior
|
## `skill://` URL behavior
|
||||||
|
|
||||||
@@ -195,7 +197,7 @@ No fallback search is performed for missing assets.
|
|||||||
- **Skills**: named, optional capability packs selected by task context or explicitly requested
|
- **Skills**: named, optional capability packs selected by task context or explicitly requested
|
||||||
- **AGENTS.md/context files**: persistent instruction files loaded as context-file capability and merged by level/depth rules
|
- **AGENTS.md/context files**: persistent instruction files loaded as context-file capability and merged by level/depth rules
|
||||||
|
|
||||||
`src/discovery/agents-md.ts` specifically walks ancestor directories from `cwd` to discover standalone `AGENTS.md` files (up to depth 20), excluding hidden-directory segments.
|
`src/discovery/agents-md.ts` specifically walks ancestor directories from `cwd` to discover standalone `AGENTS.md` files (stopping at the repo root, or home when no repo root is known), skipping files whose containing directory name starts with a dot.
|
||||||
|
|
||||||
### Skills vs slash commands
|
### Skills vs slash commands
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ omp loads extension modules from these sources:
|
|||||||
- `<cwd>/.omp/extensions/`
|
- `<cwd>/.omp/extensions/`
|
||||||
- `~/.omp/agent/extensions/`
|
- `~/.omp/agent/extensions/`
|
||||||
- legacy extension paths listed in `.omp/settings.json#extensions` or `~/.omp/agent/settings.json#extensions`
|
- legacy extension paths listed in `.omp/settings.json#extensions` or `~/.omp/agent/settings.json#extensions`
|
||||||
2. Marketplace-installed plugins from the OMP and Claude plugin registries.
|
2. Installed plugins under `~/.omp/plugins/node_modules` (`omp plugin install` npm/git specs, or `omp plugin link`) via their `omp.extensions`/`pi.extensions` manifests. Marketplace cache installs do not feed extension modules — they surface skills/commands/hooks/tools/MCP only.
|
||||||
3. Explicit configured paths passed by the CLI (`omp --extension ./my-ext.ts`, also `-e`; `--hook` is treated as an alias) and by the `extensions:` setting in config.
|
3. Explicit configured paths passed by the CLI (`omp --extension ./my-ext.ts`, also `-e`; `--hook` is treated as an alias) and by the `extensions:` setting in config.
|
||||||
|
|
||||||
The runtime de-duplicates by resolved absolute path — first seen wins.
|
The runtime de-duplicates by resolved absolute path — first seen wins.
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ my-marketplace/
|
|||||||
marketplace.json
|
marketplace.json
|
||||||
plugins/
|
plugins/
|
||||||
my-plugin/
|
my-plugin/
|
||||||
package.json
|
skills/
|
||||||
index.ts
|
my-skill/
|
||||||
|
SKILL.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -191,26 +192,21 @@ Declares the plugin as an npm package. `version` is optional:
|
|||||||
|
|
||||||
## Plugin structure
|
## Plugin structure
|
||||||
|
|
||||||
Each plugin directory (regardless of source type) should contain:
|
A plugin directory (regardless of source type) ships its content in conventional locations, all optional:
|
||||||
|
|
||||||
```
|
```
|
||||||
my-plugin/
|
my-plugin/
|
||||||
package.json ← required: declares omp.extensions entry points
|
skills/<name>/SKILL.md ← skills
|
||||||
src/
|
commands/*.md ← slash commands
|
||||||
main.ts ← extension factory
|
agents/*.md ← subagent definitions
|
||||||
README.md ← recommended: description + usage
|
hooks/pre/, hooks/post/ ← hooks
|
||||||
|
tools/ ← custom tools
|
||||||
|
.mcp.json ← MCP server definitions
|
||||||
|
package.json ← optional; its version is a fallback when the catalog entry has no version
|
||||||
|
README.md ← recommended: description + usage
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimum `package.json`:
|
> Note: extension modules declared via `package.json` `omp.extensions` are **not** loaded from marketplace installs — that mechanism only applies to npm-installed or `omp plugin link`ed plugins. Ship marketplace plugin behavior through the conventional directories above.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "my-plugin",
|
|
||||||
"omp": {
|
|
||||||
"extensions": ["./src/main.ts"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install command
|
## Install command
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ The capability registry loads all registered providers, sorted by provider prior
|
|||||||
Current slash-command providers and priorities:
|
Current slash-command providers and priorities:
|
||||||
|
|
||||||
1. `native` (OMP) — priority `100`
|
1. `native` (OMP) — priority `100`
|
||||||
2. `claude` — priority `80`
|
2. `omp-plugins` (extension packages) — priority `90`
|
||||||
3. `claude-plugins` — priority `70`
|
3. `claude` — priority `80`
|
||||||
4. `codex` — priority `70`
|
4. `claude-plugins` — priority `70`
|
||||||
5. `opencode` — priority `55`
|
5. `agents` (`.agent`/`.agents` standard dirs) — priority `70`
|
||||||
|
6. `codex` — priority `70`
|
||||||
|
7. `opencode` — priority `55`
|
||||||
|
|
||||||
Tie behavior: equal-priority providers keep registration order. Current import order registers `claude-plugins` before `codex`, so plugin commands win over codex commands on name collisions.
|
Tie behavior: equal-priority providers keep registration order. Current import order registers `claude-plugins` before `agents` before `codex`, so plugin commands win over both on name collisions.
|
||||||
|
|
||||||
### Name-collision behavior
|
### Name-collision behavior
|
||||||
|
|
||||||
@@ -70,8 +72,10 @@ Search roots come from `.omp` directories:
|
|||||||
|
|
||||||
Loads, subject to `commands.enableClaudeUser` and `commands.enableClaudeProject` settings:
|
Loads, subject to `commands.enableClaudeUser` and `commands.enableClaudeProject` settings:
|
||||||
|
|
||||||
- user: `~/.claude/commands/*.md`
|
- user: `~/.claude/commands/**/*.md` (recursive)
|
||||||
- project: `<cwd>/.claude/commands/*.md`
|
- project: `<cwd>/.claude/commands/**/*.md` (recursive)
|
||||||
|
|
||||||
|
Commands in subdirectories additionally get a namespaced alias: `foo/bar.md` is registered under both `bar` and `foo:bar` (`addClaudeCommandNamespaceAliases`).
|
||||||
|
|
||||||
The provider pushes user items before project items, so **user Claude commands beat project Claude commands** on same-name collisions inside this provider.
|
The provider pushes user items before project items, so **user Claude commands beat project Claude commands** on same-name collisions inside this provider.
|
||||||
|
|
||||||
@@ -97,7 +101,7 @@ Both sides are loaded then flattened in user-first order, so **user OpenCode com
|
|||||||
|
|
||||||
## `claude-plugins` provider (`claude-plugins.ts`)
|
## `claude-plugins` provider (`claude-plugins.ts`)
|
||||||
|
|
||||||
Loads plugin command roots from `~/.claude/plugins/installed_plugins.json`, then scans `<pluginRoot>/commands/*.md`.
|
Loads plugin command roots via `listClaudePluginRoots(...)`, which reads `~/.claude/plugins/installed_plugins.json`, `~/.omp/plugins/installed_plugins.json`, and the nearest project-scoped registry resolved from cwd. For each root it scans `<pluginRoot>/commands/*.md` (the directory can be remapped by plugin config keys `commands`/`slash-commands`), and command names are prefixed with the plugin name: `<plugin>:<command>`.
|
||||||
|
|
||||||
Ordering follows registry iteration order and per-plugin entry order from that JSON data. There is no additional sort step.
|
Ordering follows registry iteration order and per-plugin entry order from that JSON data. There is no additional sort step.
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ For each command:
|
|||||||
1. parse frontmatter/body (`parseFrontmatter`)
|
1. parse frontmatter/body (`parseFrontmatter`)
|
||||||
2. description source:
|
2. description source:
|
||||||
- `frontmatter.description` if present
|
- `frontmatter.description` if present
|
||||||
- else first non-empty body line (trimmed, max 60 chars with `...`)
|
- else first non-empty body line (max 60 chars with `...`)
|
||||||
3. keep parsed body as executable template content
|
3. keep parsed body as executable template content
|
||||||
4. compute a display source string like `via Claude Code Project`
|
4. compute a display source string like `via Claude Code Project`
|
||||||
|
|
||||||
@@ -136,7 +140,7 @@ At construction time it builds a pending command list from:
|
|||||||
- TypeScript custom commands (`session.customCommands`), mapped to slash command labels
|
- TypeScript custom commands (`session.customCommands`), mapped to slash command labels
|
||||||
- optional skill commands (`/skill:<name>`) when `skills.enableSkillCommands` is enabled
|
- optional skill commands (`/skill:<name>`) when `skills.enableSkillCommands` is enabled
|
||||||
|
|
||||||
Then `init()` calls `refreshSlashCommandState(...)` to load file-based commands and install one `CombinedAutocompleteProvider` containing:
|
Then `init()` calls `refreshSlashCommandState(...)` to load file-based commands and install one autocomplete provider (`createPromptActionAutocompleteProvider`, a `PromptActionAutocompleteProvider` wrapping a `CombinedAutocompleteProvider`) containing:
|
||||||
|
|
||||||
- pending commands above
|
- pending commands above
|
||||||
- discovered file-based commands
|
- discovered file-based commands
|
||||||
@@ -148,7 +152,8 @@ Then `init()` calls `refreshSlashCommandState(...)` to load file-based commands
|
|||||||
Slash command state is refreshed:
|
Slash command state is refreshed:
|
||||||
|
|
||||||
- during interactive init
|
- during interactive init
|
||||||
- after `/move` changes working directory (`handleMoveCommand` calls `resetCapabilities()` then `refreshSlashCommandState(newCwd)`)
|
- after `/move` changes working directory (`handleMoveCommand` -> `applyCwdChange`, which calls `resetCapabilities()` then `refreshSlashCommandState(newCwd)`)
|
||||||
|
- when the editor component is swapped (`setEditorComponent` re-runs `refreshSlashCommandState()`)
|
||||||
|
|
||||||
There is no continuous file watcher for command directories.
|
There is no continuous file watcher for command directories.
|
||||||
|
|
||||||
@@ -162,7 +167,7 @@ The Extensions dashboard also loads `slash-commands` capability and displays act
|
|||||||
|
|
||||||
1. **Extension commands** (`#tryExecuteExtensionCommand`)
|
1. **Extension commands** (`#tryExecuteExtensionCommand`)
|
||||||
If `/name` matches extension-registered command, handler executes immediately and prompt returns.
|
If `/name` matches extension-registered command, handler executes immediately and prompt returns.
|
||||||
2. **TypeScript custom commands** (`#tryExecuteCustomCommand`)
|
2. **TypeScript custom commands and MCP prompt commands** (`#tryExecuteCustomCommand`)
|
||||||
Boundary only: if matched, it executes and may return:
|
Boundary only: if matched, it executes and may return:
|
||||||
- `string` -> replace prompt text with that string
|
- `string` -> replace prompt text with that string
|
||||||
- `void/undefined` -> treated as handled; no LLM prompt
|
- `void/undefined` -> treated as handled; no LLM prompt
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Bundled agents are embedded at build time (`src/task/agents.ts`) using text impo
|
|||||||
|
|
||||||
`EMBEDDED_AGENT_DEFS` defines:
|
`EMBEDDED_AGENT_DEFS` defines:
|
||||||
|
|
||||||
- `explore`, `plan`, `designer`, `reviewer` from prompt files
|
- `explore`, `plan`, `designer`, `reviewer`, `librarian`, `oracle` from prompt files
|
||||||
- `task` and `quick_task` from shared `task.md` body plus injected frontmatter
|
- `task` and `quick_task` from shared `task.md` body plus injected frontmatter
|
||||||
|
|
||||||
Loading path:
|
Loading path:
|
||||||
@@ -56,36 +56,21 @@ Because bundled parsing uses `level: "fatal"`, malformed bundled frontmatter thr
|
|||||||
|
|
||||||
## Filesystem and plugin discovery
|
## Filesystem and plugin discovery
|
||||||
|
|
||||||
`discoverAgents(cwd, home)` (`src/task/discovery.ts`) merges agents from multiple places before appending bundled definitions.
|
`discoverAgents(cwd, home)` (`src/task/discovery.ts`) merges agents from OMP-native roots and Claude plugin roots before appending bundled definitions. Cross-harness roots such as `.claude/agents`, `.codex/agents`, and `.gemini/agents` are intentionally skipped — their frontmatter schema is not the OMP task-agent contract (`TASK_AGENT_CONFIG_SOURCE = ".omp"` filters both dir lists).
|
||||||
|
|
||||||
### Discovery inputs
|
### Discovery inputs
|
||||||
|
|
||||||
1. User config agent dirs from `getConfigDirs("agents", { project: false })`
|
1. Nearest project `.omp` agents dir from `findAllNearestProjectConfigDirs("agents", cwd)` (filtered to `.omp`; first hit only)
|
||||||
2. Nearest project agent dirs from `findAllNearestProjectConfigDirs("agents", cwd)`
|
2. User `.omp` agents dir from `getConfigDirs("agents", { project: false })` (filtered to `.omp`; first hit only)
|
||||||
3. Claude plugin roots (`listClaudePluginRoots(home)`) with `agents/` subdirs
|
3. Claude plugin roots (`listClaudePluginRoots(home, cwd)`) with `agents/` subdirs — only when `isProviderEnabled("claude-plugins")`; project-scope plugins sort before user-scope
|
||||||
4. Bundled agents (`loadBundledAgents()`)
|
4. Bundled agents (`loadBundledAgents()`)
|
||||||
|
|
||||||
### Actual source order
|
### Actual source order
|
||||||
|
|
||||||
Source-family order comes from `getConfigDirs("", { project: false })`, which is derived from `priorityList` in `src/config.ts`:
|
1. project `.omp/agents`
|
||||||
|
2. user `~/.omp/agent/agents`
|
||||||
1. `.omp`
|
3. plugin `agents/` dirs (project-scope first, then user-scope)
|
||||||
2. `.claude`
|
4. bundled agents last
|
||||||
3. `.codex`
|
|
||||||
4. `.gemini`
|
|
||||||
|
|
||||||
For each source family, discovery order is:
|
|
||||||
|
|
||||||
1. nearest project dir for that source (if found)
|
|
||||||
2. user dir for that source
|
|
||||||
|
|
||||||
After all source-family dirs, plugin `agents/` dirs are appended (project-scope plugins first, then user-scope).
|
|
||||||
|
|
||||||
Bundled agents are appended last.
|
|
||||||
|
|
||||||
### Important caveat: stale comments vs current code
|
|
||||||
|
|
||||||
`discovery.ts` header comments still mention `.pi` and do not mention `.codex`/`.gemini`. Actual runtime order is driven by `src/config.ts` and currently uses `.omp`, `.claude`, `.codex`, `.gemini`.
|
|
||||||
|
|
||||||
## Merge and collision rules
|
## Merge and collision rules
|
||||||
|
|
||||||
@@ -97,8 +82,7 @@ Discovery uses first-wins dedup by exact `agent.name`:
|
|||||||
|
|
||||||
Implications:
|
Implications:
|
||||||
|
|
||||||
- Project overrides user for same source family.
|
- Project `.omp` overrides user `.omp`.
|
||||||
- Higher-priority source family overrides lower (`.omp` before `.claude`, etc.).
|
|
||||||
- Non-bundled agents override bundled agents with the same name.
|
- Non-bundled agents override bundled agents with the same name.
|
||||||
- Name matching is case-sensitive (`Task` and `task` are distinct).
|
- Name matching is case-sensitive (`Task` and `task` are distinct).
|
||||||
- Within one directory, markdown files are read in lexicographic filename order before dedup.
|
- Within one directory, markdown files are read in lexicographic filename order before dedup.
|
||||||
@@ -125,7 +109,7 @@ Lookup is exact-name linear search:
|
|||||||
|
|
||||||
- `getAgent(agents, name)` => `agents.find(a => a.name === name)`
|
- `getAgent(agents, name)` => `agents.find(a => a.name === name)`
|
||||||
|
|
||||||
In synchronous task execution (`TaskTool.#executeSync`):
|
In spawn execution (`TaskTool.#executeSync` → `#runSpawn`):
|
||||||
|
|
||||||
1. agents are rediscovered at execution time (`discoverAgents(this.session.cwd)`)
|
1. agents are rediscovered at execution time (`discoverAgents(this.session.cwd)`)
|
||||||
2. requested `params.agent` is resolved through `getAgent`
|
2. requested `params.agent` is resolved through `getAgent`
|
||||||
@@ -146,9 +130,7 @@ Runtime output schema precedence in `TaskTool.execute`:
|
|||||||
|
|
||||||
(`effectiveOutputSchema = effectiveAgent.output ?? this.session.outputSchema` — the task call itself never carries a schema; ad-hoc structured workflows go through the eval bridge's `agent(prompt, schema)`.)
|
(`effectiveOutputSchema = effectiveAgent.output ?? this.session.outputSchema` — the task call itself never carries a schema; ad-hoc structured workflows go through the eval bridge's `agent(prompt, schema)`.)
|
||||||
|
|
||||||
Prompt-time guardrail text in `src/prompts/tools/task.md` warns about mismatch behavior for structured-output agents (`explore`, `reviewer`): output-format instructions in prose can conflict with built-in schema and produce `null` outputs.
|
The model-facing prompt (`src/prompts/tools/task.md`) no longer carries the old structured-output mismatch warning; it tags read-only agents and warns against offloading reasoning to `explore`/`quick_task` instead.
|
||||||
|
|
||||||
This is guidance, not hard runtime validation logic in `discoverAgents`.
|
|
||||||
|
|
||||||
## Command discovery interaction
|
## Command discovery interaction
|
||||||
|
|
||||||
@@ -197,10 +179,10 @@ So deeper levels cannot spawn further tasks even if the agent definition include
|
|||||||
|
|
||||||
## Plan mode behavior
|
## Plan mode behavior
|
||||||
|
|
||||||
When parent plan mode is enabled, `TaskTool.execute` builds an `effectiveAgent` before launching subprocesses:
|
When parent plan mode is enabled, `TaskTool.#runSpawn` builds an `effectiveAgent` before launching subprocesses:
|
||||||
|
|
||||||
- prepends the plan-mode subagent system prompt
|
- prepends the plan-mode subagent system prompt
|
||||||
- restricts tools to `read`, `search`, `find`, `lsp`, and `web_search`
|
- restricts tools to `read`, `search`, `find`, `lsp`, and `web_search`, plus `ast_grep`/`report_finding` when the agent's own tool list declares them (`PLAN_MODE_AGENT_TOOL_ALLOWLIST`)
|
||||||
- clears child spawns
|
- clears child spawns
|
||||||
|
|
||||||
The same `effectiveAgent` is used for subprocess launch, model/thinking overrides, and output-schema selection.
|
The same `effectiveAgent` is used for subprocess launch, model/thinking overrides, and output-schema selection.
|
||||||
|
|||||||
+3
-3
@@ -17,7 +17,7 @@ Primary implementation: `src/modes/theme/theme.ts`.
|
|||||||
|
|
||||||
## Theme JSON shape
|
## Theme JSON shape
|
||||||
|
|
||||||
Theme files are JSON objects validated against the runtime schema in `theme.ts` (`ThemeJsonSchema`) and mirrored by `src/modes/theme/theme-schema.json`.
|
Theme files are JSON objects validated against the runtime schema in `theme.ts` (`themeJsonSchema`) and mirrored by `src/modes/theme/theme-schema.json`.
|
||||||
|
|
||||||
Top-level fields:
|
Top-level fields:
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ All tokens below are required in `colors`.
|
|||||||
|
|
||||||
`thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `bashMode`, `pythonMode`
|
`thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `bashMode`, `pythonMode`
|
||||||
|
|
||||||
### Status line segment colors (14)
|
### Status line segment colors (13)
|
||||||
|
|
||||||
`statusLineSep`, `statusLineModel`, `statusLinePath`, `statusLineGitClean`, `statusLineGitDirty`, `statusLineContext`, `statusLineSpend`, `statusLineStaged`, `statusLineDirty`, `statusLineUntracked`, `statusLineOutput`, `statusLineCost`, `statusLineSubagents`
|
`statusLineSep`, `statusLineModel`, `statusLinePath`, `statusLineGitClean`, `statusLineGitDirty`, `statusLineContext`, `statusLineSpend`, `statusLineStaged`, `statusLineDirty`, `statusLineUntracked`, `statusLineOutput`, `statusLineCost`, `statusLineSubagents`
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ Legacy migration exists: old flat `theme: "name"` is migrated to nested `theme.d
|
|||||||
1. Create file in custom themes dir, e.g. `~/.omp/agent/themes/my-theme.json`.
|
1. Create file in custom themes dir, e.g. `~/.omp/agent/themes/my-theme.json`.
|
||||||
2. Include `name`, optional `vars`, and **all required** `colors` tokens.
|
2. Include `name`, optional `vars`, and **all required** `colors` tokens.
|
||||||
3. Optionally include `symbols` and `export`.
|
3. Optionally include `symbols` and `export`.
|
||||||
4. Select the theme in Settings (`Display -> Dark theme` or `Display -> Light theme`) depending on which auto slot you want.
|
4. Select the theme in Settings (`Appearance -> Dark Theme` or `Appearance -> Light Theme`) depending on which auto slot you want.
|
||||||
|
|
||||||
Minimal skeleton:
|
Minimal skeleton:
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -22,7 +22,7 @@
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `id` | `string` | Yes | Stable identifier used in multi-question results. |
|
| `id` | `string` | Yes | Stable identifier used in multi-question results. |
|
||||||
| `question` | `string` | Yes | Prompt text shown to the user. |
|
| `question` | `string` | Yes | Prompt text shown to the user. |
|
||||||
| `options` | `{ label: string }[]` | Yes | Option labels for the picker. The schema does not require a minimum length; the UI always appends `Other (type your own)`, and callers must not include it. |
|
| `options` | `{ label: string; description?: string }[]` | Yes | Option labels for the picker, each with optional explanatory `description` text shown below the label. The schema does not require a minimum length; the UI always appends `Other (type your own)`, and callers must not include it. |
|
||||||
| `multi` | `boolean` | No | Enables multi-select mode. Default: `false`. |
|
| `multi` | `boolean` | No | Enables multi-select mode. Default: `false`. |
|
||||||
| `recommended` | `number` | No | Zero-based recommended option index. In single-select mode the label gets ` (Recommended)` appended in the UI. |
|
| `recommended` | `number` | No | Zero-based recommended option index. In single-select mode the label gets ` (Recommended)` appended in the UI. |
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
- single question: `User selected: ...` and/or `User provided custom input: ...`
|
- single question: `User selected: ...` and/or `User provided custom input: ...`
|
||||||
- multiple questions: `User answers:` followed by one line per `id`
|
- multiple questions: `User answers:` followed by one line per `id`
|
||||||
- `details`:
|
- `details`:
|
||||||
- single question: `{ question, options, multi, selectedOptions, customInput? }`
|
- single question: `{ question, options, multi, selectedOptions, customInput?, timedOut? }`
|
||||||
- multiple questions: `{ results: QuestionResult[] }`, where each item includes `id`, `question`, `options`, `multi`, `selectedOptions`, and optional `customInput`
|
- multiple questions: `{ results: QuestionResult[] }`, where each item includes `id`, `question`, `options`, `multi`, `selectedOptions`, and optional `customInput` and `timedOut`
|
||||||
- Cancellation and headless cases throw instead of returning a structured success result.
|
- Cancellation and headless cases throw instead of returning a structured success result.
|
||||||
|
|
||||||
## Flow
|
## Flow
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
- single-select list + optional editor for `Other`
|
- single-select list + optional editor for `Other`
|
||||||
- multi-select checkbox loop + `Done selecting` sentinel + optional editor for `Other`
|
- multi-select checkbox loop + `Done selecting` sentinel + optional editor for `Other`
|
||||||
6. In multi-question mode, left/right arrow handlers enable back/forward navigation between questions and preserve prior selections.
|
6. In multi-question mode, left/right arrow handlers enable back/forward navigation between questions and preserve prior selections.
|
||||||
7. If a timeout fires before any selection/custom input, the tool auto-selects the recommended option, or the first option when no valid `recommended` index exists.
|
7. If a timeout fires before any selection/custom input, the tool auto-selects the recommended option, or the first option when no valid `recommended` index exists; the result text gets an ` (auto-selected after timeout)` suffix and `details.timedOut` is set.
|
||||||
8. If the user cancels without timeout, `execute()` aborts the tool context and throws `ToolAbortError("Ask tool was cancelled by the user")`.
|
8. If the user cancels without timeout, `execute()` aborts the tool context and throws `ToolAbortError("Ask tool was cancelled by the user")`.
|
||||||
9. On success it formats human-readable text plus structured `details`; the TUI renderer uses `details` for rich display.
|
9. On success it formats human-readable text plus structured `details`; the TUI renderer uses `details` for rich display.
|
||||||
|
|
||||||
@@ -70,7 +70,8 @@
|
|||||||
- `questions` must contain at least 1 item (`askSchema` in `packages/coding-agent/src/tools/ask.ts`).
|
- `questions` must contain at least 1 item (`askSchema` in `packages/coding-agent/src/tools/ask.ts`).
|
||||||
- `ask.timeout` default is `0` seconds, which disables timeout (`packages/coding-agent/src/config/settings-schema.ts`). Configured non-zero values are seconds.
|
- `ask.timeout` default is `0` seconds, which disables timeout (`packages/coding-agent/src/config/settings-schema.ts`). Configured non-zero values are seconds.
|
||||||
- Prompt guidance says provide 2-5 options, but code only requires the `options` array field and does not enforce a minimum or maximum length (`packages/coding-agent/src/prompts/tools/ask.md`).
|
- Prompt guidance says provide 2-5 options, but code only requires the `options` array field and does not enforce a minimum or maximum length (`packages/coding-agent/src/prompts/tools/ask.md`).
|
||||||
- Timeout only applies to the option picker; once the user chooses `Other`, the editor has no timeout (`packages/coding-agent/src/prompts/tools/ask.md`).
|
- Timeout only applies to the option picker; once the user chooses `Other`, the editor has no timeout (`promptForCustomInput()` in `packages/coding-agent/src/tools/ask.ts`).
|
||||||
|
- `AskTool.concurrency = "exclusive"`: the tool runs alone in its tool batch because the selector/editor UI surface is shared and concurrent `ask` calls would clobber each other.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- Missing interactive UI: throws `ToolAbortError("Ask tool requires interactive mode")`.
|
- Missing interactive UI: throws `ToolAbortError("Ask tool requires interactive mode")`.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Shared AST pattern grammar and language catalog: see [`ast_grep`](./ast-grep.md#
|
|||||||
## Outputs
|
## Outputs
|
||||||
- Single-shot preview result from `ast_edit` itself.
|
- Single-shot preview result from `ast_edit` itself.
|
||||||
- Model-facing `content` is one text block showing proposed edits, grouped by file for directory/multi-file runs.
|
- Model-facing `content` is one text block showing proposed edits, grouped by file for directory/multi-file runs.
|
||||||
- Each change renders as two lines. Hashline mode uses `-LINE:before` / `+LINE:after` under a `¶PATH#TAG` header; plain mode uses `-LINE:COLUMN before` / `+LINE:COLUMN after`.
|
- Each change renders as two lines. Hashline mode uses `-LINE:before` / `+LINE:after` under a `[PATH#TAG]` header; plain mode uses `-LINE:COLUMN before` / `+LINE:COLUMN after`.
|
||||||
- Only the first line of each `before`/`after` snippet is shown, truncated to 120 characters in the wrapper.
|
- Only the first line of each `before`/`after` snippet is shown, truncated to 120 characters in the wrapper.
|
||||||
- `Limit reached; narrow paths.` and formatted parse issues are appended when applicable.
|
- `Limit reached; narrow paths.` and formatted parse issues are appended when applicable.
|
||||||
- If no rewrites match, text is `No replacements made` plus formatted parse issues when present.
|
- If no rewrites match, text is `No replacements made` plus formatted parse issues when present.
|
||||||
@@ -63,7 +63,7 @@ Shared AST pattern grammar and language catalog: see [`ast_grep`](./ast-grep.md#
|
|||||||
7. The TS wrapper deduplicates parse errors, groups changes by file, and renders preview diff lines.
|
7. The TS wrapper deduplicates parse errors, groups changes by file, and renders preview diff lines.
|
||||||
8. If preview found replacements and `applied` is false, `queueResolveHandler(...)` registers a forced `resolve` action and injects a `resolve-reminder` steering message.
|
8. If preview found replacements and `applied` is false, `queueResolveHandler(...)` registers a forced `resolve` action and injects a `resolve-reminder` steering message.
|
||||||
9. On `resolve(action: "apply")`, the queued callback reruns the same rewrite set with `dryRun: false`, recomputes counts, and returns an error result if the live result no longer matches the preview (`stalePreview`). The current implementation compares replacement totals and per-file counts after the rerun; if the new run has already written different counts, the result is marked error.
|
9. On `resolve(action: "apply")`, the queued callback reruns the same rewrite set with `dryRun: false`, recomputes counts, and returns an error result if the live result no longer matches the preview (`stalePreview`). The current implementation compares replacement totals and per-file counts after the rerun; if the new run has already written different counts, the result is marked error.
|
||||||
10. On a non-stale apply, the callback returns `Applied N replacements in M files.`; on discard, `resolve` returns a discard message without mutating files.
|
10. On a non-stale apply, the callback returns `Applied N replacements in M files.` (in hashline mode followed by fresh `[path#tag]` snapshot headers re-recorded from the post-apply content); on discard, `resolve` returns a discard message without mutating files.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
- Single file: preview or apply against one file.
|
- Single file: preview or apply against one file.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Pattern grammar and language support exposed to the model:
|
|||||||
- Single-shot tool result.
|
- Single-shot tool result.
|
||||||
- Model-facing `content` is one text block:
|
- Model-facing `content` is one text block:
|
||||||
- grouped by file for directory/multi-file searches,
|
- grouped by file for directory/multi-file searches,
|
||||||
- match lines rendered under `¶PATH#HASH` as `*LINE:text` in hashline mode or `*LINE|text` otherwise,
|
- match lines rendered under `[PATH#HASH]` as `*LINE:text` in hashline mode or `*LINE|text` otherwise,
|
||||||
- continuation lines for multi-line matches rendered with a leading space,
|
- continuation lines for multi-line matches rendered with a leading space,
|
||||||
- optional `meta: NAME=value` lines when ast-grep captured metavariables.
|
- optional `meta: NAME=value` lines when ast-grep captured metavariables.
|
||||||
- If no matches are found, text is `No matches found` or `No matches found. Parse issues mean the query may be mis-scoped; narrow paths before concluding absence.` plus formatted parse issues.
|
- If no matches are found, text is `No matches found` or `No matches found. Parse issues mean the query may be mis-scoped; narrow paths before concluding absence.` plus formatted parse issues.
|
||||||
@@ -70,7 +70,7 @@ Pattern grammar and language support exposed to the model:
|
|||||||
- Directory + optional glob: native scan walks the directory, then filters by compiled glob.
|
- Directory + optional glob: native scan walks the directory, then filters by compiled glob.
|
||||||
- Multiple explicit paths/globs: wrapper unions them into one synthetic scope or runs per-target native calls when paths only meet at root.
|
- Multiple explicit paths/globs: wrapper unions them into one synthetic scope or runs per-target native calls when paths only meet at root.
|
||||||
- Internal URL inputs: only supported when the router can resolve them to a backing file path.
|
- Internal URL inputs: only supported when the router can resolve them to a backing file path.
|
||||||
- Hashline output mode vs plain line-number mode: controlled by `resolveFileDisplayMode()`; hashline mode requires the edit tool and non-raw, mutable sources.
|
- Hashline output mode vs plain line-number mode: controlled by `resolveFileDisplayMode()`; hashline mode requires the edit tool and hashline edit mode, and per-file anchors additionally require a successful whole-file snapshot (`recordFileSnapshot()`) — over-cap or unreadable files fall back to plain output.
|
||||||
|
|
||||||
## Side Effects
|
## Side Effects
|
||||||
- Filesystem
|
- Filesystem
|
||||||
|
|||||||
+2
-2
@@ -51,7 +51,7 @@ The tool returns a single `text` content block plus optional `details`.
|
|||||||
Stdout and stderr are merged before the model sees them. Definite non-zero exit codes are appended to the returned error result text as `Command exited with code <n>`.
|
Stdout and stderr are merged before the model sees them. Definite non-zero exit codes are appended to the returned error result text as `Command exited with code <n>`.
|
||||||
|
|
||||||
## Flow
|
## Flow
|
||||||
1. `BashTool.execute()` in `packages/coding-agent/src/tools/bash.ts` reads `command`, normalizes `env`, and defaults `timeout` to `300`.
|
1. `BashTool.execute()` in `packages/coding-agent/src/tools/bash.ts` reads `command`, normalizes `env`, and defaults `timeout` to `300`. When `bash.stripTrailingHeadTail` is enabled (default), `applyBashFixups()` from `packages/coding-agent/src/tools/bash-command-fixup.ts` first strips safe trailing `| head`/`| tail` pipes and redundant trailing `2>&1` from single-line commands.
|
||||||
2. If `cwd` is absent, it rewrites a leading `cd <path> && ...` into the structured `cwd` field and strips that prefix from `command`.
|
2. If `cwd` is absent, it rewrites a leading `cd <path> && ...` into the structured `cwd` field and strips that prefix from `command`.
|
||||||
3. If `async: true` is requested while `async.enabled` is off, it throws `ToolError` before any execution.
|
3. If `async: true` is requested while `async.enabled` is off, it throws `ToolError` before any execution.
|
||||||
4. If `bashInterceptor.enabled` is on, `checkBashInterception()` runs against both the original command and the `cd`-stripped command. A matching enabled rule throws before URL expansion or execution.
|
4. If `bashInterceptor.enabled` is on, `checkBashInterception()` runs against both the original command and the `cd`-stripped command. A matching enabled rule throws before URL expansion or execution.
|
||||||
@@ -120,7 +120,7 @@ Stdout and stderr are merged before the model sees them. Definite non-zero exit
|
|||||||
- Default timeout: `300s` (`TOOL_TIMEOUTS.bash.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`).
|
- Default timeout: `300s` (`TOOL_TIMEOUTS.bash.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`).
|
||||||
- Timeout clamp: `1..3600s` (`TOOL_TIMEOUTS.bash.min/max`).
|
- Timeout clamp: `1..3600s` (`TOOL_TIMEOUTS.bash.min/max`).
|
||||||
- Auto-background default threshold: `60_000ms` (`DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS` in `packages/coding-agent/src/tools/bash.ts`), further capped to `timeoutMs - 1000` by `#resolveAutoBackgroundWaitMs()`.
|
- Auto-background default threshold: `60_000ms` (`DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS` in `packages/coding-agent/src/tools/bash.ts`), further capped to `timeoutMs - 1000` by `#resolveAutoBackgroundWaitMs()`.
|
||||||
- Hard kill grace beyond requested timeout in non-PTY executor: `5_000ms` (`HARD_TIMEOUT_GRACE_MS` in `packages/coding-agent/src/exec/bash-executor.ts`).
|
- Non-PTY executor timeout: `executeBash()` arms a host-side timer at `max(1_000, timeoutMs)` that aborts the run and quarantines the persistent shell session; the same timeout is also passed to the native run as `timeoutMs` (`packages/coding-agent/src/exec/bash-executor.ts`).
|
||||||
- In-memory output tail cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). Once exceeded, the sink keeps only the tail window in memory.
|
- In-memory output tail cap: `50 * 1024` bytes (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`). Once exceeded, the sink keeps only the tail window in memory.
|
||||||
- Streaming callback throttle in `executeBash()`: `50ms` between `onChunk` calls when streaming is enabled.
|
- Streaming callback throttle in `executeBash()`: `50ms` between `onChunk` calls when streaming is enabled.
|
||||||
- TUI collapsed preview: `10` visual lines (`BASH_DEFAULT_PREVIEW_LINES`) when rendered inline in the agent UI; this is a renderer cap, not a tool output cap.
|
- TUI collapsed preview: `10` visual lines (`BASH_DEFAULT_PREVIEW_LINES`) when rendered inline in the agent UI; this is a renderer cap, not a tool output cap.
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ The tool returns one result per call; no streaming partial output is emitted fro
|
|||||||
- `string` becomes text content,
|
- `string` becomes text content,
|
||||||
- other values become pretty JSON text when serializable, else `String(value)`.
|
- other values become pretty JSON text when serializable, else `String(value)`.
|
||||||
- `tab.screenshot()` also appends text plus an image content item unless `silent: true`; `details.screenshots` records persisted screenshot metadata `{ dest, mimeType, bytes, width, height }`.
|
- `tab.screenshot()` also appends text plus an image content item unless `silent: true`; `details.screenshots` records persisted screenshot metadata `{ dest, mimeType, bytes, width, height }`.
|
||||||
- `run` `details` includes `action`, `name`, current `browser`/`url` when the tab exists, optional `screenshots`, and `details.result` containing only the concatenated text outputs.
|
- `run` `details` includes `action`, `name`, current `browser`/`url` when the tab exists, optional `screenshots`, and `details.result` containing only the concatenated text outputs. Combined run text is capped at the inline byte limit via `enforceInlineByteCap()`; over-cap text is saved as a session artifact (`saveBrowserOutputArtifact()`) and the capped text replaces it in content and `details.result`.
|
||||||
|
|
||||||
## Flow
|
## Flow
|
||||||
1. `BrowserTool.execute()` (`packages/coding-agent/src/tools/browser.ts`) abort-checks, clamps `timeout` via `clampTimeout("browser", ...)`, defaults `name` to `"main"`, and dispatches on `action`.
|
1. `BrowserTool.execute()` (`packages/coding-agent/src/tools/browser.ts`) abort-checks, clamps `timeout` via `clampTimeout("browser", ...)`, defaults `name` to `"main"`, and dispatches on `action`.
|
||||||
|
|||||||
+8
-2
@@ -19,6 +19,8 @@
|
|||||||
- `packages/coding-agent/src/debug/profiler.ts` — CPU/heap profiling helpers
|
- `packages/coding-agent/src/debug/profiler.ts` — CPU/heap profiling helpers
|
||||||
- `packages/coding-agent/src/debug/report-bundle.ts` — `.tar.gz` report bundling, log source, cache cleanup
|
- `packages/coding-agent/src/debug/report-bundle.ts` — `.tar.gz` report bundling, log source, cache cleanup
|
||||||
- `packages/coding-agent/src/debug/system-info.ts` — system snapshot collection and env redaction
|
- `packages/coding-agent/src/debug/system-info.ts` — system snapshot collection and env redaction
|
||||||
|
- `packages/coding-agent/src/debug/terminal-info.ts` — terminal state collection/formatting
|
||||||
|
- `packages/coding-agent/src/debug/protocol-probe.ts` — terminal protocol probe panel and sample image
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
- `custom_request`: `command`
|
- `custom_request`: `command`
|
||||||
|
|
||||||
### Interactive selector values
|
### Interactive selector values
|
||||||
`packages/coding-agent/src/debug/index.ts` also exposes a fixed UI-only selector with values `open-artifacts`, `performance`, `work`, `dump`, `memory`, `logs`, `system`, `raw-sse`, `transcript`, `clear-cache`. These are not model-callable through `debugSchema`; they are local TUI menu routes.
|
`packages/coding-agent/src/debug/index.ts` also exposes a fixed UI-only selector with values `open-artifacts`, `performance`, `work`, `dump`, `memory`, `logs`, `system`, `terminal`, `protocols`, `raw-sse`, `transcript`, `clear-cache`. These are not model-callable through `debugSchema`; they are local TUI menu routes.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
The agent tool returns a standard `toolResult()` payload from `packages/coding-agent/src/tools/debug.ts`:
|
The agent tool returns a standard `toolResult()` payload from `packages/coding-agent/src/tools/debug.ts`:
|
||||||
@@ -141,6 +143,8 @@ Side-channel artifacts outside the model tool result:
|
|||||||
- `logs`: build a `DebugLogSource` and mount `DebugLogViewerComponent`
|
- `logs`: build a `DebugLogSource` and mount `DebugLogViewerComponent`
|
||||||
- `raw-sse`: resolve a `RawSseDebugBuffer` from the session and mount `RawSseViewerComponent`
|
- `raw-sse`: resolve a `RawSseDebugBuffer` from the session and mount `RawSseViewerComponent`
|
||||||
- `system`: call `collectSystemInfo()` and render `formatSystemInfo()` into the chat pane
|
- `system`: call `collectSystemInfo()` and render `formatSystemInfo()` into the chat pane
|
||||||
|
- `terminal`: `collectTerminalState()` + `formatTerminalState()` rendered into the chat pane
|
||||||
|
- `protocols`: fires a test desktop notification (unless suppressed), then mounts `ProtocolProbeComponent` with a sample image
|
||||||
- `open-artifacts`: open the current session artifact directory if it exists
|
- `open-artifacts`: open the current session artifact directory if it exists
|
||||||
- `transcript`: delegates to `ctx.handleDebugTranscriptCommand()`
|
- `transcript`: delegates to `ctx.handleDebugTranscriptCommand()`
|
||||||
- `clear-cache`: show confirmation, then remove artifact directories older than 30 days with `clearArtifactCache()`
|
- `clear-cache`: show confirmation, then remove artifact directories older than 30 days with `clearArtifactCache()`
|
||||||
@@ -186,6 +190,8 @@ Side-channel artifacts outside the model tool result:
|
|||||||
- `dump` — report bundle without profiler artifacts.
|
- `dump` — report bundle without profiler artifacts.
|
||||||
- `work` — standalone work-profile flamegraph export/open.
|
- `work` — standalone work-profile flamegraph export/open.
|
||||||
- `system` — formatted OS/arch/CPU/memory/version/cwd/shell/terminal dump.
|
- `system` — formatted OS/arch/CPU/memory/version/cwd/shell/terminal dump.
|
||||||
|
- `terminal` — formatted terminal subprotocol/geometry/scrollback state dump.
|
||||||
|
- `protocols` — terminal protocol test: desktop-notification side effect plus a probe panel sampling special protocols.
|
||||||
- `open-artifacts` / `transcript` / `clear-cache` — artifact directory open, transcript export, artifact-cache pruning.
|
- `open-artifacts` / `transcript` / `clear-cache` — artifact directory open, transcript export, artifact-cache pruning.
|
||||||
|
|
||||||
## Side Effects
|
## Side Effects
|
||||||
@@ -226,7 +232,7 @@ Side-channel artifacts outside the model tool result:
|
|||||||
- Single active session: enforced by `#ensureLaunchSlot()` in `packages/coding-agent/src/dap/session.ts`.
|
- Single active session: enforced by `#ensureLaunchSlot()` in `packages/coding-agent/src/dap/session.ts`.
|
||||||
- Idle session cleanup: `IDLE_TIMEOUT_MS = 10 * 60 * 1000`, checked every `CLEANUP_INTERVAL_MS = 30 * 1000`.
|
- Idle session cleanup: `IDLE_TIMEOUT_MS = 10 * 60 * 1000`, checked every `CLEANUP_INTERVAL_MS = 30 * 1000`.
|
||||||
- Adapter liveness heartbeat: `HEARTBEAT_INTERVAL_MS = 5 * 1000`.
|
- Adapter liveness heartbeat: `HEARTBEAT_INTERVAL_MS = 5 * 1000`.
|
||||||
- Output capture cap: `MAX_OUTPUT_BYTES = 128 * 1024`; older text is trimmed in ~1 KiB slices and `outputTruncated` is recorded.
|
- Output capture cap: `MAX_OUTPUT_BYTES = 128 * 1024`; whole chunks are dropped from the front (then the front chunk is byte-sliced so exactly the cap remains) and `outputTruncated` is recorded.
|
||||||
- Initial stop capture timeout after launch/attach: `STOP_CAPTURE_TIMEOUT_MS = 5_000`.
|
- Initial stop capture timeout after launch/attach: `STOP_CAPTURE_TIMEOUT_MS = 5_000`.
|
||||||
- Socket-mode adapter readiness timeout: `10_000` ms in `waitForCondition()` and TCP connect timeout logic in `packages/coding-agent/src/dap/client.ts`.
|
- Socket-mode adapter readiness timeout: `10_000` ms in `waitForCondition()` and TCP connect timeout logic in `packages/coding-agent/src/dap/client.ts`.
|
||||||
- Raw SSE buffer caps in `packages/coding-agent/src/debug/raw-sse-buffer.ts`:
|
- Raw SSE buffer caps in `packages/coding-agent/src/debug/raw-sse-buffer.ts`:
|
||||||
|
|||||||
+22
-23
@@ -8,8 +8,8 @@
|
|||||||
- Key collaborators:
|
- Key collaborators:
|
||||||
- `packages/coding-agent/src/utils/edit-mode.ts` — selects active edit mode
|
- `packages/coding-agent/src/utils/edit-mode.ts` — selects active edit mode
|
||||||
- `packages/hashline/src/grammar.lark` — canonical constrained-decoding grammar
|
- `packages/hashline/src/grammar.lark` — canonical constrained-decoding grammar
|
||||||
- `packages/hashline/src/format.ts` — sigils and header constants (`¶`, `#`, `+`, `replace`, `delete`, `insert`)
|
- `packages/hashline/src/format.ts` — sigils and header constants (`[`, `]`, `#`, `+`, `replace`, `delete`, `insert`)
|
||||||
- `packages/hashline/src/input.ts` — parses `¶PATH#TAG` sections
|
- `packages/hashline/src/input.ts` — parses `[PATH#TAG]` sections
|
||||||
- `packages/hashline/src/tokenizer.ts` / `packages/hashline/src/parser.ts` — tokenizes and parses ops
|
- `packages/hashline/src/tokenizer.ts` / `packages/hashline/src/parser.ts` — tokenizes and parses ops
|
||||||
- `packages/hashline/src/apply.ts` — applies parsed edits to file text
|
- `packages/hashline/src/apply.ts` — applies parsed edits to file text
|
||||||
- `packages/hashline/src/mismatch.ts` — stale-anchor mismatch formatting
|
- `packages/hashline/src/mismatch.ts` — stale-anchor mismatch formatting
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `input` | `string` | Yes | One or more file sections. Anchored sections must start with `¶PATH#TAG`; `TAG` is the four-hex snapshot tag emitted by the latest `read`/`search`/`write`/successful `edit`. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
|
| `input` | `string` | Yes | One or more file sections. Anchored sections must start with `[PATH#TAG]`; `TAG` is the four-hex snapshot tag emitted by the latest `read`/`search`/`write`/successful `edit`. Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
|
||||||
|
|
||||||
Patch language inside `input`:
|
Patch language inside `input`:
|
||||||
|
|
||||||
- **File header**: `¶PATH#TAG`. `TAG` is four uppercase-hex chars minted by the session snapshot store.
|
- **File header**: `[PATH#TAG]`. `TAG` is four uppercase-hex chars — a content-derived hash of the whole normalized file (`computeFileHash()`), recorded in the session snapshot store.
|
||||||
- **Operations**:
|
- **Operations**:
|
||||||
- `replace N..M:` — replace original lines N..M with the body rows below.
|
- `replace N..M:` — replace original lines N..M with the body rows below.
|
||||||
- `replace block N:` — replace the whole tree-sitter block beginning on line N (its header line through its closing line) with the body rows. The line span is resolved at apply time from the file's parse tree; point N at the line that opens the construct. The resolved span is exactly the node that begins on line N — a leading decorator, attribute, or doc-comment is a separate node and is not included; point N at the first decorator line (Python wraps `@dec` + `def` as one block) or fall back to `replace N..M:` to take a leading line-comment that parses as its own node (e.g. Rust `///`). On success the result echoes the matched span (`replace block N → resolved lines A-B`). Errors (and steers to `replace N..M:`) when the language is unsupported, line N is blank or a closing delimiter, no node begins there, or the resolved block has a syntax error.
|
- `replace block N:` — replace the whole tree-sitter block beginning on line N (its header line through its closing line) with the body rows. The line span is resolved at apply time from the file's parse tree; point N at the line that opens the construct. The resolved span is exactly the node that begins on line N — a leading decorator, attribute, or doc-comment is a separate node and is not included; point N at the first decorator line (Python wraps `@dec` + `def` as one block) or fall back to `replace N..M:` to take a leading line-comment that parses as its own node (e.g. Rust `///`). On success the result echoes the matched span (`replace block N → resolved lines A-B`). Errors (and steers to `replace N..M:`) when the language is unsupported, line N is blank or a closing delimiter, no node begins there, or the resolved block has a syntax error.
|
||||||
@@ -44,7 +44,7 @@ Patch language inside `input`:
|
|||||||
- There is no repeat row kind. To keep a line, leave it out of every range; split edits into multiple hunks when needed.
|
- There is no repeat row kind. To keep a line, leave it out of every range; split edits into multiple hunks when needed.
|
||||||
- `-` rows are invalid. Literal text beginning with `-` or `+` must be written as `+-text` / `++text`.
|
- `-` rows are invalid. Literal text beginning with `-` or `+` must be written as `+-text` / `++text`.
|
||||||
|
|
||||||
Anchors come from `read`/`search` output. `read` emits a `¶PATH#TAG` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
|
Anchors come from `read`/`search` output. `read` emits a `[PATH#TAG]` header from the session snapshot store and lines as `LINE:TEXT`; copy the header into the edit section and copy only the line number into hunk headers.
|
||||||
|
|
||||||
### Tolerated input shapes (lenient parsing)
|
### Tolerated input shapes (lenient parsing)
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ The canonical grammar is strict, but the hand parser accepts a few non-dangerous
|
|||||||
- `replace N-M:`, `replace N…M:`, and `replace N M:` — accepted as `replace N..M:`.
|
- `replace N-M:`, `replace N…M:`, and `replace N M:` — accepted as `replace N..M:`.
|
||||||
- Bare body rows with no `+` prefix are auto-prepended with `+` and a `BARE_BODY_AUTO_PIPED_WARNING` is appended.
|
- Bare body rows with no `+` prefix are auto-prepended with `+` and a `BARE_BODY_AUTO_PIPED_WARNING` is appended.
|
||||||
- `*** Begin Patch` / `*** End Patch` envelopes are silently consumed. `*** Abort` terminates parsing silently — ops parsed before the marker still apply, no warning surfaced.
|
- `*** Begin Patch` / `*** End Patch` envelopes are silently consumed. `*** Abort` terminates parsing silently — ops parsed before the marker still apply, no warning surfaced.
|
||||||
- Some malformed `¶` headers are recovered after stripping apply-patch path noise such as `Update File:` / `Add File:` and extra `***`, but the recovered header still needs a valid four-hex tag for the patcher to apply it.
|
- Some malformed bracketed headers are recovered after stripping apply-patch path noise such as `Update File:` / `Add File:` and extra `***`, but the recovered header still needs a valid four-hex tag for the patcher to apply it.
|
||||||
- `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:` apply_patch sentinels inside the diff body throw an `apply_patch sentinel … is not valid in hashline` error.
|
- `*** Update File:` / `*** Add File:` / `*** Delete File:` / `*** Move to:` apply_patch sentinels inside the diff body throw an `apply_patch sentinel … is not valid in hashline` error.
|
||||||
- `@@`-bracketed hunk headers are rejected with guidance to write a verb header.
|
- `@@`-bracketed hunk headers are rejected with guidance to write a verb header.
|
||||||
- Bare `N` and bare `N M` / `N..M` headers are rejected with guidance to write `replace` or `delete`.
|
- Bare `N` and bare `N M` / `N..M` headers are rejected with guidance to write `replace` or `delete`.
|
||||||
@@ -67,10 +67,8 @@ The canonical grammar is strict, but the hand parser accepts a few non-dangerous
|
|||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
- Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake.
|
- Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake.
|
||||||
- `content` contains one text block per call. For a successful single-file edit it is either:
|
- `content` contains one text block per call. For a successful single-file edit it is the post-edit `[path#TAG]` section header (a fresh snapshot tag for the written content), followed by a compact diff preview from `packages/hashline/src/diff-preview.ts` when one is emitted.
|
||||||
- `<path>:` plus a compact diff preview from `packages/hashline/src/diff-preview.ts`, or
|
- When the patch used `replace block`/`delete block`/`insert after block` ops (and the apply matched the tagged content), one `replace block N → resolved lines A-B (K lines)` line per block op (single-line spans render `resolved line A (1 line)`; insert-after appends `; body lands after line B`) is inserted between the `[PATH#TAG]` header and the diff preview, so the caller can confirm tree-sitter resolved the construct it intended.
|
||||||
- `Updated <path>` / `Created <path>` when no compact preview text is emitted.
|
|
||||||
- When the patch used `replace block`/`delete block` ops (and the apply matched the tagged content), one `replace block N → resolved lines A-B (K lines)` line per block op is inserted between the `¶PATH#TAG` header and the diff preview, so the caller can confirm tree-sitter resolved the construct it intended.
|
|
||||||
- Parse, apply, or recovery warnings are appended as:
|
- Parse, apply, or recovery warnings are appended as:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -92,7 +90,7 @@ Warnings:
|
|||||||
Reference file (the exact shape `read` returns):
|
Reference file (the exact shape `read` returns):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
1:const X = "a";
|
1:const X = "a";
|
||||||
2:const Y = X;
|
2:const Y = X;
|
||||||
3:
|
3:
|
||||||
@@ -104,7 +102,7 @@ Reference file (the exact shape `read` returns):
|
|||||||
Replace line 1 with two lines:
|
Replace line 1 with two lines:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
replace 1..1:
|
replace 1..1:
|
||||||
+const X = "b";
|
+const X = "b";
|
||||||
+export const Y = X;
|
+export const Y = X;
|
||||||
@@ -113,7 +111,7 @@ replace 1..1:
|
|||||||
Insert below line 5:
|
Insert below line 5:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
insert after 5:
|
insert after 5:
|
||||||
+console.log(X + Y);
|
+console.log(X + Y);
|
||||||
```
|
```
|
||||||
@@ -121,7 +119,7 @@ insert after 5:
|
|||||||
Insert above line 5:
|
Insert above line 5:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
insert before 5:
|
insert before 5:
|
||||||
+console.log(X + Y);
|
+console.log(X + Y);
|
||||||
```
|
```
|
||||||
@@ -129,14 +127,14 @@ insert before 5:
|
|||||||
Delete lines 4..5 entirely:
|
Delete lines 4..5 entirely:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
delete 4..5
|
delete 4..5
|
||||||
```
|
```
|
||||||
|
|
||||||
Insert at start and end of file:
|
Insert at start and end of file:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶a.ts#0A3B
|
[a.ts#0A3B]
|
||||||
insert head:
|
insert head:
|
||||||
+// header
|
+// header
|
||||||
insert tail:
|
insert tail:
|
||||||
@@ -146,24 +144,24 @@ insert tail:
|
|||||||
Multi-file:
|
Multi-file:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
¶src/a.ts#0A3B
|
[src/a.ts#0A3B]
|
||||||
replace 4..4:
|
replace 4..4:
|
||||||
+const enabled = true;
|
+const enabled = true;
|
||||||
¶src/b.ts#1F7C
|
[src/b.ts#1F7C]
|
||||||
delete 20
|
delete 20
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limits & Caps
|
## Limits & Caps
|
||||||
- File snapshot tags are exactly four uppercase-hex chars minted by the per-session snapshot store.
|
- File snapshot tags are exactly four uppercase-hex chars — content-derived hashes (`computeFileHash()`) recorded in the per-session snapshot store.
|
||||||
- The visible mismatch report shows 2 lines of context on each side (`MISMATCH_CONTEXT`) in `packages/hashline/src/messages.ts`.
|
- The visible mismatch report shows 2 lines of context on each side (`MISMATCH_CONTEXT`) in `packages/hashline/src/messages.ts`.
|
||||||
- Stale-anchor recovery uses `fuzzFactor: 0` in `packages/hashline/src/recovery.ts`.
|
- Stale-anchor recovery uses `fuzzFactor: 0` in `packages/hashline/src/recovery.ts`.
|
||||||
- `HL_FILE_PREFIX` is `¶`, `HL_PAYLOAD_REPLACE` is `+`, `HL_RANGE_SEP` is `..`, `HL_FILE_HASH_SEP` is `#`, and hunk keyword constants are `replace` / `delete` / `insert` (`packages/hashline/src/format.ts`).
|
- `HL_FILE_PREFIX` is `[`, `HL_FILE_SUFFIX` is `]`, `HL_PAYLOAD_REPLACE` is `+`, `HL_RANGE_SEP` is `..`, `HL_FILE_HASH_SEP` is `#`, and hunk keyword constants are `replace` / `delete` / `insert` (`packages/hashline/src/format.ts`).
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- Missing section header:
|
- Missing section header:
|
||||||
- `input must begin with "¶PATH#HASH" on the first non-blank line for anchored edits; got: ...`
|
- `input must begin with "[PATH#HASH]" on the first non-blank line for anchored edits; got: ...`
|
||||||
- Missing tag for any section:
|
- Missing tag for any section:
|
||||||
- `Missing hashline snapshot tag for anchored edit to <path>; use ¶<path>#tag from your latest read/search output.`
|
- `Missing hashline snapshot tag for edit to <path>; use \`[<path>#tag]\` from your latest read/search output. To create a new file, use the write tool.`
|
||||||
- Stray payload line:
|
- Stray payload line:
|
||||||
- `line N: payload line has no preceding hunk header. Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` above the body. Got "...".`
|
- `line N: payload line has no preceding hunk header. Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` above the body. Got "...".`
|
||||||
- Minus row:
|
- Minus row:
|
||||||
@@ -183,7 +181,7 @@ delete 20
|
|||||||
- Overlapping hunks on the same anchor:
|
- Overlapping hunks on the same anchor:
|
||||||
- `line N: anchor line X is already targeted by another hunk on line Y. Issue ONE hunk per range; payload is only the final desired content, never a before/after pair.`
|
- `line N: anchor line X is already targeted by another hunk on line Y. Issue ONE hunk per range; payload is only the final desired content, never a before/after pair.`
|
||||||
- apply_patch / unified-diff contamination:
|
- apply_patch / unified-diff contamination:
|
||||||
- `line N: apply_patch sentinel "*** …" is not valid in hashline. File sections start with \`¶path#HASH\` (no \`Update File:\` / \`Add File:\` keyword). Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
- `line N: apply_patch sentinel "*** …" is not valid in hashline. File sections start with \`[path#HASH]\` (no \`Update File:\` / \`Add File:\` keyword). Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
||||||
- `line N: unified-diff hunk header (\`@@ -N,M +N,M @@\`) is not valid in hashline. Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
- `line N: unified-diff hunk header (\`@@ -N,M +N,M @@\`) is not valid in hashline. Use \`replace N..M:\`, \`delete N..M\`, or \`insert before|after|head|tail:\` ops.`
|
||||||
- `line N: \`@@\`-bracketed hunk header "@@ …" is not valid in hashline. Drop the \`@@ ... @@\` brackets and write a verb header such as \`replace N..M:\`.`
|
- `line N: \`@@\`-bracketed hunk header "@@ …" is not valid in hashline. Drop the \`@@ ... @@\` brackets and write a verb header such as \`replace N..M:\`.`
|
||||||
- `line N: hunk headers need a verb. Use \`replace N..N:\` to replace, or \`delete N\` to delete.`
|
- `line N: hunk headers need a verb. Use \`replace N..N:\` to replace, or \`delete N\` to delete.`
|
||||||
@@ -193,6 +191,7 @@ delete 20
|
|||||||
- Stale snapshot tag: the `Patcher` first attempts snapshot-based recovery. When recovery cannot prove a valid result it throws `MismatchError`, which distinguishes recognized-but-drifted hashes from never-recorded hashes. The error includes the current file hash plus context around each anchor.
|
- Stale snapshot tag: the `Patcher` first attempts snapshot-based recovery. When recovery cannot prove a valid result it throws `MismatchError`, which distinguishes recognized-but-drifted hashes from never-recorded hashes. The error includes the current file hash plus context around each anchor.
|
||||||
- No-op edit:
|
- No-op edit:
|
||||||
- `Edits to <path> parsed and applied cleanly, but produced no change: your body row(s) are byte-identical to the file at the targeted lines. The bug is somewhere else — re-read the file before issuing another edit. Do NOT widen the payload or add lines; verify the anchor first.`
|
- `Edits to <path> parsed and applied cleanly, but produced no change: your body row(s) are byte-identical to the file at the targeted lines. The bug is somewhere else — re-read the file before issuing another edit. Do NOT widen the payload or add lines; verify the anchor first.`
|
||||||
|
- After `NOOP_HARD_LIMIT = 3` consecutive byte-identical no-ops of the same payload on the same file, the soft text result escalates to a `ToolError` (`STOP. Edits to <path> have been a byte-identical no-op N times in a row …`) from `packages/coding-agent/src/edit/hashline/noop-loop-guard.ts`.
|
||||||
- Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the mismatch error is surfaced unchanged.
|
- Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the mismatch error is surfaced unchanged.
|
||||||
|
|
||||||
## Warnings
|
## Warnings
|
||||||
|
|||||||
+18
-21
@@ -16,10 +16,10 @@
|
|||||||
- `packages/coding-agent/src/eval/js/shared/helpers.ts` — JS filesystem/text/env helper implementations
|
- `packages/coding-agent/src/eval/js/shared/helpers.ts` — JS filesystem/text/env helper implementations
|
||||||
- `packages/coding-agent/src/eval/py/index.ts` — Python backend adapter
|
- `packages/coding-agent/src/eval/py/index.ts` — Python backend adapter
|
||||||
- `packages/coding-agent/src/eval/py/executor.ts` — kernel session retention, reset, cleanup
|
- `packages/coding-agent/src/eval/py/executor.ts` — kernel session retention, reset, cleanup
|
||||||
- `packages/coding-agent/src/eval/py/kernel.ts` — Jupyter gateway/kernel protocol, display capture
|
- `packages/coding-agent/src/eval/py/kernel.ts` — subprocess NDJSON runner protocol, display capture
|
||||||
- `packages/coding-agent/src/eval/py/prelude.py` — Python helper functions and status events
|
- `packages/coding-agent/src/eval/py/prelude.py` — Python helper functions and status events
|
||||||
- `packages/coding-agent/src/session/streaming-output.ts` — truncation, artifacts, streamed chunks
|
- `packages/coding-agent/src/session/streaming-output.ts` — truncation, artifacts, streamed chunks
|
||||||
- `docs/python-repl.md` — Python kernel/gateway internals
|
- `docs/python-repl.md` — Python kernel/runner internals
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@ Each `EvalCellInput` (from `evalCellSchema` in `packages/coding-agent/src/tools/
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `language` | `"py" \| "js"` | Yes | Backend selector. `"py"` maps to the IPython/Jupyter kernel (`python` backend); `"js"` maps to the persistent JavaScript VM. |
|
| `language` | `"py" \| "js"` | Yes | Backend selector. `"py"` maps to the IPython-style subprocess kernel (`python` backend); `"js"` maps to the persistent JavaScript VM. |
|
||||||
| `code` | `string` | Yes | Cell body, verbatim. JSON-encoded — embed newlines, quotes, and indentation directly; no fences, no headers. |
|
| `code` | `string` | Yes | Cell body, verbatim. JSON-encoded — embed newlines, quotes, and indentation directly; no fences, no headers. |
|
||||||
| `title` | `string` | No | Short label rendered in the transcript (e.g. `"imports"`, `"load config"`). |
|
| `title` | `string` | No | Short label rendered in the transcript (e.g. `"imports"`, `"load config"`). |
|
||||||
| `timeout` | `integer` | No | Per-cell timeout in seconds, clamped to `1..600`. Defaults to 30 when omitted. |
|
| `timeout` | `integer` | No | Per-cell timeout in seconds, clamped to `1..3600`. Defaults to 30 when omitted. |
|
||||||
| `reset` | `boolean` | No | Wipe this cell's language kernel before running. Reset is per-language: a `py` cell's reset does not touch the JS VM and vice versa. Defaults to `false`. |
|
| `reset` | `boolean` | No | Wipe this cell's language kernel before running. Reset is per-language: a `py` cell's reset does not touch the JS VM and vice versa. Defaults to `false`. |
|
||||||
|
|
||||||
Minimal example matching the live schema:
|
Minimal example matching the live schema:
|
||||||
@@ -95,7 +95,7 @@ Side-channel artifacts:
|
|||||||
- clamps `cell.timeout ?? 30` seconds through `clampTimeout("eval", ...)`
|
- clamps `cell.timeout ?? 30` seconds through `clampTimeout("eval", ...)`
|
||||||
- builds a combined abort signal from the tool signal, the timeout, and the session abort controller
|
- builds a combined abort signal from the tool signal, the timeout, and the session abort controller
|
||||||
- marks the cell `running` and emits an update
|
- marks the cell `running` and emits an update
|
||||||
- calls the backend’s `execute()` with `cwd`, `sessionId`, `sessionFile`, `kernelOwnerId`, `deadlineMs`, `reset` (defaults to `false`), artifact info, and chunk callback
|
- calls the backend's `execute()` with `cwd`, `sessionId`, `sessionFile`, `kernelOwnerId`, `idleTimeoutMs`, `reset` (defaults to `false`), the combined signal, and chunk/status callbacks
|
||||||
6. JS cells dispatch through `packages/coding-agent/src/eval/js/index.ts` into `executeJs()`; Python cells dispatch through `packages/coding-agent/src/eval/py/index.ts` into `executePython()`.
|
6. JS cells dispatch through `packages/coding-agent/src/eval/js/index.ts` into `executeJs()`; Python cells dispatch through `packages/coding-agent/src/eval/py/index.ts` into `executePython()`.
|
||||||
7. Backend text chunks stream into the shared `OutputSink`; rich outputs are accumulated separately as JSON, images, markdown markers, and status events.
|
7. Backend text chunks stream into the shared `OutputSink`; rich outputs are accumulated separately as JSON, images, markdown markers, and status events.
|
||||||
8. After each cell:
|
8. After each cell:
|
||||||
@@ -113,7 +113,7 @@ Side-channel artifacts:
|
|||||||
|
|
||||||
Backend choice is **explicit per cell** — there is no auto-detection.
|
Backend choice is **explicit per cell** — there is no auto-detection.
|
||||||
|
|
||||||
- `language: "py"` → Python (IPython/Jupyter) backend
|
- `language: "py"` → Python (IPython-style subprocess kernel) backend
|
||||||
- `language: "js"` → JavaScript VM backend
|
- `language: "js"` → JavaScript VM backend
|
||||||
|
|
||||||
If the requested backend is disabled or unavailable, the tool throws `ToolError` for that cell. The caller chooses; the tool does not silently substitute.
|
If the requested backend is disabled or unavailable, the tool throws `ToolError` for that cell. The caller chooses; the tool does not silently substitute.
|
||||||
@@ -150,9 +150,9 @@ Implemented in `packages/coding-agent/src/eval/js/worker-core.ts`, `packages/cod
|
|||||||
|
|
||||||
### Python runtime
|
### Python runtime
|
||||||
|
|
||||||
Implemented in `packages/coding-agent/src/eval/py/executor.ts`, `packages/coding-agent/src/eval/py/kernel.ts`, and `packages/coding-agent/src/eval/py/prelude.py`. See `docs/python-repl.md` for gateway and kernel details.
|
Implemented in `packages/coding-agent/src/eval/py/executor.ts`, `packages/coding-agent/src/eval/py/kernel.ts`, and `packages/coding-agent/src/eval/py/prelude.py`. See `docs/python-repl.md` for kernel and runner details.
|
||||||
|
|
||||||
- Default mode is retained `session` kernels keyed by `python:${sessionId}`
|
- Default mode is retained `session` kernels keyed by `python:${sessionId}` plus normalized cwd and interpreter
|
||||||
- Optional `python.kernelMode = "per-call"` creates a fresh kernel for each cell and shuts it down afterward
|
- Optional `python.kernelMode = "per-call"` creates a fresh kernel for each cell and shuts it down afterward
|
||||||
- `reset: true` disposes the retained kernel for that session before the cell runs; later Python cells in the same tool call reuse the fresh kernel
|
- `reset: true` disposes the retained kernel for that session before the cell runs; later Python cells in the same tool call reuse the fresh kernel
|
||||||
- Startup path:
|
- Startup path:
|
||||||
@@ -163,14 +163,14 @@ Implemented in `packages/coding-agent/src/eval/py/executor.ts`, `packages/coding
|
|||||||
- Python cells run in the runner's persistent asyncio event loop, so top-level `await` works; the prompt warns not to use `asyncio.run(...)`
|
- Python cells run in the runner's persistent asyncio event loop, so top-level `await` works; the prompt warns not to use `asyncio.run(...)`
|
||||||
- The Python prelude defines helpers with the same surface as JS where practical, including `tool.<name>(args)`, `completion(...)`, and `agent(...)` through a per-run loopback bridge
|
- The Python prelude defines helpers with the same surface as JS where practical, including `tool.<name>(args)`, `completion(...)`, and `agent(...)` through a per-run loopback bridge
|
||||||
- Synchronous statement blocks run in the default executor with ContextVar state copied in; the GIL still serializes bytecode execution, but awaited regions can interleave with sibling cells
|
- Synchronous statement blocks run in the default executor with ContextVar state copied in; the GIL still serializes bytecode execution, but awaited regions can interleave with sibling cells
|
||||||
- Kernel `display_data` / `execute_result` messages map to:
|
- Kernel `display` / `result` frames map to:
|
||||||
- `application/x-omp-status` → status event
|
- `application/x-omp-status` → status event
|
||||||
- `image/png` → image output
|
- `image/png` → image output
|
||||||
- `application/json` → JSON output
|
- `application/json` → JSON output
|
||||||
- `text/markdown` → markdown output
|
- `text/markdown` → markdown output
|
||||||
- `text/plain` → text output
|
- `text/plain` → text output
|
||||||
- `text/html` → HTML converted to markdown with `htmlToBasicMarkdown()`
|
- `text/html` → HTML converted to markdown with `htmlToBasicMarkdown()`
|
||||||
- Interactive stdin is rejected: `input_request` sends an empty reply, marks `stdinRequested`, and the executor returns exit code `1`
|
- Interactive stdin is rejected: a stdin-flagged result returns exit code `1` with `Kernel requested stdin; interactive input is not supported.`
|
||||||
|
|
||||||
### Oneshot completion helper (`completion`)
|
### Oneshot completion helper (`completion`)
|
||||||
|
|
||||||
@@ -239,21 +239,18 @@ A single tool call can mix Python and JS cells. Persistence is per language runt
|
|||||||
## Limits & Caps
|
## Limits & Caps
|
||||||
|
|
||||||
- Per-cell timeout default: 30s (applied when `timeout` is omitted in `EvalTool.execute()`; clamped through `TOOL_TIMEOUTS.eval.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`)
|
- Per-cell timeout default: 30s (applied when `timeout` is omitted in `EvalTool.execute()`; clamped through `TOOL_TIMEOUTS.eval.default` in `packages/coding-agent/src/tools/tool-timeouts.ts`)
|
||||||
- Schema-level `timeout` range: integer `1..600` seconds (enforced by Zod on the cell schema)
|
- Schema-level `timeout` range: integer `1..3600` seconds (enforced by Zod on the cell schema)
|
||||||
- Timeout clamp at runtime: 1s minimum, 600s maximum (`TOOL_TIMEOUTS.eval` in `packages/coding-agent/src/tools/tool-timeouts.ts`)
|
- Timeout clamp at runtime: 1s minimum, 3600s maximum (`TOOL_TIMEOUTS.eval` in `packages/coding-agent/src/tools/tool-timeouts.ts`)
|
||||||
- Transcript code/output preview: 10 lines by default (`EVAL_DEFAULT_PREVIEW_LINES` in `packages/coding-agent/src/tools/eval.ts`)
|
- Transcript code/output preview: 10 lines by default (`EVAL_DEFAULT_PREVIEW_LINES` in `packages/coding-agent/src/tools/eval-render.ts`, re-exported from `eval.ts`)
|
||||||
- Output truncation window: 50KB default (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`)
|
- Output truncation window: 50KB default (`DEFAULT_MAX_BYTES` in `packages/coding-agent/src/session/streaming-output.ts`)
|
||||||
- Output line cap inside truncation helpers: 3000 lines (`DEFAULT_MAX_LINES` in `packages/coding-agent/src/session/streaming-output.ts`)
|
- Output line cap inside truncation helpers: 3000 lines (`DEFAULT_MAX_LINES` in `packages/coding-agent/src/session/streaming-output.ts`)
|
||||||
- Streaming tail buffer for live updates: `DEFAULT_MAX_BYTES * 2` = 100KB (`packages/coding-agent/src/tools/eval.ts`)
|
- Streaming tail buffer for live updates: `DEFAULT_MAX_BYTES * 2` = 100KB (`packages/coding-agent/src/tools/eval.ts`)
|
||||||
- JS/Python `parallel()` / `pipeline()` helper pool width: the `task.maxConcurrency` setting (default 32; `0` = unbounded), resolved live via the `__concurrency__` bridge (`packages/coding-agent/src/eval/concurrency-bridge.ts`)
|
- JS/Python `parallel()` / `pipeline()` helper pool width: the `task.maxConcurrency` setting (default 32; `0` = unbounded), resolved live via the `__concurrency__` bridge (`packages/coding-agent/src/eval/concurrency-bridge.ts`)
|
||||||
- Eval-driven `agent()` recursion cap: task depth 3 (`EVAL_AGENT_MAX_DEPTH`)
|
- Eval-driven `agent()` recursion cap: task depth 3 (`EVAL_AGENT_MAX_DEPTH`)
|
||||||
- Python retained kernel idle timeout: 5 minutes (`IDLE_TIMEOUT_MS` in `packages/coding-agent/src/eval/py/executor.ts`)
|
- Python kernel startup wait: 10s (`STARTUP_TIMEOUT_MS` in `packages/coding-agent/src/eval/py/kernel.ts`)
|
||||||
- Python retained kernel cap: 4 sessions (`MAX_KERNEL_SESSIONS` in `packages/coding-agent/src/eval/py/executor.ts`)
|
- Python kernel shutdown grace per escalation step (`exit` request → `SIGTERM` → `SIGKILL`): 1000ms (`SHUTDOWN_GRACE_MS` in `packages/coding-agent/src/eval/py/kernel.ts`)
|
||||||
- Python retained kernel cleanup sweep: every 30s (`CLEANUP_INTERVAL_MS` in `packages/coding-agent/src/eval/py/executor.ts`)
|
- Python SIGINT escalation window: 5s without a `done` frame before the subprocess is killed (`INTERRUPT_ESCALATION_MS` in `packages/coding-agent/src/eval/py/kernel.ts`)
|
||||||
- Python owner-cleanup shutdown wait: 2000ms (`OWNER_CLEANUP_KERNEL_SHUTDOWN_TIMEOUT_MS` in `packages/coding-agent/src/eval/py/executor.ts`)
|
- Python auto-restart budget: a dead retained kernel is replaced and the cell retried once per execution (`executeOnSession` in `packages/coding-agent/src/eval/py/executor.ts`)
|
||||||
- Python heartbeat interval: 5s (`ensureKernelHeartbeat()` in `packages/coding-agent/src/eval/py/executor.ts`)
|
|
||||||
- Python external gateway availability check timeout: 5s (`AbortSignal.timeout(5000)` in `packages/coding-agent/src/eval/py/kernel.ts`)
|
|
||||||
- Python auto-restart budget: one restart per retained session before hard failure (`restartCount > 1` in `packages/coding-agent/src/eval/py/executor.ts`)
|
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
@@ -274,7 +271,7 @@ A single tool call can mix Python and JS cells. Persistence is per language runt
|
|||||||
- Parent agents and subagents share eval state bidirectionally when a subagent inherits the parent's executor id. Mutations in either direction are visible to the other participant.
|
- Parent agents and subagents share eval state bidirectionally when a subagent inherits the parent's executor id. Mutations in either direction are visible to the other participant.
|
||||||
- Async regions of concurrent runs can interleave. Synchronous JS still blocks the VM event loop; synchronous Python still contends on the GIL.
|
- Async regions of concurrent runs can interleave. Synchronous JS still blocks the VM event loop; synchronous Python still contends on the GIL.
|
||||||
- Cancelling one run is destructive to the shared backend executor. This is intentional: JS worker termination and Python SIGINT/subprocess shutdown are the only reliable way to interrupt arbitrary user code.
|
- Cancelling one run is destructive to the shared backend executor. This is intentional: JS worker termination and Python SIGINT/subprocess shutdown are the only reliable way to interrupt arbitrary user code.
|
||||||
- `reset: true` is destructive for every live run on that backend session id. New starts on that backend are rejected while reset is in flight.
|
- `reset: true` is destructive for every live run on that backend session id. Concurrent Python resets coalesce — a reset already in flight is awaited rather than duplicated, and runs queued behind it proceed on the freshly-restarted kernel.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `paths` | `string[]` | Yes | One or more globs, files, directories, or internal URLs with backing files. Empty strings are rejected. Single entries accidentally joined with comma, semicolon, or whitespace are expanded only after existence validation; existing paths containing delimiters stay intact. Multiple entries may be merged into one brace-union search when their base paths can be resolved together. |
|
| `paths` | `string[]` | Yes | One or more globs, files, directories, or internal URLs with backing files. Empty strings are rejected. Single entries accidentally joined with comma, semicolon, or whitespace are expanded only after existence validation; existing paths containing delimiters stay intact. Each entry becomes its own walk root; multi-entry calls run those scans concurrently. |
|
||||||
| `hidden` | `boolean` | No | Whether hidden files are included. Defaults to `true` (`hidden ?? true`). |
|
| `hidden` | `boolean` | No | Whether hidden files are included. Defaults to `true` (`hidden ?? true`). |
|
||||||
| `gitignore` | `boolean` | No | Whether `.gitignore` is respected during local native globbing. Defaults to `true`; set `false` to include gitignored files. |
|
| `gitignore` | `boolean` | No | Whether `.gitignore` is respected during local native globbing. Defaults to `true`; set `false` to include gitignored files. |
|
||||||
| `limit` | `number` | No | Max returned paths. Defaults to `200`; finite positive inputs are floored then clamped to `1..200`. |
|
| `limit` | `number` | No | Max returned paths. Defaults to `200`; finite positive inputs are floored then clamped to `1..200`. |
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
## Outputs
|
## Outputs
|
||||||
The tool returns a single text block plus structured `details`.
|
The tool returns a single text block plus structured `details`.
|
||||||
|
|
||||||
- Success text: matching paths grouped by directory. Each non-root group starts with `# <dir>/` and then lists basenames; root-level matches are listed without a header. Directory matches carry a trailing `/`. Exact file inputs return that file path as one line.
|
- Success text: matching paths grouped as a multi-level, prefix-folded directory tree (`formatGroupedPaths()`): one `#` per nesting level, single-child directory chains fold into one header (`# a/b/c/`), and files are listed bare under the deepest owning header; root-level matches are listed without a header. Directory matches carry a trailing `/`. Exact file inputs return that file path as one line.
|
||||||
- Empty result text: `No files found matching pattern`, optionally followed by a timeout or missing-path notice.
|
- Empty result text: `No files found matching pattern`, optionally followed by a timeout or missing-path notice.
|
||||||
- Multi-path partial miss: appends `Skipped missing paths: ...` after the result block, or after the empty-result line.
|
- Multi-path partial miss: appends `Skipped missing paths: ...` after the result block, or after the empty-result line.
|
||||||
- `details` may include:
|
- `details` may include:
|
||||||
@@ -45,7 +45,7 @@ The tool returns a single text block plus structured `details`.
|
|||||||
1. `FindTool.execute()` expands delimiter-flattened local `paths` entries with `expandDelimitedPathEntries(..., parseFindPattern)` unless custom operations are injected. The splitter validates candidate parts by statting their parsed base paths, keeps existing delimiter-containing paths intact, accepts comma/semicolon splits when at least one part resolves, and accepts whitespace splits only when every part resolves.
|
1. `FindTool.execute()` expands delimiter-flattened local `paths` entries with `expandDelimitedPathEntries(..., parseFindPattern)` unless custom operations are injected. The splitter validates candidate parts by statting their parsed base paths, keeps existing delimiter-containing paths intact, accepts comma/semicolon splits when at least one part resolves, and accepts whitespace splits only when every part resolves.
|
||||||
2. The tool normalizes each resulting entry with `normalizePathLikeInput()` and `/\\/g -> "/"` (`packages/coding-agent/src/tools/find.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.
|
2. The tool normalizes each resulting entry with `normalizePathLikeInput()` and `/\\/g -> "/"` (`packages/coding-agent/src/tools/find.ts`). Empty normalized entries fail with `` `paths` must contain non-empty globs or paths ``.
|
||||||
3. For multi-path local calls, `partitionExistingPaths(..., parseFindPattern)` (`packages/coding-agent/src/tools/path-utils.ts`) stats each base path. Missing entries are skipped; if all are missing, the tool throws `Path not found: ...`. Single missing paths still hard-fail.
|
3. For multi-path local calls, `partitionExistingPaths(..., parseFindPattern)` (`packages/coding-agent/src/tools/path-utils.ts`) stats each base path. Missing entries are skipped; if all are missing, the tool throws `Path not found: ...`. Single missing paths still hard-fail.
|
||||||
4. The tool tries `resolveExplicitFindPatterns()` to merge multiple inputs into one search rooted at a common base path. If that does not apply, it parses one input with `parseFindPattern()`.
|
4. The tool calls `resolveExplicitFindPatterns()` for multi-entry calls; it parses each entry into its own `(basePath, globPattern, hasGlob)` target so every path is walked as its own root (collapsing to a shared ancestor would scan unrelated siblings). Single-entry calls parse with `parseFindPattern()` directly.
|
||||||
5. `parseFindPattern()` determines `(basePath, globPattern, hasGlob)`:
|
5. `parseFindPattern()` determines `(basePath, globPattern, hasGlob)`:
|
||||||
- no glob chars (`*`, `?`, `[`, `{`) => search that path with implicit `**/*`.
|
- no glob chars (`*`, `?`, `[`, `{`) => search that path with implicit `**/*`.
|
||||||
- glob in the first segment => search from `.` and, unless the pattern already starts with `**/`, prefix it with `**/`.
|
- glob in the first segment => search from `.` and, unless the pattern already starts with `**/`, prefix it with `**/`.
|
||||||
@@ -54,17 +54,17 @@ The tool returns a single text block plus structured `details`.
|
|||||||
7. `limit` defaults to `DEFAULT_LIMIT` (`200`), must be positive and finite, is floored, then clamped to `MAX_LIMIT` (`200`). `hidden` and `gitignore` both default to `true`. `timeout` is converted to milliseconds and clamped to `500..60_000` before building an `AbortSignal.timeout(...)`.
|
7. `limit` defaults to `DEFAULT_LIMIT` (`200`), must be positive and finite, is floored, then clamped to `MAX_LIMIT` (`200`). `hidden` and `gitignore` both default to `true`. `timeout` is converted to milliseconds and clamped to `500..60_000` before building an `AbortSignal.timeout(...)`.
|
||||||
8. Execution then branches:
|
8. Execution then branches:
|
||||||
- **Custom operations branch**: if `FindToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit })`.
|
- **Custom operations branch**: if `FindToolOptions.operations.glob` exists, the tool checks existence with `operations.exists()`, short-circuits exact-file inputs via `operations.stat()` when available, then calls `operations.glob(globPattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit })`.
|
||||||
- **Built-in local branch**: the tool stats `searchPath`. Exact-file inputs return immediately. Directory inputs call `natives.glob()` with `hidden`, `maxResults: effectiveLimit`, `sortByMtime: true`, `gitignore: useGitignore`, and the combined abort signal.
|
- **Built-in local branch**: the tool stats each target's `searchPath`. Exact-file inputs return immediately. Directory inputs call `natives.glob()` with `hidden`, `maxResults: effectiveLimit`, `sortByMtime: true`, `gitignore: useGitignore`, `recursive: false` (recursion comes from the `**/` prefix `parseFindPattern()` adds), and the combined abort signal; multi-target calls run their globs concurrently.
|
||||||
9. In the local branch, optional `onMatch` callbacks convert each match to a cwd-relative display path and emit throttled progress updates.
|
9. In the local branch, optional `onMatch` callbacks convert each match to a cwd-relative display path and emit throttled progress updates.
|
||||||
10. After native glob returns, JS sorts `result.matches` by `mtime` descending (`(b.mtime ?? 0) - (a.mtime ?? 0)`) before formatting paths.
|
10. After native glob returns, JS merges per-target results, deduplicates repeated display paths, and sorts the merged list by `mtime` descending before formatting paths.
|
||||||
11. `buildResult()` applies `applyListLimit()` to cap the array again at `effectiveLimit`, formats paths with `formatFindGroupedOutput()`, appends notices, then runs `truncateHead()` with `maxLines: Number.MAX_SAFE_INTEGER`. In practice this leaves the 50 KB byte cap in place while disabling the default 3000-line cap.
|
11. `buildResult()` applies `applyListLimit()` to cap the array again at `effectiveLimit`, formats paths with `formatGroupedPaths()` (from `@oh-my-pi/pi-utils`), appends notices, then runs `truncateHead()` with `maxLines: Number.MAX_SAFE_INTEGER`. In practice this leaves the 50 KB byte cap in place while disabling the default 3000-line cap.
|
||||||
12. `toolResult()` packages text plus `details`, and records result-limit / truncation metadata for renderers.
|
12. `toolResult()` packages text plus `details`, and records result-limit / truncation metadata for renderers.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
- **Exact file path**: if the parsed input has no glob and the resolved path stats as a file, output is that one path.
|
- **Exact file path**: if the parsed input has no glob and the resolved path stats as a file, output is that one path.
|
||||||
- **Directory path**: if the parsed input has no glob and stats as a directory, the tool searches it with implicit `**/*`.
|
- **Directory path**: if the parsed input has no glob and stats as a directory, the tool searches it with implicit `**/*`.
|
||||||
- **Single glob path**: one input parsed by `parseFindPattern()`.
|
- **Single glob path**: one input parsed by `parseFindPattern()`.
|
||||||
- **Merged multi-path search**: multiple inputs resolved by `resolveExplicitFindPatterns()` into one brace-union glob rooted at a common base path.
|
- **Multi-path search**: multiple inputs resolved by `resolveExplicitFindPatterns()` into per-entry targets, each walked as its own root concurrently and merged afterwards.
|
||||||
- **Partial multi-path search with missing inputs**: local multi-path calls skip missing base paths and surface them as `missingPaths` / `Skipped missing paths: ...`.
|
- **Partial multi-path search with missing inputs**: local multi-path calls skip missing base paths and surface them as `missingPaths` / `Skipped missing paths: ...`.
|
||||||
- **Internal URL input**: supported when the internal router resolves the URL to a backing file. Internal URL globs are rejected.
|
- **Internal URL input**: supported when the internal router resolves the URL to a backing file. Internal URL globs are rejected.
|
||||||
- **Custom delegated search**: uses injected `FindOperations` instead of local fs + native glob.
|
- **Custom delegated search**: uses injected `FindOperations` instead of local fs + native glob.
|
||||||
@@ -107,6 +107,6 @@ The tool returns a single text block plus structured `details`.
|
|||||||
- Bare top-level globs are made recursive. `*.ts` is parsed as base `.` plus glob `**/*.ts`; `src/*.ts` stays rooted at `src` with a non-recursive `*.ts` segment; `src/**/*.ts` preserves explicit recursion.
|
- Bare top-level globs are made recursive. `*.ts` is parsed as base `.` plus glob `**/*.ts`; `src/*.ts` stays rooted at `src` with a non-recursive `*.ts` segment; `src/**/*.ts` preserves explicit recursion.
|
||||||
- `.gitignore` defaults to enabled in the built-in local branch. Use `gitignore: false` to disable it for native traversal.
|
- `.gitignore` defaults to enabled in the built-in local branch. Use `gitignore: false` to disable it for native traversal.
|
||||||
- `hidden` defaults to `true`; hidden-file exclusion is opt-out, not opt-in.
|
- `hidden` defaults to `true`; hidden-file exclusion is opt-out, not opt-in.
|
||||||
- Multi-path missing-input tolerance only applies in the built-in local branch. The custom-operations branch hard-fails the first missing `searchPath` it checks.
|
- Multi-path missing-input tolerance applies in both branches, but only the built-in local branch surfaces `missingPaths` / `Skipped missing paths: ...`. The custom-operations branch hard-fails a missing `searchPath` only for single-input calls; in multi-input calls a missing target silently contributes no results.
|
||||||
- The custom `FindOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.
|
- The custom `FindOperations.glob()` hook receives `ignore` and `limit`, but not the `hidden` flag or an explicit `.gitignore` toggle. A remote delegate must account for that itself if it wants parity with the local branch.
|
||||||
- Built-in local globbing does not force `fileType: File`; it can return files and directories from native glob. Directory outputs also occur through exact-path passthrough or custom delegates that return them.
|
- Built-in local globbing does not force `fileType: File`; it can return files and directories from native glob. Directory outputs also occur through exact-path passthrough or custom delegates that return them.
|
||||||
@@ -68,7 +68,7 @@ The tool returns a single text result built by `buildTextResult()` in `packages/
|
|||||||
7. `pr_checkout` resolves PR metadata first, then enters `git.withRepoLock()` before any git mutation so parallel checkout calls for the same primary repo do not race on shared `.git` state.
|
7. `pr_checkout` resolves PR metadata first, then enters `git.withRepoLock()` before any git mutation so parallel checkout calls for the same primary repo do not race on shared `.git` state.
|
||||||
8. `pr_push` reads PR head metadata back from git branch config, derives a refspec, then pushes with `git.push()`.
|
8. `pr_push` reads PR head metadata back from git branch config, derives a refspec, then pushes with `git.push()`.
|
||||||
9. `pr_create` shells out once, then best-effort re-reads the created PR for a richer summary.
|
9. `pr_create` shells out once, then best-effort re-reads the created PR for a richer summary.
|
||||||
10. `run_watch` chooses either run mode (`run` supplied) or commit mode (`run` omitted), polls GitHub Actions APIs every 3 seconds, emits streaming updates, and may save a full failed-log artifact before returning.
|
10. `run_watch` chooses either run mode (`run` supplied) or commit mode (`run` omitted), polls GitHub Actions APIs every 3 seconds for the first minute and every 15 seconds after that, emits streaming updates, and may save a full failed-log artifact before returning.
|
||||||
11. Final text goes through `toolResult().text(...)`; if `session.allocateOutputArtifact()` returns a slot, failed-log text is persisted with `Bun.write()`.
|
11. Final text goes through `toolResult().text(...)`; if `session.allocateOutputArtifact()` returns a slot, failed-log text is persisted with `Bun.write()`.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
@@ -213,7 +213,7 @@ Push target resolution reads the `branch.<name>.ompPrHeadRef`, `pushRemote`/`rem
|
|||||||
|
|
||||||
Watch flow:
|
Watch flow:
|
||||||
- `run` parsing accepts either a decimal run ID or a full run URL. URL repo must match explicit `repo` when both are given.
|
- `run` parsing accepts either a decimal run ID or a full run URL. URL repo must match explicit `repo` when both are given.
|
||||||
- Poll interval is fixed at 3 seconds (`RUN_WATCH_INTERVAL_DEFAULT`).
|
- Poll interval is `3` seconds (`RUN_WATCH_INTERVAL_DEFAULT`) for the first `60` seconds of the watch (`RUN_WATCH_FAST_WINDOW_MS`), then `15` seconds (`RUN_WATCH_INTERVAL_SLOW`). Rate-limited poll errors back off at the slow interval and are retried up to `5` consecutive failures (`RUN_WATCH_MAX_POLL_FAILURES`). Commit mode gives up with a clear message after `90` seconds if no runs ever appear (`RUN_WATCH_NO_RUNS_GIVE_UP_MS`).
|
||||||
- Failure grace period is fixed at 5 seconds (`RUN_WATCH_GRACE_DEFAULT`). When any failed job appears before completion, the tool emits a note, waits once, re-fetches state, then collects logs so concurrent failures are included.
|
- Failure grace period is fixed at 5 seconds (`RUN_WATCH_GRACE_DEFAULT`). When any failed job appears before completion, the tool emits a note, waits once, re-fetches state, then collects logs so concurrent failures are included.
|
||||||
- Failed-job logs are fetched with `gh api /repos/<repo>/actions/jobs/<jobId>/logs` via `git.github.run()`, not `json()`. Non-zero exit leaves `available: false` instead of failing the whole watch.
|
- Failed-job logs are fetched with `gh api /repos/<repo>/actions/jobs/<jobId>/logs` via `git.github.run()`, not `json()`. Non-zero exit leaves `available: false` instead of failing the whole watch.
|
||||||
- Inline result includes only the last `tail` lines per failed job. The saved artifact contains full logs (`mode: "full"`).
|
- Inline result includes only the last `tail` lines per failed job. The saved artifact contains full logs (`mode: "full"`).
|
||||||
@@ -245,7 +245,7 @@ Watch flow:
|
|||||||
- Search result default: `10` (`SEARCH_LIMIT_DEFAULT` in `packages/coding-agent/src/tools/gh.ts`).
|
- Search result default: `10` (`SEARCH_LIMIT_DEFAULT` in `packages/coding-agent/src/tools/gh.ts`).
|
||||||
- Search result max: `50` (`SEARCH_LIMIT_MAX`).
|
- Search result max: `50` (`SEARCH_LIMIT_MAX`).
|
||||||
- PR file preview inside the `pr://` view: first `50` files only (`FILE_PREVIEW_LIMIT` in `gh.ts`).
|
- PR file preview inside the `pr://` view: first `50` files only (`FILE_PREVIEW_LIMIT` in `gh.ts`).
|
||||||
- Run-watch poll interval: `3s` (`RUN_WATCH_INTERVAL_DEFAULT`).
|
- Run-watch poll interval: `3s` for the first `60s`, then `15s` (`RUN_WATCH_INTERVAL_DEFAULT`, `RUN_WATCH_FAST_WINDOW_MS`, `RUN_WATCH_INTERVAL_SLOW`); commit mode with no runs gives up after `90s` (`RUN_WATCH_NO_RUNS_GIVE_UP_MS`); up to `5` consecutive rate-limited poll failures are tolerated (`RUN_WATCH_MAX_POLL_FAILURES`).
|
||||||
- Run-watch failure grace period: `5s` (`RUN_WATCH_GRACE_DEFAULT`).
|
- Run-watch failure grace period: `5s` (`RUN_WATCH_GRACE_DEFAULT`).
|
||||||
- Run-watch failed-log tail default: `15` lines (`RUN_WATCH_TAIL_DEFAULT`).
|
- Run-watch failed-log tail default: `15` lines (`RUN_WATCH_TAIL_DEFAULT`).
|
||||||
- Run-watch failed-log tail max: `200` lines (`RUN_WATCH_TAIL_MAX`).
|
- Run-watch failed-log tail max: `200` lines (`RUN_WATCH_TAIL_MAX`).
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ TUI rendering adds presentation-only truncation from `packages/coding-agent/src/
|
|||||||
- `{ type: "text", text: params.question }`
|
- `{ type: "text", text: params.question }`
|
||||||
10. `systemPrompt` is a one-element array rendered from `packages/coding-agent/src/prompts/tools/inspect-image-system.md`; telemetry is tagged with oneshot kind `inspect_image`.
|
10. `systemPrompt` is a one-element array rendered from `packages/coding-agent/src/prompts/tools/inspect-image-system.md`; telemetry is tagged with oneshot kind `inspect_image`.
|
||||||
11. If the model response stop reason is `error` or `aborted`, the tool maps that to `ToolError`.
|
11. If the model response stop reason is `error` or `aborted`, the tool maps that to `ToolError`.
|
||||||
12. `extractResponseText(...)` concatenates only `text` content blocks from the assistant message, trims the result, and fails if nothing remains.
|
12. `extractTextContent(...)` from `packages/coding-agent/src/commit/utils.ts` concatenates only `text` content blocks from the assistant message, trims the result, and the tool fails if nothing remains.
|
||||||
13. Success returns the text plus `details`; `inspectImageToolRenderer` formats the result for the TUI.
|
13. Success returns the text plus `details`; `inspectImageToolRenderer` formats the result for the TUI.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
@@ -75,16 +75,17 @@ TUI rendering adds presentation-only truncation from `packages/coding-agent/src/
|
|||||||
## Limits & Caps
|
## Limits & Caps
|
||||||
- Supported detected input formats: `image/png`, `image/jpeg`, `image/gif`, `image/webp` (`SUPPORTED_IMAGE_MIME_TYPES` in `packages/utils/src/mime.ts`).
|
- Supported detected input formats: `image/png`, `image/jpeg`, `image/gif`, `image/webp` (`SUPPORTED_IMAGE_MIME_TYPES` in `packages/utils/src/mime.ts`).
|
||||||
- Metadata sniff cap: `DEFAULT_IMAGE_METADATA_HEADER_BYTES = 256 * 1024` bytes. Format detection only reads up to 256 KiB from the file header.
|
- Metadata sniff cap: `DEFAULT_IMAGE_METADATA_HEADER_BYTES = 256 * 1024` bytes. Format detection only reads up to 256 KiB from the file header.
|
||||||
|
- Availability is gated by `inspect_image.enabled`, default `false`, in `packages/coding-agent/src/config/settings-schema.ts` / `packages/coding-agent/src/tools/index.ts`.
|
||||||
- Upload input cap: `MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024` bytes (20 MiB) in `packages/coding-agent/src/utils/image-loading.ts`.
|
- Upload input cap: `MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024` bytes (20 MiB) in `packages/coding-agent/src/utils/image-loading.ts`.
|
||||||
- Auto-resize defaults in `packages/coding-agent/src/utils/image-resize.ts`:
|
- Auto-resize defaults in `packages/coding-agent/src/utils/image-resize.ts`:
|
||||||
- `maxWidth: 1568`
|
- `maxWidth: 1568`
|
||||||
- `maxHeight: 1568`
|
- `maxHeight: 1568`
|
||||||
- `maxBytes: 500 * 1024` bytes (500 KiB target)
|
- `maxBytes: 500 * 1024` bytes (500 KiB target)
|
||||||
- `jpegQuality: 75`
|
- `jpegQuality: 80`
|
||||||
- Resize fast path: if the original image is already within `1568x1568` and within `maxBytes / 4` (125 KiB by default), `resizeImage(...)` returns the original bytes unchanged.
|
- Resize fast path: if the original image is already within `1568x1568` and within `maxBytes / 4` (125 KiB by default), `resizeImage(...)` returns the original bytes unchanged.
|
||||||
- Resize quality ladder: after the first encode pass, lossy retries use qualities `[70, 60, 50, 40]`.
|
- Resize quality ladder: after the first encode pass, lossy retries use qualities `[70, 60, 50, 40]`.
|
||||||
- Resize dimension ladder: if quality reduction still misses the byte target, retries scale dimensions by `[1.0, 0.75, 0.5, 0.35, 0.25]` and stop if either dimension would fall below `100` pixels.
|
- Resize dimension ladder: if quality reduction still misses the byte target, retries scale dimensions by `[1.0, 0.75, 0.5, 0.35, 0.25]` and stop if either dimension would fall below `100` pixels.
|
||||||
- First resize pass encodes PNG, JPEG, and WebP, then keeps the smallest encoded buffer. Fallback passes encode JPEG and WebP only, again keeping the smaller output.
|
- First resize pass encodes PNG, JPEG, and WebP, then keeps the smallest encoded buffer. Fallback passes encode JPEG and WebP only, again keeping the smaller output. WebP is excluded from both ladders when `OMP_NO_WEBP=1`/`true` (or `excludeWebP` is passed).
|
||||||
- Renderer caps:
|
- Renderer caps:
|
||||||
- `INSPECT_QUESTION_PREVIEW_WIDTH = 100`
|
- `INSPECT_QUESTION_PREVIEW_WIDTH = 100`
|
||||||
- `INSPECT_OUTPUT_COLLAPSED_LINES = 4`
|
- `INSPECT_OUTPUT_COLLAPSED_LINES = 4`
|
||||||
|
|||||||
+5
-4
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `poll` | `string[]` | No | Job ids to watch. Cannot be combined with `list`. If omitted (and `cancel` is also omitted), the tool watches all running jobs. If provided, missing ids are silently filtered out before waiting. |
|
| `poll` | `string[]` | No | Job ids to watch. Cannot be combined with `list`. If omitted (and `cancel` is also omitted), the tool watches all running jobs owned by the calling agent. If provided, missing ids — and ids owned by other agents — are silently filtered out before waiting. |
|
||||||
| `cancel` | `string[]` | No | Job ids to cancel before any polling. Missing ids are reported as `not_found`; non-running ids as `already_completed`. |
|
| `cancel` | `string[]` | No | Job ids to cancel before any polling. Missing ids (and other agents' jobs) are reported as `not_found`; non-running ids as `already_completed`. |
|
||||||
| `list` | `boolean` | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with `poll` or `cancel`. |
|
| `list` | `boolean` | No | Return an immediate snapshot of every job spawned by the calling agent (running + completed within retention) without waiting. Read-only — cannot be combined with `poll` or `cancel`. |
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
@@ -54,8 +54,8 @@ Read-only snapshot path:
|
|||||||
- only `cancel` present → return immediately, no wait.
|
- only `cancel` present → return immediately, no wait.
|
||||||
- explicit `poll`, or no args at all → proceed to watch jobs.
|
- explicit `poll`, or no args at all → proceed to watch jobs.
|
||||||
5. Watch set resolution:
|
5. Watch set resolution:
|
||||||
- explicit `poll` → map ids through `manager.getJob(...)` and drop missing ones.
|
- explicit `poll` → resolve ids via `#visibleJobs(...)`, dropping missing ids and jobs owned by other agents.
|
||||||
- no `poll` and no `cancel` → `manager.getRunningJobs()`.
|
- no `poll` and no `cancel` → `manager.getRunningJobs(ownerFilter)` (jobs owned by the calling agent).
|
||||||
6. Empty watch set returns immediately:
|
6. Empty watch set returns immediately:
|
||||||
- if cancellations happened, return snapshots for the cancelled ids that still exist.
|
- if cancellations happened, return snapshots for the cancelled ids that still exist.
|
||||||
- else return either `No matching jobs found for IDs: ...` or `No running background jobs to wait for.`
|
- else return either `No matching jobs found for IDs: ...` or `No running background jobs to wait for.`
|
||||||
@@ -128,6 +128,7 @@ Lifecycle and exact state names:
|
|||||||
- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.
|
- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.
|
||||||
- Failures inside the underlying async work are stored on the job (`status: "failed"`, `errorText`) and reported in normal tool output, not rethrown by `job`.
|
- Failures inside the underlying async work are stored on the job (`status: "failed"`, `errorText`) and reported in normal tool output, not rethrown by `job`.
|
||||||
- Calling `list: true` against an empty manager returns a normal empty-list result rather than throwing; missing ids passed to `poll` are silently filtered.
|
- Calling `list: true` against an empty manager returns a normal empty-list result rather than throwing; missing ids passed to `poll` are silently filtered.
|
||||||
|
- Combining `list` with `poll` or `cancel` throws a `ToolError`: `` `list` cannot be combined with `poll` or `cancel`. ``
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.
|
- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.
|
||||||
|
|||||||
+13
-10
@@ -27,7 +27,7 @@
|
|||||||
| `action` | string enum | Yes | One of `diagnostics`, `definition`, `references`, `hover`, `symbols`, `rename`, `rename_file`, `code_actions`, `type_definition`, `implementation`, `status`, `reload`, `capabilities`, `request`. |
|
| `action` | string enum | Yes | One of `diagnostics`, `definition`, `references`, `hover`, `symbols`, `rename`, `rename_file`, `code_actions`, `type_definition`, `implementation`, `status`, `reload`, `capabilities`, `request`. |
|
||||||
| `file` | string | No | File path; for `diagnostics` also a glob; for workspace forms use `"*"`; for `rename_file` this is the source path. |
|
| `file` | string | No | File path; for `diagnostics` also a glob; for workspace forms use `"*"`; for `rename_file` this is the source path. |
|
||||||
| `line` | number | No | 1-indexed line number for position-based actions. Defaults to `1` on the single-file action path. |
|
| `line` | number | No | 1-indexed line number for position-based actions. Defaults to `1` on the single-file action path. |
|
||||||
| `symbol` | string | No | Substring used to resolve the column on `line`. Supports `name#N` occurrence selectors; `N` is 1-indexed and defaults to `1`. |
|
| `symbol` | string | No | Substring used to resolve the column on `line`. Supports `name#N` occurrence selectors; `N` is 1-indexed and defaults to `1`. Required when `line` is given for `definition`/`references`/`rename` against project-aware servers. |
|
||||||
| `query` | string | No | Workspace symbol query, code-action selector/filter, or LSP method name for `action=request`. |
|
| `query` | string | No | Workspace symbol query, code-action selector/filter, or LSP method name for `action=request`. |
|
||||||
| `new_name` | string | No | Required for `rename` and `rename_file`. |
|
| `new_name` | string | No | Required for `rename` and `rename_file`. |
|
||||||
| `apply` | boolean | No | For `rename`/`rename_file`, apply unless explicitly `false`. For `code_actions`, list unless explicitly `true`. |
|
| `apply` | boolean | No | For `rename`/`rename_file`, apply unless explicitly `false`. For `code_actions`, list unless explicitly `true`. |
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
- `status` ignores `file`.
|
- `status` ignores `file`.
|
||||||
- `capabilities` with omitted `file` or `"*"` inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.
|
- `capabilities` with omitted `file` or `"*"` inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.
|
||||||
- `request` with omitted `file` or `"*"` chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.
|
- `request` with omitted `file` or `"*"` chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.
|
||||||
- `rename_file` sends `workspace/willRenameFiles` and `workspace/didRenameFiles` to every non-custom LSP server from `getLspServers(config)`, not just one file-scoped server.
|
- `rename_file` sends `workspace/willRenameFiles` and `workspace/didRenameFiles` to every non-custom LSP server from `getLspServers(config)` whose `fileTypes` match the source, destination, or any enumerated rename pair — not just one file-scoped server.
|
||||||
- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (`BiomeClient`, `SwiftLintClient`, or `LspLinterClient`).
|
- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (`BiomeClient`, `SwiftLintClient`, or `LspLinterClient`).
|
||||||
|
|
||||||
### `diagnostics`
|
### `diagnostics`
|
||||||
@@ -90,6 +90,7 @@
|
|||||||
**Execution**
|
**Execution**
|
||||||
- Sends `textDocument/definition` with `{ textDocument, position }`.
|
- Sends `textDocument/definition` with `{ textDocument, position }`.
|
||||||
- Accepts `Location`, `Location[]`, `LocationLink`, or `LocationLink[]`; `normalizeLocationResult()` converts `LocationLink` to `targetSelectionRange ?? targetRange`.
|
- Accepts `Location`, `Location[]`, `LocationLink`, or `LocationLink[]`; `normalizeLocationResult()` converts `LocationLink` to `targetSelectionRange ?? targetRange`.
|
||||||
|
- Requires `symbol` when `line` is given on project-aware servers (the first-non-whitespace-column fallback is disabled for this action).
|
||||||
- Waits for project load before the request.
|
- Waits for project load before the request.
|
||||||
|
|
||||||
**Output text**
|
**Output text**
|
||||||
@@ -108,6 +109,7 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
|
|
||||||
**Execution**
|
**Execution**
|
||||||
- Sends `textDocument/references` with `includeDeclaration: true`.
|
- Sends `textDocument/references` with `includeDeclaration: true`.
|
||||||
|
- Requires `symbol` when `line` is given on project-aware servers (the first-non-whitespace-column fallback is disabled for this action).
|
||||||
- For project-aware servers, retries up to `REFERENCES_RETRY_COUNT` times when the only hit is the queried declaration; between retries it waits for project load and sleeps `REFERENCES_RETRY_DELAY_MS`.
|
- For project-aware servers, retries up to `REFERENCES_RETRY_COUNT` times when the only hit is the queried declaration; between retries it waits for project load and sleeps `REFERENCES_RETRY_DELAY_MS`.
|
||||||
- First `REFERENCE_CONTEXT_LIMIT` references include surrounding context; the rest are location-only.
|
- First `REFERENCE_CONTEXT_LIMIT` references include surrounding context; the rest are location-only.
|
||||||
|
|
||||||
@@ -146,7 +148,7 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
- Optional: `line`, `symbol`, `apply`, `timeout`.
|
- Optional: `line`, `symbol`, `apply`, `timeout`.
|
||||||
|
|
||||||
**Execution**
|
**Execution**
|
||||||
- Waits for project load, sends `textDocument/rename`, receives a `WorkspaceEdit`.
|
- Requires `symbol` when `line` is given on project-aware servers, then waits for project load, sends `textDocument/rename`, receives a `WorkspaceEdit`.
|
||||||
- `apply !== false` applies edits immediately with `applyWorkspaceEdit()`.
|
- `apply !== false` applies edits immediately with `applyWorkspaceEdit()`.
|
||||||
- `apply === false` renders a preview with `formatWorkspaceEdit()`.
|
- `apply === false` renders a preview with `formatWorkspaceEdit()`.
|
||||||
|
|
||||||
@@ -161,9 +163,9 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
**Execution**
|
**Execution**
|
||||||
- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than `MAX_RENAME_PAIRS` files.
|
- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than `MAX_RENAME_PAIRS` files.
|
||||||
- `enumerateRenamePairs()` returns one `{oldUri,newUri}` pair for a file or walks every regular file in a directory tree.
|
- `enumerateRenamePairs()` returns one `{oldUri,newUri}` pair for a file or walks every regular file in a directory tree.
|
||||||
- Sends `workspace/willRenameFiles` with `{ files: pairs }` to every non-custom LSP server; collects returned `WorkspaceEdit`s and server notes.
|
- Sends `workspace/willRenameFiles` with `{ files: pairs }` to every non-custom LSP server whose `fileTypes` match an affected path; collects returned `WorkspaceEdit`s and server notes.
|
||||||
- Preview mode (`apply === false`) only formats those edits.
|
- Preview mode (`apply === false`) only formats those edits.
|
||||||
- Apply mode runs each returned `WorkspaceEdit`, renames the source path on disk, sends `textDocument/didClose` for every renamed open file, deletes those `openFiles` entries, then sends `workspace/didRenameFiles`.
|
- Apply mode coalesces the returned text edits per URI (a project-aware server's edits win on overlap; overlapping edits from other servers are discarded with a note), applies each URI once from a single snapshot, creates the destination parent directory and renames the source path on disk, sends `textDocument/didClose` for every renamed open file, deletes those `openFiles` entries, then sends `workspace/didRenameFiles`.
|
||||||
|
|
||||||
**Output text**
|
**Output text**
|
||||||
- Preview: `Rename preview: <file-count label> → <dest>` plus per-server edit summaries and optional server notes.
|
- Preview: `Rename preview: <file-count label> → <dest>` plus per-server edit summaries and optional server notes.
|
||||||
@@ -192,11 +194,11 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
- None.
|
- None.
|
||||||
|
|
||||||
**Execution**
|
**Execution**
|
||||||
- Reads configured servers from cached `LspConfig`, not `getActiveClients()`.
|
- Reads configured servers from cached `LspConfig` and cross-references `getActiveClients()` so each server is labelled `(configured, not started)` or with its live client status.
|
||||||
- Calls `detectLspmux()` and appends status text when `lspmux` is installed.
|
- Calls `detectLspmux()` and appends status text when `lspmux` is installed.
|
||||||
|
|
||||||
**Output text**
|
**Output text**
|
||||||
- `Active language servers: ...` or `No language servers configured for this project`, optionally followed by `lspmux: active (multiplexing enabled)` or `lspmux: installed but server not running`.
|
- `Language servers: <name (configured, not started) | name (<status>)>` plus an explanatory note line, or `No language servers configured for this project`, optionally followed by `lspmux: active (multiplexing enabled)` or `lspmux: installed but server not running`.
|
||||||
|
|
||||||
### `reload`
|
### `reload`
|
||||||
**Inputs**
|
**Inputs**
|
||||||
@@ -240,7 +242,7 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
|
|
||||||
**Output text**
|
**Output text**
|
||||||
- Success: `<server> ← <method>:\n<formatted result>`, where non-string results are `JSON.stringify(..., null, 2)` and nullish values become `null`.
|
- Success: `<server> ← <method>:\n<formatted result>`, where non-string results are `JSON.stringify(..., null, 2)` and nullish values become `null`.
|
||||||
- Failure: `LSP error from <server> on <method>: ...`.
|
- Failure: `LSP error from <server> on <method>: ...` followed by ` params: <preview>` echoing the request params (truncated to 400 chars).
|
||||||
|
|
||||||
## Side Effects
|
## Side Effects
|
||||||
- Filesystem
|
- Filesystem
|
||||||
@@ -297,13 +299,14 @@ Same as `definition`, but sends `textDocument/implementation` and reports `imple
|
|||||||
- diagnostics continue when one server fails
|
- diagnostics continue when one server fails
|
||||||
- `rename_file` suppresses `workspace/willRenameFiles` “method not found” errors and records other server errors as notes
|
- `rename_file` suppresses `workspace/willRenameFiles` “method not found” errors and records other server errors as notes
|
||||||
- `code_actions` ignores `codeAction/resolve` failures and applies unresolved actions when possible
|
- `code_actions` ignores `codeAction/resolve` failures and applies unresolved actions when possible
|
||||||
- Aborts are not converted to text: `ToolAbortError` is rethrown.
|
- Caller aborts are not converted to text: `ToolAbortError` is rethrown. A wall-clock tool timeout without a caller abort instead throws `ToolError`: `LSP <action> timed out after <N>s on <server>. ...`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- `status` reports configured/available servers from `LspConfig`, not currently active client processes from `getActiveClients()`.
|
- `status` reports configured servers from `LspConfig` and labels each one via `getActiveClients()`: `(configured, not started)` means the binary resolves on PATH but no request has spawned it; a live client reports its status.
|
||||||
- `getLspServerForFile()` excludes `createClient` adapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.
|
- `getLspServerForFile()` excludes `createClient` adapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.
|
||||||
- `getServersForFile()` matches both file extensions and exact basenames from `fileTypes`; config can target names like `Dockerfile` if present.
|
- `getServersForFile()` matches both file extensions and exact basenames from `fileTypes`; config can target names like `Dockerfile` if present.
|
||||||
- `symbol` matching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.
|
- `symbol` matching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.
|
||||||
|
- For `definition`, `references`, and `rename` against project-aware servers, omitting `symbol` while passing `line` is rejected with a `ToolError` instead of silently falling back to the first non-whitespace column.
|
||||||
- `code_actions` uses `query` in two different ways: server-side `context.only` filter in list mode, client-side title/index selector in apply mode.
|
- `code_actions` uses `query` in two different ways: server-side `context.only` filter in list mode, client-side title/index selector in apply mode.
|
||||||
- `rename` and `rename_file` default to apply. Preview requires `apply: false`.
|
- `rename` and `rename_file` default to apply. Preview requires `apply: false`.
|
||||||
- `request` with `file: "*"` is treated the same as omitted `file`: it does not build workspace-specific params.
|
- `request` with `file: "*"` is treated the same as omitted `file`: it does not build workspace-specific params.
|
||||||
|
|||||||
+17
-16
@@ -31,8 +31,8 @@ For normal file-like reads, `splitPathAndSel()` in `packages/coding-agent/src/to
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `:raw` | Raw/verbatim mode. Disables structural summaries and line prefixes. |
|
| `:raw` | Raw/verbatim mode. Disables structural summaries and line prefixes. |
|
||||||
| `:conflicts` | Render unresolved Git merge-conflict regions for a local file. |
|
| `:conflicts` | Render unresolved Git merge-conflict regions for a local file. |
|
||||||
| `:N` / `:LN` / `:N-` | Start at 1-indexed line `N`, open-ended. |
|
| `:N` / `:LN` / `:N-` / `:N..` | Start at 1-indexed line `N`, open-ended. |
|
||||||
| `:A-B` / `:LA-LB` | Inclusive 1-indexed line range. |
|
| `:A-B` / `:LA-LB` / `:A..B` | Inclusive 1-indexed line range (`..` is a forgiving alias normalized to `-`). |
|
||||||
| `:A+C` / `:LA+LC` | `C` lines starting at `A`; tool converts this to end line `A + C - 1`. |
|
| `:A+C` / `:LA+LC` | `C` lines starting at `A`; tool converts this to end line `A + C - 1`. |
|
||||||
| `:R1,R2,...` | Multiple ranges, sorted and merged before reading (for example `:5-16,960-973`). |
|
| `:R1,R2,...` | Multiple ranges, sorted and merged before reading (for example `:5-16,960-973`). |
|
||||||
| `:range:raw` or `:raw:range` | Same line selection, but raw output. |
|
| `:range:raw` or `:raw:range` | Same line selection, but raw output. |
|
||||||
@@ -88,8 +88,8 @@ URL selectors are parsed separately in `packages/coding-agent/src/tools/fetch.ts
|
|||||||
- markit-converted document
|
- markit-converted document
|
||||||
- structural summary for parseable code/prose
|
- structural summary for parseable code/prose
|
||||||
- streamed text/line-range read
|
- streamed text/line-range read
|
||||||
9. Local text reads are streamed by `streamLinesFromFile()` rather than loading the whole file. The tool adds up to 3 lines of context before/after explicit bounded ranges.
|
9. Local text reads are streamed by `streamLinesFromFile()` rather than loading the whole file. The tool adds `1` leading and `3` trailing context lines around explicit bounded ranges (constrained sides only).
|
||||||
10. Non-empty contiguous local reads are recorded into `getFileReadCache(session)` for later hashline edit recovery.
|
10. Hashline-eligible local reads record a whole-file snapshot into the session snapshot store (`getFileSnapshotStore()` on `session.fileSnapshotStore`, `packages/coding-agent/src/edit/file-snapshot-store.ts`) for later hashline edit verification/recovery.
|
||||||
11. If suffix resolution happened, the first text block is prefixed with `[Path '...' not found; resolved to '...' via suffix match]`.
|
11. If suffix resolution happened, the first text block is prefixed with `[Path '...' not found; resolved to '...' via suffix match]`.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
@@ -106,8 +106,8 @@ URL selectors are parsed separately in `packages/coding-agent/src/tools/fetch.ts
|
|||||||
- hashline numbered output when edit mode is hashline, read is not raw, source is mutable, edit tool exists, and `readHashLines !== false`
|
- hashline numbered output when edit mode is hashline, read is not raw, source is mutable, edit tool exists, and `readHashLines !== false`
|
||||||
- otherwise optional line numbers when `readLineNumbers === true`
|
- otherwise optional line numbers when `readLineNumbers === true`
|
||||||
- raw mode suppresses both
|
- raw mode suppresses both
|
||||||
- Prefix format in hashline mode is a `¶PATH#TAG` header followed by `LINE:TEXT`, e.g. `¶src/foo.ts#0A1B` and `41:def alpha():`, from the session snapshot store plus `formatNumberedLine()` / `formatHashlineHeader()`.
|
- Prefix format in hashline mode is a `[PATH#TAG]` header followed by `LINE:TEXT`, e.g. `[src/foo.ts#0A1B]` and `41:def alpha():`, from the session snapshot store plus `formatNumberedLine()` / `formatHashlineHeader()`.
|
||||||
- The `edit`/hashline path consumes that header plus bare line numbers later; the four-hex tag is opaque and only meaningful in the session snapshot store that minted it. Immutable sources and `:raw` intentionally suppress hashline headers.
|
- The `edit`/hashline path consumes that header plus bare line numbers later; the four-hex tag is a content-derived hash of the whole normalized file, resolvable through the session snapshot store that recorded it. Immutable sources and `:raw` intentionally suppress hashline headers.
|
||||||
|
|
||||||
### Directory listings
|
### Directory listings
|
||||||
- `#readDirectory()` calls `buildDirectoryTree()` with:
|
- `#readDirectory()` calls `buildDirectoryTree()` with:
|
||||||
@@ -121,9 +121,9 @@ URL selectors are parsed separately in `packages/coding-agent/src/tools/fetch.ts
|
|||||||
### Archives
|
### Archives
|
||||||
- Supported archive containers: `.tar`, `.tar.gz`, `.tgz`, `.zip`.
|
- Supported archive containers: `.tar`, `.tar.gz`, `.tgz`, `.zip`.
|
||||||
- Syntax: `archive.ext`, `archive.ext:path/inside`, `archive.ext:path/inside:50-60`.
|
- Syntax: `archive.ext`, `archive.ext:path/inside`, `archive.ext:path/inside:50-60`.
|
||||||
- `openArchive()` reads the whole archive into memory, then:
|
- `openArchive()` branches by format:
|
||||||
- tar/tgz uses `new Bun.Archive(bytes)`
|
- tar/tgz reads the whole archive into memory (capped at `MAX_TAR_ARCHIVE_BYTES = 256 MiB`) and indexes it with `new Bun.Archive(bytes)`
|
||||||
- zip uses `fflate.unzipSync()`
|
- zip is indexed via ranged central-directory reads (`readZipEntries()`); entries are inflated on demand with `fflate.inflateSync()`, with declared member sizes capped at `MAX_ARCHIVE_MEMBER_BYTES = 64 MiB`
|
||||||
- Archive paths normalize `/`, drop `.` segments, and reject `..`.
|
- Archive paths normalize `/`, drop `.` segments, and reject `..`.
|
||||||
- Directory reads list immediate children; files show `name` plus ` (size)` when size > 0.
|
- Directory reads list immediate children; files show `name` plus ` (size)` when size > 0.
|
||||||
- Directory listing default limit is `500` entries in `#readArchiveDirectory()`.
|
- Directory listing default limit is `500` entries in `#readArchiveDirectory()`.
|
||||||
@@ -161,7 +161,7 @@ URL selectors are parsed separately in `packages/coding-agent/src/tools/fetch.ts
|
|||||||
- `kind: "raw"`
|
- `kind: "raw"`
|
||||||
- Cannot be combined with table selectors or any other query param.
|
- Cannot be combined with table selectors or any other query param.
|
||||||
- Empty `q` throws.
|
- Empty `q` throws.
|
||||||
- `executeReadQuery()` runs `db.prepare(sql).all()` and rejects bound parameters; it does not verify that the SQL starts with `SELECT`.
|
- `executeReadQuery()` prepares the SQL, rejects bound parameters, and collects rows from `statement.iterate()` capped at `MAX_RAW_QUERY_ROWS = 1000`; it does not verify that the SQL starts with `SELECT`.
|
||||||
|
|
||||||
- Rendering caps in `packages/coding-agent/src/tools/sqlite-reader.ts`:
|
- Rendering caps in `packages/coding-agent/src/tools/sqlite-reader.ts`:
|
||||||
- ASCII table width `120` (`MAX_RENDER_WIDTH`)
|
- ASCII table width `120` (`MAX_RENDER_WIDTH`)
|
||||||
@@ -196,7 +196,7 @@ URL selectors are parsed separately in `packages/coding-agent/src/tools/fetch.ts
|
|||||||
|
|
||||||
### Internal URLs
|
### Internal URLs
|
||||||
- `read` does not resolve these itself; it delegates to `session.internalRouter.resolve()`.
|
- `read` does not resolve these itself; it delegates to `session.internalRouter.resolve()`.
|
||||||
- Registered protocols are outside this file, but the router in `packages/coding-agent/src/internal-urls/router.ts` is built for `agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, and `skill://`.
|
- Registered protocols are outside this file, but the router in `packages/coding-agent/src/internal-urls/router.ts` is built for `agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`.
|
||||||
- `#handleInternalUrl()` behavior:
|
- `#handleInternalUrl()` behavior:
|
||||||
- parses the URL with `parseInternalUrl()` so colons inside the host segment are legal
|
- parses the URL with `parseInternalUrl()` so colons inside the host segment are legal
|
||||||
- for `agent://`, treats non-root path extraction or `?q=` extraction as a special no-pagination mode
|
- for `agent://`, treats non-root path extraction or `?q=` extraction as a special no-pagination mode
|
||||||
@@ -235,7 +235,7 @@ Notes: ...
|
|||||||
## Side Effects
|
## Side Effects
|
||||||
- Filesystem
|
- Filesystem
|
||||||
- Opens and streams local files.
|
- Opens and streams local files.
|
||||||
- Reads entire archives into memory before indexing.
|
- Reads tar/tgz archives fully into memory before indexing (256 MiB cap); ZIP archives are indexed via ranged central-directory reads.
|
||||||
- May read URL-cache artifact files from the session artifacts directory.
|
- May read URL-cache artifact files from the session artifacts directory.
|
||||||
- Writes URL output artifacts when URL output is truncated or when line-range pagination needs a persisted cache body.
|
- Writes URL output artifacts when URL output is truncated or when line-range pagination needs a persisted cache body.
|
||||||
- Network
|
- Network
|
||||||
@@ -245,7 +245,7 @@ Notes: ...
|
|||||||
- Uses `Bun.Archive` for tar/tgz and `fflate` for zip.
|
- Uses `Bun.Archive` for tar/tgz and `fflate` for zip.
|
||||||
- URL HTML rendering can delegate into site handlers and HTML-to-text backends from `packages/coding-agent/src/tools/fetch.ts`.
|
- URL HTML rendering can delegate into site handlers and HTML-to-text backends from `packages/coding-agent/src/tools/fetch.ts`.
|
||||||
- Session state
|
- Session state
|
||||||
- Records local text lines into `session.fileReadCache` for later stale-anchor recovery.
|
- Records whole-file snapshots of local text reads into `session.fileSnapshotStore` for later stale-anchor recovery.
|
||||||
- Uses `session.internalRouter` for internal URLs.
|
- Uses `session.internalRouter` for internal URLs.
|
||||||
- Uses `session.allocateOutputArtifact()` for cached/truncated URL output.
|
- Uses `session.allocateOutputArtifact()` for cached/truncated URL output.
|
||||||
- Background work / cancellation
|
- Background work / cancellation
|
||||||
@@ -267,6 +267,7 @@ Notes: ...
|
|||||||
- default row query limit `20`
|
- default row query limit `20`
|
||||||
- schema sample limit `5`
|
- schema sample limit `5`
|
||||||
- max query limit `500`
|
- max query limit `500`
|
||||||
|
- raw `?q=` row cap `1000` (`MAX_RAW_QUERY_ROWS`)
|
||||||
- table list cap `500`
|
- table list cap `500`
|
||||||
- render width `120`, column width `40`
|
- render width `120`, column width `40`
|
||||||
- busy timeout `3000` ms
|
- busy timeout `3000` ms
|
||||||
@@ -275,14 +276,14 @@ Notes: ...
|
|||||||
- source bytes cap `20 MiB`
|
- source bytes cap `20 MiB`
|
||||||
- post-resize inline output cap `300 KiB`
|
- post-resize inline output cap `300 KiB`
|
||||||
- Unique suffix auto-resolution glob timeout: `5000` ms.
|
- Unique suffix auto-resolution glob timeout: `5000` ms.
|
||||||
- File-read cache holds `30` paths per session.
|
- File snapshot store holds `30` paths with up to `4` versions each (`DEFAULT_MAX_PATHS` / `DEFAULT_MAX_VERSIONS_PER_PATH` in `packages/hashline/src/snapshots.ts`); files over `4 MiB` (`SNAPSHOT_MAX_BYTES`) are not snapshotted.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- Validation and operational failures surface as `ToolError`.
|
- Validation and operational failures surface as `ToolError`.
|
||||||
- Selector errors include:
|
- Selector errors include:
|
||||||
- `Line selector 0 is invalid; lines are 1-indexed. Use :1.`
|
- `Line selector 0 is invalid; lines are 1-indexed. Use :1.`
|
||||||
- invalid `A+B` / `A-B` shapes
|
- invalid `A+B` / `A-B` shapes
|
||||||
- `Cannot combine query extraction with offset/limit` for `agent://.../path:50`
|
- `Cannot combine query extraction with line selectors` for `agent://.../path:50`
|
||||||
- Missing local/archive/sqlite paths first attempt unique suffix resolution; if no unique match exists they error.
|
- Missing local/archive/sqlite paths first attempt unique suffix resolution; if no unique match exists they error.
|
||||||
- Out-of-bounds line reads do not throw. They return explanatory text with a suggestion such as `Use :1 ...` or `Use :<last line> ...`.
|
- Out-of-bounds line reads do not throw. They return explanatory text with a suggestion such as `Use :1 ...` or `Use :<last line> ...`.
|
||||||
- Binary archive entries do not throw; they return a text notice.
|
- Binary archive entries do not throw; they return a text notice.
|
||||||
@@ -299,4 +300,4 @@ Notes: ...
|
|||||||
- URL cache keys are session-scoped and normalized by requested URL + raw/rendered mode; both requested URL and final redirected URL are cached.
|
- URL cache keys are session-scoped and normalized by requested URL + raw/rendered mode; both requested URL and final redirected URL are cached.
|
||||||
- URL line-range reads request `ensureArtifact: true, preferCached: true` so a later paginated read can reopen the same rendered body from artifact storage.
|
- URL line-range reads request `ensureArtifact: true, preferCached: true` so a later paginated read can reopen the same rendered body from artifact storage.
|
||||||
- Raw SQLite `q=` execution is not keyword-restricted beyond “no bound parameters”; the read tool relies on the surrounding contract to keep it read-only.
|
- Raw SQLite `q=` execution is not keyword-restricted beyond “no bound parameters”; the read tool relies on the surrounding contract to keep it read-only.
|
||||||
- The file-read cache is not a read acceleration cache. It exists to recover hashline edits when the file changed after the read.
|
- The file snapshot store is not a read acceleration cache. It exists to verify and recover hashline edits when the file changed after the read.
|
||||||
@@ -60,8 +60,8 @@ When no matches exist:
|
|||||||
- Backend auto-recall has a richer query-composition path in `HindsightSessionState.beforeAgentStartPrompt(...)` / `maybeRecallOnAgentStart(...)` and `MnemopiSessionState.beforeAgentStartPrompt(...)` / `maybeRecallOnAgentStart(...)`.
|
- Backend auto-recall has a richer query-composition path in `HindsightSessionState.beforeAgentStartPrompt(...)` / `maybeRecallOnAgentStart(...)` and `MnemopiSessionState.beforeAgentStartPrompt(...)` / `maybeRecallOnAgentStart(...)`.
|
||||||
- Hindsight bank scoping:
|
- Hindsight bank scoping:
|
||||||
- `global` — no tag filter.
|
- `global` — no tag filter.
|
||||||
- `per-project` — separate bank id per cwd basename.
|
- `per-project` — separate bank id per project label (git primary checkout root basename; cwd basename outside a repo).
|
||||||
- `per-project-tagged` — shared bank id plus `project:<cwd basename>` filter with `tagsMatch = "any"`, so project-tagged and untagged global memories can both surface.
|
- `per-project-tagged` — shared bank id plus `project:<project label>` filter with `tagsMatch = "any"`, so project-tagged and untagged global memories can both surface.
|
||||||
- Mnemopi bank scoping:
|
- Mnemopi bank scoping:
|
||||||
- `global` — recall reads the shared bank.
|
- `global` — recall reads the shared bank.
|
||||||
- `per-project` — recall reads the project bank.
|
- `per-project` — recall reads the project bank.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- Entry: `packages/coding-agent/src/tools/memory-reflect.ts`
|
- Entry: `packages/coding-agent/src/tools/memory-reflect.ts`
|
||||||
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/reflect.md`
|
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/reflect.md`
|
||||||
- Hindsight collaborators:
|
- Hindsight collaborators:
|
||||||
- `packages/coding-agent/src/hindsight/bank.ts` — best-effort bank mission initialization.
|
- `packages/coding-agent/src/hindsight/bank.ts` — best-effort first-use bank/mission setup (`ensureBankExists`).
|
||||||
- `packages/coding-agent/src/hindsight/state.ts` — session state, shared bank scope, recall/reflect config.
|
- `packages/coding-agent/src/hindsight/state.ts` — session state, shared bank scope, recall/reflect config.
|
||||||
- `packages/coding-agent/src/hindsight/client.ts` — HTTP `reflect` call and error mapping.
|
- `packages/coding-agent/src/hindsight/client.ts` — HTTP `reflect` call and error mapping.
|
||||||
- Mnemopi collaborators:
|
- Mnemopi collaborators:
|
||||||
@@ -45,8 +45,8 @@ Mnemopi:
|
|||||||
- if results exist, it renders them through `state.formatContextScoped(...)` and prefixes `Based on recalled memories:`.
|
- if results exist, it renders them through `state.formatContextScoped(...)` and prefixes `Based on recalled memories:`.
|
||||||
4. If the backend is `hindsight`:
|
4. If the backend is `hindsight`:
|
||||||
- it reads `session.getHindsightSessionState()` and throws if the backend was not started;
|
- it reads `session.getHindsightSessionState()` and throws if the backend was not started;
|
||||||
- it calls `ensureBankMission(...)` with the current `bankId`, config, and process-local `missionsSet`;
|
- it calls `ensureBankExists(...)` with the current `bankId`, config, and the session state's `banksSet`;
|
||||||
- `ensureBankMission(...)` best-effort `PUT`s `/v1/default/banks/{bank_id}` with `reflect_mission` and optional `retain_mission` exactly once per bank/process; failures are swallowed;
|
- `ensureBankExists(...)` best-effort `PUT`s `/v1/default/banks/{bank_id}` (`createBank`) with optional `reflect_mission` / `retain_mission` once per bank per session state; failures are swallowed;
|
||||||
- it calls `state.client.reflect(...)` with `query`, optional `context`, configured recall budget, and bank-scope tag filters;
|
- it calls `state.client.reflect(...)` with `query`, optional `context`, configured recall budget, and bank-scope tag filters;
|
||||||
- `HindsightApi.reflect(...)` POSTs `/v1/default/banks/{bank_id}/reflect` and defaults its own budget to `"low"` when callers omit one; this tool always passes the configured budget;
|
- `HindsightApi.reflect(...)` POSTs `/v1/default/banks/{bank_id}/reflect` and defaults its own budget to `"low"` when callers omit one; this tool always passes the configured budget;
|
||||||
- blank or whitespace-only responses are replaced with `No relevant information found to reflect on.`
|
- blank or whitespace-only responses are replaced with `No relevant information found to reflect on.`
|
||||||
@@ -57,8 +57,8 @@ Mnemopi:
|
|||||||
- Mnemopi tool path: one local scoped recall followed by context formatting.
|
- Mnemopi tool path: one local scoped recall followed by context formatting.
|
||||||
- Hindsight bank scoping:
|
- Hindsight bank scoping:
|
||||||
- `global` — no tag filter.
|
- `global` — no tag filter.
|
||||||
- `per-project` — separate bank id per cwd basename.
|
- `per-project` — separate bank id per project label (git primary checkout root basename; cwd basename outside a repo).
|
||||||
- `per-project-tagged` — shared bank id plus `project:<cwd basename>` filter with `tagsMatch = "any"`.
|
- `per-project-tagged` — shared bank id plus `project:<project label>` filter with `tagsMatch = "any"`.
|
||||||
- Mnemopi bank scoping:
|
- Mnemopi bank scoping:
|
||||||
- `global` — reads the shared bank.
|
- `global` — reads the shared bank.
|
||||||
- `per-project` — reads the project bank.
|
- `per-project` — reads the project bank.
|
||||||
@@ -67,7 +67,7 @@ Mnemopi:
|
|||||||
|
|
||||||
## Side Effects
|
## Side Effects
|
||||||
- Network
|
- Network
|
||||||
- Hindsight: optional `PUT /v1/default/banks/{bank_id}` from `ensureBankMission(...)`, then `POST /v1/default/banks/{bank_id}/reflect`.
|
- Hindsight: optional `PUT /v1/default/banks/{bank_id}` from `ensureBankExists(...)`, then `POST /v1/default/banks/{bank_id}/reflect`.
|
||||||
- Mnemopi: none unless configured embedding or LLM providers are used by the local runtime during recall.
|
- Mnemopi: none unless configured embedding or LLM providers are used by the local runtime during recall.
|
||||||
- Session state
|
- Session state
|
||||||
- Reads session-held backend scope and config only. Does not update `lastRecallSnippet`, Hindsight mental-model cache, or retain queues.
|
- Reads session-held backend scope and config only. Does not update `lastRecallSnippet`, Hindsight mental-model cache, or retain queues.
|
||||||
@@ -79,14 +79,14 @@ Mnemopi:
|
|||||||
- Tool-level params: only `query` is required; `context` is optional.
|
- Tool-level params: only `query` is required; `context` is optional.
|
||||||
- Hindsight budget setting comes from `hindsight.recallBudget`, default `"mid"`.
|
- Hindsight budget setting comes from `hindsight.recallBudget`, default `"mid"`.
|
||||||
- Hindsight `reflect` has no client-side token cap parameter here; unlike `recall`, the tool does not pass `maxTokens`.
|
- Hindsight `reflect` has no client-side token cap parameter here; unlike `recall`, the tool does not pass `maxTokens`.
|
||||||
- Hindsight mission initialization tracks up to `MISSION_SET_CAP = 10_000` bank ids, then drops the oldest half of the sorted set.
|
- Hindsight bank initialization tracks up to `MISSION_SET_CAP = 10_000` bank ids per session state, then drops half of the sorted set.
|
||||||
- Mnemopi result count is capped by `mnemopi.recallLimit`, default `8`.
|
- Mnemopi result count is capped by `mnemopi.recallLimit`, default `8`.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- Throws `Mnemopi backend is not initialised for this session.` when `memory.backend == "mnemopi"` but no state exists.
|
- Throws `Mnemopi backend is not initialised for this session.` when `memory.backend == "mnemopi"` but no state exists.
|
||||||
- Throws `Hindsight backend is not initialised for this session.` when `memory.backend == "hindsight"` but no state exists.
|
- Throws `Hindsight backend is not initialised for this session.` when `memory.backend == "hindsight"` but no state exists.
|
||||||
- Hindsight HTTP and fetch failures become `HindsightError` with `statusCode` and parsed `details` when available.
|
- Hindsight HTTP and fetch failures become `HindsightError` with `statusCode` and parsed `details` when available.
|
||||||
- Hindsight `ensureBankMission(...)` failures are silent to the tool caller; only the later reflect request can fail visibly.
|
- Hindsight `ensureBankExists(...)` failures are silent to the tool caller; only the later reflect request can fail visibly.
|
||||||
- Mnemopi recall target failures inside `collectScopedRecallResults(...)` are caught per bank and logged only when `mnemopi.debug` is enabled; if all targets fail, the tool can return the no-information text.
|
- Mnemopi recall target failures inside `collectScopedRecallResults(...)` are caught per bank and logged only when `mnemopi.debug` is enabled; if all targets fail, the tool can return the no-information text.
|
||||||
- Non-`Error` failures caught by the tool are normalized to `new Error(String(err))` before rethrow.
|
- Non-`Error` failures caught by the tool are normalized to `new Error(String(err))` before rethrow.
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ This is a preview. Call the `resolve` tool to apply or discard these changes.
|
|||||||
```
|
```
|
||||||
|
|
||||||
6. When `resolve.execute()` runs, it wraps the call in `untilAborted(...)` and fetches `session.peekQueueInvoker?.() ?? session.peekStandingResolveHandler?.()`.
|
6. When `resolve.execute()` runs, it wraps the call in `untilAborted(...)` and fetches `session.peekQueueInvoker?.() ?? session.peekStandingResolveHandler?.()`.
|
||||||
7. If no invoker exists, it throws `ToolError("No pending action to resolve. Nothing to apply or discard.")`.
|
7. If no invoker exists, `apply` throws `ToolError("No pending action to resolve. Nothing to apply or discard.")`; `discard` instead returns a success payload `Nothing to discard; no pending action remains.` because the desired end-state (no staged change) already holds.
|
||||||
8. Otherwise it invokes the current handler with the full params object.
|
8. Otherwise it invokes the current handler with the full params object.
|
||||||
9. `runResolveInvocation(...)` builds base details from `action`, `reason`, `extra`, `sourceToolName`, and `label`.
|
9. `runResolveInvocation(...)` builds base details from `action`, `reason`, `extra`, `sourceToolName`, and `label`.
|
||||||
10. For `apply`, it calls the producer's `apply(reason, extra)` callback.
|
10. For `apply`, it calls the producer's `apply(reason, extra)` callback.
|
||||||
@@ -57,6 +57,7 @@ This is a preview. Call the `resolve` tool to apply or discard these changes.
|
|||||||
- `apply`: runs the pending action's `apply(reason, extra?)` callback and returns its content.
|
- `apply`: runs the pending action's `apply(reason, extra?)` callback and returns its content.
|
||||||
- `discard` with reject callback: runs `reject(reason, extra?)` and returns that callback's content when non-`undefined`.
|
- `discard` with reject callback: runs `reject(reason, extra?)` and returns that callback's content when non-`undefined`.
|
||||||
- `discard` without reject callback, or with a reject callback returning `undefined`: returns the built-in `Discarded: ...` text payload.
|
- `discard` without reject callback, or with a reject callback returning `undefined`: returns the built-in `Discarded: ...` text payload.
|
||||||
|
- `discard` with no pending action at all: returns `Nothing to discard; no pending action remains.` as a success result.
|
||||||
- Queued handler: one in-flight tool-choice queue invoker, used by preview producers such as `ast_edit`.
|
- Queued handler: one in-flight tool-choice queue invoker, used by preview producers such as `ast_edit`.
|
||||||
- Standing handler: long-lived mode-owned handler, used as a fallback when no queue invoker is active.
|
- Standing handler: long-lived mode-owned handler, used as a fallback when no queue invoker is active.
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ This is a preview. Call the `resolve` tool to apply or discard these changes.
|
|||||||
- There is no independent queue depth cap in this tool; ordering follows the shared tool-choice queue and mode-owned standing handler lifecycle.
|
- There is no independent queue depth cap in this tool; ordering follows the shared tool-choice queue and mode-owned standing handler lifecycle.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- No pending action or standing handler: throws `ToolError("No pending action to resolve. Nothing to apply or discard.")`.
|
- `apply` with no pending action or standing handler: throws `ToolError("No pending action to resolve. Nothing to apply or discard.")`. `discard` in the same situation succeeds with `Nothing to discard; no pending action remains.` instead of erroring.
|
||||||
- `apply` callback throws `ToolError`: the original `ToolError` propagates.
|
- `apply` callback throws `ToolError`: the original `ToolError` propagates.
|
||||||
- `apply` callback throws any other value: `resolve` wraps it as `ToolError("Apply failed: <message>")` after running `onApplyError` when present.
|
- `apply` callback throws any other value: `resolve` wraps it as `ToolError("Apply failed: <message>")` after running `onApplyError` when present.
|
||||||
- `reject` callback exceptions propagate without the apply-specific wrapper.
|
- `reject` callback exceptions propagate without the apply-specific wrapper.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
- Hindsight collaborators:
|
- Hindsight collaborators:
|
||||||
- `packages/coding-agent/src/hindsight/state.ts` — per-session queue, flush, auto-retain.
|
- `packages/coding-agent/src/hindsight/state.ts` — per-session queue, flush, auto-retain.
|
||||||
- `packages/coding-agent/src/hindsight/backend.ts` — session bootstrap, prompt injection, subagent aliasing.
|
- `packages/coding-agent/src/hindsight/backend.ts` — session bootstrap, prompt injection, subagent aliasing.
|
||||||
- `packages/coding-agent/src/hindsight/bank.ts` — bank id derivation, tag scoping, mission setup.
|
- `packages/coding-agent/src/hindsight/bank.ts` — bank id derivation, tag scoping, first-use bank/mission setup.
|
||||||
- `packages/coding-agent/src/hindsight/client.ts` — HTTP `retain` / `retainBatch` calls.
|
- `packages/coding-agent/src/hindsight/client.ts` — HTTP `retain` / `retainBatch` calls.
|
||||||
- `packages/coding-agent/src/hindsight/content.ts` — retention transcript shaping, memory-tag stripping.
|
- `packages/coding-agent/src/hindsight/content.ts` — retention transcript shaping, memory-tag stripping.
|
||||||
- `packages/coding-agent/src/hindsight/mental-models.ts` — bank-scoped mental-model seeding and cache rendering.
|
- `packages/coding-agent/src/hindsight/mental-models.ts` — bank-scoped mental-model seeding and cache rendering.
|
||||||
@@ -52,15 +52,15 @@ Mnemopi:
|
|||||||
- it fetches `session.getHindsightSessionState()` and throws if the backend was not started;
|
- it fetches `session.getHindsightSessionState()` and throws if the backend was not started;
|
||||||
- each input item is handed to `HindsightSessionState.enqueueRetain(...)`;
|
- each input item is handed to `HindsightSessionState.enqueueRetain(...)`;
|
||||||
- `HindsightRetainQueue.enqueue(...)` appends the item and either flushes immediately when the queue reaches `RETAIN_FLUSH_BATCH_SIZE`, or starts a debounce timer for `RETAIN_FLUSH_INTERVAL_MS`;
|
- `HindsightRetainQueue.enqueue(...)` appends the item and either flushes immediately when the queue reaches `RETAIN_FLUSH_BATCH_SIZE`, or starts a debounce timer for `RETAIN_FLUSH_INTERVAL_MS`;
|
||||||
- on flush, `HindsightRetainQueue.#doFlush(...)` verifies ownership, best-effort initializes the bank mission, maps items to `MemoryItemInput` with `context ?? config.retainContext`, `metadata.session_id`, and bank-scope tags, then sends one async `retainBatch(...)` request.
|
- on flush, `HindsightRetainQueue.#doFlush(...)` verifies ownership, best-effort ensures the bank exists via `ensureBankExists(...)`, maps items to `MemoryItemInput` with `context ?? config.retainContext`, `metadata.session_id`, and bank-scope tags, then sends one async `retainBatch(...)` request.
|
||||||
|
|
||||||
## Modes / Variants
|
## Modes / Variants
|
||||||
- Hindsight tool path: queued batch write only.
|
- Hindsight tool path: queued batch write only.
|
||||||
- Mnemopi tool path: direct local `remember(...)` into the scoped retain bank.
|
- Mnemopi tool path: direct local `remember(...)` into the scoped retain bank.
|
||||||
- Hindsight bank scoping from `computeBankScope(...)`:
|
- Hindsight bank scoping from `computeBankScope(...)`:
|
||||||
- `global` — one shared bank, no project tags.
|
- `global` — one shared bank, no project tags.
|
||||||
- `per-project` — bank id gets `-<cwd basename>` appended.
|
- `per-project` — bank id gets `-<project label>` appended, where the label is the git primary checkout root basename (cwd basename outside a repo).
|
||||||
- `per-project-tagged` — shared bank plus `project:<cwd basename>` tags on retained memories.
|
- `per-project-tagged` — shared bank plus `project:<project label>` tags on retained memories.
|
||||||
- Mnemopi bank scoping from `resolveBankScope(...)`:
|
- Mnemopi bank scoping from `resolveBankScope(...)`:
|
||||||
- `global` — retain and recall use the shared bank.
|
- `global` — retain and recall use the shared bank.
|
||||||
- `per-project` — retain and recall use the project bank.
|
- `per-project` — retain and recall use the project bank.
|
||||||
@@ -76,7 +76,7 @@ Mnemopi:
|
|||||||
- Hindsight: none for retained memories. No local memory file is written.
|
- Hindsight: none for retained memories. No local memory file is written.
|
||||||
- Mnemopi: writes to local SQLite under `mnemopi.dbPath`, defaulting beneath the agent memories directory (`mnemopi/mnemopi.db`) with one database file per scoped bank when needed.
|
- Mnemopi: writes to local SQLite under `mnemopi.dbPath`, defaulting beneath the agent memories directory (`mnemopi/mnemopi.db`) with one database file per scoped bank when needed.
|
||||||
- Network
|
- Network
|
||||||
- Hindsight: `POST /v1/default/banks/{bank_id}/memories` via `retainBatch(...)`, plus optional `PUT /v1/default/banks/{bank_id}` via `ensureBankMission(...)` before first write per bank/process.
|
- Hindsight: `POST /v1/default/banks/{bank_id}/memories` via `retainBatch(...)`, plus optional `PUT /v1/default/banks/{bank_id}` via `ensureBankExists(...)` before the first write per bank per session state (the set is created with the primary session state and shared with subagent aliases).
|
||||||
- Mnemopi: none unless configured embedding or LLM providers make calls during extraction.
|
- Mnemopi: none unless configured embedding or LLM providers make calls during extraction.
|
||||||
- Session state
|
- Session state
|
||||||
- Hindsight: appends to the in-memory `HindsightRetainQueue`, includes `metadata.session_id`, and shares parent state for subagents.
|
- Hindsight: appends to the in-memory `HindsightRetainQueue`, includes `metadata.session_id`, and shares parent state for subagents.
|
||||||
@@ -109,7 +109,7 @@ Mnemopi:
|
|||||||
- Throws `Hindsight backend is not initialised for this session.` when `memory.backend == "hindsight"` but no state exists.
|
- Throws `Hindsight backend is not initialised for this session.` when `memory.backend == "hindsight"` but no state exists.
|
||||||
- Hindsight queue enqueue on disposed state throws `Hindsight retain queue is closed.`
|
- Hindsight queue enqueue on disposed state throws `Hindsight retain queue is closed.`
|
||||||
- Hindsight flush-time API failures are caught in `HindsightRetainQueue.#doFlush(...)`, logged, and converted into a warning notice instead of a tool error.
|
- Hindsight flush-time API failures are caught in `HindsightRetainQueue.#doFlush(...)`, logged, and converted into a warning notice instead of a tool error.
|
||||||
- Hindsight mission creation failures are swallowed in `ensureBankMission(...)`; writes continue.
|
- Hindsight bank/mission creation failures are swallowed in `ensureBankExists(...)`; writes continue.
|
||||||
- Mnemopi `remember(...)` failures are caught in `MnemopiSessionState.rememberInScope(...)`, logged, and not rethrown to the tool caller.
|
- Mnemopi `remember(...)` failures are caught in `MnemopiSessionState.rememberInScope(...)`, logged, and not rethrown to the tool caller.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
+21
-15
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `pattern` | `string` | Yes | Regex pattern. `search.ts` trims it and rejects empty input. The native matcher enables multiline only when the pattern text contains a literal newline or the two-character sequence `\\n`. The model prompt explicitly documents literal-brace escaping such as ``interface\\{\\}``, although the native layer also auto-escapes braces that cannot be valid repetition quantifiers. |
|
| `pattern` | `string` | Yes | Regex pattern. `search.ts` rejects whitespace-only input but otherwise preserves the pattern verbatim (leading/trailing whitespace is meaningful in regexes). The native matcher enables multiline only when the pattern text contains a literal newline or the two-character sequence `\\n`. The model prompt explicitly documents literal-brace escaping such as ``interface\\{\\}``, although the native layer also auto-escapes braces that cannot be valid repetition quantifiers. |
|
||||||
| `paths` | `string \| string[]` | Yes | One file path, directory path, glob-like path, archive member, internal URL, or an array of those. Append a line-range selector such as `:50-100` or `:5-16,960-973` to a single file/archive/internal-resource input to constrain matches. Empty strings are rejected after trimming/quote stripping. Single entries accidentally joined with comma, semicolon, or whitespace are expanded only after existence validation; existing paths containing delimiters stay intact. Filesystem-backed internal URLs search their backing file; virtual internal resources search resolved text in memory. Internal URLs cannot contain glob characters. |
|
| `paths` | `string \| string[]` | No | One file path, directory path, glob-like path, archive member, internal URL, or an array of those. Omitted or empty defaults to `.` (the workspace root). Append a line-range selector such as `:50-100` or `:5-16,960-973` to a single file/archive/internal-resource input to constrain matches. Empty strings are rejected after trimming/quote stripping. Single entries accidentally joined with comma, semicolon, or whitespace are expanded only after existence validation; existing paths containing delimiters stay intact. Filesystem-backed internal URLs search their backing file; virtual internal resources search resolved text in memory. Internal URLs cannot contain glob characters. |
|
||||||
| `i` | `boolean` | No | Case-insensitive search. Defaults to `false`. Passed to native `ignoreCase` or JS `RegExp` flags for virtual resources. |
|
| `i` | `boolean` | No | Case-insensitive search. Defaults to `false`. Passed to native `ignoreCase` or JS `RegExp` flags for virtual resources. |
|
||||||
| `gitignore` | `boolean` | No | Respect `.gitignore` during directory scans. Defaults to `true`. Passed to native `gitignore`. |
|
| `gitignore` | `boolean` | No | Respect `.gitignore` during directory scans. Defaults to `true`. Passed to native `gitignore`. |
|
||||||
| `skip` | `number` | No | File-page offset for multi-file results. Defaults to `0`; `search.ts` floors finite numbers and rejects negative or non-finite values. Single-file searches ignore it because they do not paginate by file. |
|
| `skip` | `number` | No | File-page offset for multi-file results. Defaults to `0`; `search.ts` floors finite numbers and rejects negative or non-finite values. Single-file searches ignore it because they do not paginate by file. |
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
## Outputs
|
## Outputs
|
||||||
The tool returns a single text block in `content[0].text` plus structured `details`.
|
The tool returns a single text block in `content[0].text` plus structured `details`.
|
||||||
|
|
||||||
- Match lines are formatted by `formatMatchLine()` as `*LINE:content` for matches and ` LINE:content` for context under a `¶PATH#TAG` header in hashline mode.
|
- Match lines are formatted by `formatMatchLine()` as `*LINE:content` for matches and ` LINE:content` for context under a `[PATH#TAG]` header in hashline mode.
|
||||||
- Hashline mode: `¶src/login.ts#1F2A`, `*5:content`, ` 9:content`.
|
- Hashline mode: `[src/login.ts#1F2A]`, `*5:content`, ` 9:content`.
|
||||||
- Plain mode: `*5|content`, ` 9|content`.
|
- Plain mode: `*5|content`, ` 9|content`.
|
||||||
- Directory and multi-file results are grouped by file, with `# <path>#TAG` headings when editable hashline anchors are available and `# <path>` headings otherwise.
|
- Directory and multi-file results are grouped through `formatGroupedFiles()` as a multi-level, prefix-folded directory tree: one `#` per nesting level, directory headers end with `/`, and file headers carry a `#TAG` suffix when editable hashline anchors are available.
|
||||||
- `details` may include:
|
- `details` may include:
|
||||||
- `scopePath` — formatted search scope.
|
- `scopePath` — formatted search scope.
|
||||||
- `matchCount`, `fileCount`, `files`, `fileMatches` — counts for the returned page.
|
- `matchCount`, `fileCount`, `files`, `fileMatches` — counts for the returned page.
|
||||||
@@ -42,11 +42,12 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- `truncated` and `meta.truncation` — final text output was head-truncated by `truncateHead()`.
|
- `truncated` and `meta.truncation` — final text output was head-truncated by `truncateHead()`.
|
||||||
- `displayContent` — TUI-only rendering text with `│` gutters instead of model anchors.
|
- `displayContent` — TUI-only rendering text with `│` gutters instead of model anchors.
|
||||||
- `missingPaths` — multi-path entries skipped because their base path did not exist.
|
- `missingPaths` — multi-path entries skipped because their base path did not exist.
|
||||||
- No-match result text is `No matches found`, optionally followed by skipped missing-path or unreadable-archive notes.
|
- No-match result text is `No matches found` (or `No more results (...)` when `skip` points past the last file page), optionally followed by skipped missing-path, unreadable-archive, or oversized-file notes.
|
||||||
|
|
||||||
## Flow
|
## Flow
|
||||||
1. `SearchTool.execute()` validates and normalizes input in `packages/coding-agent/src/tools/search.ts`:
|
1. `SearchTool.execute()` validates and normalizes input in `packages/coding-agent/src/tools/search.ts`:
|
||||||
- trims `pattern`, rejects empty patterns;
|
- rejects whitespace-only patterns while preserving the pattern verbatim;
|
||||||
|
- defaults omitted or empty `paths` to `["."]` (the workspace root);
|
||||||
- normalizes `skip` to a non-negative integer;
|
- normalizes `skip` to a non-negative integer;
|
||||||
- expands delimiter-flattened `paths` entries with `expandDelimitedPathEntries()`, keeping existing delimiter-containing paths intact, accepting comma/semicolon splits when at least one part resolves, and accepting whitespace splits only when every part resolves;
|
- expands delimiter-flattened `paths` entries with `expandDelimitedPathEntries()`, keeping existing delimiter-containing paths intact, accepting comma/semicolon splits when at least one part resolves, and accepting whitespace splits only when every part resolves;
|
||||||
- peels any line-range selector from each resulting entry;
|
- peels any line-range selector from each resulting entry;
|
||||||
@@ -63,7 +64,7 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
5. For multi-path calls, `partitionExistingPaths()` skips only ENOENT entries. If every filesystem entry is missing and no virtual internal resources remain, the tool errors.
|
5. For multi-path calls, `partitionExistingPaths()` skips only ENOENT entries. If every filesystem entry is missing and no virtual internal resources remain, the tool errors.
|
||||||
6. Path resolution branches:
|
6. Path resolution branches:
|
||||||
- one entry: `parseSearchPath()` splits `basePath` and optional glob;
|
- one entry: `parseSearchPath()` splits `basePath` and optional glob;
|
||||||
- multiple entries: `resolveExplicitSearchPaths()` computes a common base directory, brace-union glob, exact-file list, or degenerate-root target list.
|
- multiple entries: `resolveExplicitSearchPaths()` (via `resolveToolSearchScope()`) computes a common base directory, brace-union glob, exact-file list, or per-entry target list. Targets fan out when the common ancestor is not itself a requested scope, or when a plain-file entry would otherwise be demoted into a directory walk's glob union (`fanOutFileTargets`).
|
||||||
7. Line-range selectors are validated after path/archive/internal resolution. They are allowed only for single files, archive members, or virtual resources; glob/directory line-range selectors error.
|
7. Line-range selectors are validated after path/archive/internal resolution. They are allowed only for single files, archive members, or virtual resources; glob/directory line-range selectors error.
|
||||||
8. `search.ts` stats the resolved base path to decide file vs directory behavior.
|
8. `search.ts` stats the resolved base path to decide file vs directory behavior.
|
||||||
9. It calls native `grep()` from `@oh-my-pi/pi-natives` with:
|
9. It calls native `grep()` from `@oh-my-pi/pi-natives` with:
|
||||||
@@ -73,13 +74,15 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- `contextBefore` / `contextAfter` from settings;
|
- `contextBefore` / `contextAfter` from settings;
|
||||||
- `maxColumns: DEFAULT_MAX_COLUMN` (`512`);
|
- `maxColumns: DEFAULT_MAX_COLUMN` (`512`);
|
||||||
- `maxCount: INTERNAL_TOTAL_CAP` (`2000`);
|
- `maxCount: INTERNAL_TOTAL_CAP` (`2000`);
|
||||||
- `mode: content`.
|
- `maxCountPerFile`: the per-file match cap plus one;
|
||||||
|
- `mode: content`;
|
||||||
|
- the combined abort `signal` and `timeoutMs: SEARCH_GREP_TIMEOUT_MS` (`30_000`).
|
||||||
10. Native execution happens in `crates/pi-natives/src/grep.rs`:
|
10. Native execution happens in `crates/pi-natives/src/grep.rs`:
|
||||||
- `build_matcher()` sanitizes non-quantifier braces before regex compile;
|
- `build_matcher()` sanitizes non-quantifier braces before regex compile;
|
||||||
- if compile fails with unopened/unclosed-group errors, it retries after escaping previously unescaped parentheses;
|
- if compile fails with unopened/unclosed-group errors, it retries after escaping previously unescaped parentheses;
|
||||||
- directory scans use the grep pipeline described in `docs/natives-text-search-pipeline.md`.
|
- directory scans use the grep pipeline described in `docs/natives-text-search-pipeline.md`.
|
||||||
11. Search dispatch differs by resolved path set:
|
11. Search dispatch differs by resolved path set:
|
||||||
- exact explicit files or degenerate-root multi-targets: JS loops over targets and merges `grep()` results itself;
|
- exact explicit files or fanned-out multi-targets: JS loops over targets, merges `grep()` results itself, and deduplicates overlapping targets by absolute path + line number;
|
||||||
- single file/directory base: one `grep()` call handles native scanning.
|
- single file/directory base: one `grep()` call handles native scanning.
|
||||||
12. Virtual internal resources are searched in JS with `RegExp`; archive scratch paths and virtual paths are remapped back to user-facing selectors before rendering.
|
12. Virtual internal resources are searched in JS with `RegExp`; archive scratch paths and virtual paths are remapped back to user-facing selectors before rendering.
|
||||||
13. JS output shaping then:
|
13. JS output shaping then:
|
||||||
@@ -87,7 +90,7 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- caps matches per file to 20 for multi-file scopes and 200 for single-file scopes;
|
- caps matches per file to 20 for multi-file scopes and 200 for single-file scopes;
|
||||||
- round-robins selected per-file matches so one file does not monopolize the page;
|
- round-robins selected per-file matches so one file does not monopolize the page;
|
||||||
- formats lines through `formatMatchLine()` for the model and `formatCodeFrameLine()` for TUI;
|
- formats lines through `formatMatchLine()` for the model and `formatCodeFrameLine()` for TUI;
|
||||||
- records non-truncated matched/context lines into the session file-read cache with `recordSparse()`.
|
- in hashline mode, records a whole-file snapshot per rendered file with `recordFileSnapshot()` to mint the `#TAG` anchor (archive, virtual, and immutable paths are skipped).
|
||||||
14. Final text is passed through `truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER })`, so the effective cap is the default byte cap from `streaming-output.ts`, not the default line cap.
|
14. Final text is passed through `truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER })`, so the effective cap is the default byte cap from `streaming-output.ts`, not the default line cap.
|
||||||
15. `toolResult()` attaches text plus limit/truncation metadata.
|
15. `toolResult()` attaches text plus limit/truncation metadata.
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- Results are grouped into a 20-file page; use `skip` with the next file offset shown in the limit message.
|
- Results are grouped into a 20-file page; use `skip` with the next file offset shown in the limit message.
|
||||||
- JS round-robins the selected files' matches.
|
- JS round-robins the selected files' matches.
|
||||||
3. **Multiple explicit paths/globs**
|
3. **Multiple explicit paths/globs**
|
||||||
- `resolveExplicitSearchPaths()` collapses them into a common base and either a brace-union glob, an explicit file list, or per-target searches when the only common base is the filesystem root.
|
- `resolveExplicitSearchPaths()` collapses them into a common base and either a brace-union glob, an explicit file list, or per-target searches when the common ancestor is not itself a requested scope (or a plain-file entry would be demoted into a directory walk).
|
||||||
- Missing entries are skipped non-fatally unless all are missing.
|
- Missing entries are skipped non-fatally unless all are missing.
|
||||||
4. **Archive member paths**
|
4. **Archive member paths**
|
||||||
- Supported for UTF-8 text entries only. The member is extracted to a temporary scratch file for native grep, then displayed as `archive.ext:member`.
|
- Supported for UTF-8 text entries only. The member is extracted to a temporary scratch file for native grep, then displayed as `archive.ext:member`.
|
||||||
@@ -117,14 +120,14 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- Filesystem
|
- Filesystem
|
||||||
- Stats resolved search roots and input paths.
|
- Stats resolved search roots and input paths.
|
||||||
- Reads matched files through native `grep()`.
|
- Reads matched files through native `grep()`.
|
||||||
- Records sparse matched/context lines into the session file-read cache via `getFileReadCache(...).recordSparse(...)`.
|
- Records whole-file snapshots into the session file-snapshot store via `recordFileSnapshot()` for hashline anchors.
|
||||||
- Session state (transcript, memory, jobs, checkpoints, registries)
|
- Session state (transcript, memory, jobs, checkpoints, registries)
|
||||||
- Reads session settings for context defaults.
|
- Reads session settings for context defaults.
|
||||||
- Uses `session.internalRouter` to resolve internal URLs.
|
- Uses `session.internalRouter` to resolve internal URLs.
|
||||||
- Populates tool `details.meta` with truncation/limit metadata.
|
- Populates tool `details.meta` with truncation/limit metadata.
|
||||||
- Background work / cancellation
|
- Background work / cancellation
|
||||||
- Wrapped in `untilAborted(signal, ...)` at the JS level.
|
- Wrapped in `untilAborted(signal, ...)` at the JS level.
|
||||||
- `search.ts` does not pass `signal` or `timeoutMs` into native `grep()`, so native grep cancellation/timeouts are not used by this tool.
|
- `search.ts` passes the abort `signal` and `timeoutMs: SEARCH_GREP_TIMEOUT_MS` (`30_000`) into native `grep()`, so native scans are cancellable and time-bounded.
|
||||||
|
|
||||||
## Limits & Caps
|
## Limits & Caps
|
||||||
- File page limit: `20` files (`DEFAULT_FILE_LIMIT` in `packages/coding-agent/src/tools/search.ts`).
|
- File page limit: `20` files (`DEFAULT_FILE_LIMIT` in `packages/coding-agent/src/tools/search.ts`).
|
||||||
@@ -135,6 +138,8 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- Context defaults: `search.contextBefore = 1`, `search.contextAfter = 3` in `packages/coding-agent/src/config/settings-schema.ts`.
|
- Context defaults: `search.contextBefore = 1`, `search.contextAfter = 3` in `packages/coding-agent/src/config/settings-schema.ts`.
|
||||||
- Pagination: `skip` is a file-page offset for multi-file scopes. The result text says `Use skip=<N> for the next page` when more files remain.
|
- Pagination: `skip` is a file-page offset for multi-file scopes. The result text says `Use skip=<N> for the next page` when more files remain.
|
||||||
- Native directory-scan cache: available in `grep.rs`, but this tool always sets `cache: false`.
|
- Native directory-scan cache: available in `grep.rs`, but this tool always sets `cache: false`.
|
||||||
|
- Native grep wall-clock budget: `30_000ms` per invocation (`SEARCH_GREP_TIMEOUT_MS` in `packages/coding-agent/src/tools/search.ts`); hitting it raises `Search timed out after 30s; ...`.
|
||||||
|
- Native per-file size cap: `4 * 1024 * 1024` bytes (`MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`, mirrored as `NATIVE_GREP_MAX_FILE_BYTES` in `search.ts`). Oversized files are silently skipped by native grep; `search.ts` surfaces a `Skipped oversized file(s)` note (with names for explicit file targets, a count for directory scans).
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- `Pattern must not be empty` when trimmed `pattern` is empty.
|
- `Pattern must not be empty` when trimmed `pattern` is empty.
|
||||||
@@ -143,9 +148,10 @@ The tool returns a single text block in `content[0].text` plus structured `detai
|
|||||||
- `Glob patterns are not supported for internal URLs: ...` for internal URL + glob metacharacters.
|
- `Glob patterns are not supported for internal URLs: ...` for internal URL + glob metacharacters.
|
||||||
- Line-range selector errors include `Line-range selector requires a single file, not a glob: ...`, `Line-range selector requires a single file: ... is a directory`, and `Path not found for line-range selector: ...`.
|
- Line-range selector errors include `Line-range selector requires a single file, not a glob: ...`, `Line-range selector requires a single file: ... is a directory`, and `Path not found for line-range selector: ...`.
|
||||||
- `Cannot search archive member(s): ...` when all archive selectors are unreadable, binary, or non-UTF-8.
|
- `Cannot search archive member(s): ...` when all archive selectors are unreadable, binary, or non-UTF-8.
|
||||||
- `Path not found: ...` when a filesystem-backed resolved base path is missing, or when every multi-path filesystem entry is missing.
|
- `Path not found: ...; pass each path as its own array element` when a filesystem-backed resolved base path is missing, or when every multi-path filesystem entry is missing (with an archive hint when unreadable archive members contributed).
|
||||||
- Virtual internal URL regex compile failures are reported as `Invalid regex: ...` from JavaScript `RegExp`; filesystem-backed regex failures beginning with `regex` or `regex parse error` are normalized to `Invalid regex: ...`.
|
- Virtual internal URL regex compile failures are reported as `Invalid regex: ...` from JavaScript `RegExp`; filesystem-backed regex failures beginning with `regex` or `regex parse error` are normalized to `Invalid regex: ...`.
|
||||||
- Multi-file native scans skip per-file open/search failures inside `grep.rs`; the scan continues with surviving files.
|
- Multi-file native scans skip per-file open/search failures inside `grep.rs`; the scan continues with surviving files.
|
||||||
|
- ``Search timed out after 30s; narrow paths or pattern, or scope with `find` first`` when native grep hits `SEARCH_GREP_TIMEOUT_MS`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- The model-facing prompt documents Rust regex syntax for filesystem-backed searches and JavaScript `RegExp` for virtual internal URL content.
|
- The model-facing prompt documents Rust regex syntax for filesystem-backed searches and JavaScript `RegExp` for virtual internal URL content.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
- missing discovery hooks -> `ToolError("Tool discovery is unavailable in this session.")`
|
- missing discovery hooks -> `ToolError("Tool discovery is unavailable in this session.")`
|
||||||
- discovery disabled -> `ToolError("Tool discovery is disabled. Enable tools.discoveryMode or mcp.discoveryMode to use search_tool_bm25.")`
|
- discovery disabled -> `ToolError("Tool discovery is disabled. Enable tools.discoveryMode or mcp.discoveryMode to use search_tool_bm25.")`
|
||||||
4. `query` is trimmed and validated; `limit` is defaulted/validated.
|
4. `query` is trimmed and validated; `limit` is defaulted/validated.
|
||||||
5. `getDiscoverableToolSearchIndexForExecution()` fetches the cached generic search index from the session when available, otherwise falls back to the legacy MCP cache, otherwise rebuilds an index from the current discoverable-tool list.
|
5. `getDiscoverableToolSearchIndexForExecution()` fetches the cached generic search index from the session when available, otherwise rebuilds an index from the current discoverable-tool list.
|
||||||
6. `getSelectedToolNames()` reads the current discovered selections so already-selected tools can be excluded from fresh results.
|
6. `getSelectedToolNames()` reads the current discovered selections so already-selected tools can be excluded from fresh results.
|
||||||
7. `searchDiscoverableTools()` in `packages/coding-agent/src/tool-discovery/tool-index.ts` tokenizes the query, scores every document with BM25, sorts by descending score then `tool.name`, and returns up to `searchIndex.documents.length` results; `execute()` then filters already-selected names and slices to `limit`.
|
7. `searchDiscoverableTools()` in `packages/coding-agent/src/tool-discovery/tool-index.ts` tokenizes the query, scores every document with BM25, sorts by descending score then `tool.name`, and returns up to `searchIndex.documents.length` results; `execute()` then filters already-selected names and slices to `limit`.
|
||||||
8. If any matches remain, `activateTools()` activates all matched tool names through `session.activateDiscoveredTools()` or legacy `activateDiscoveredMCPTools()`.
|
8. If any matches remain, `activateTools()` activates all matched tool names through `session.activateDiscoveredTools()` or legacy `activateDiscoveredMCPTools()`.
|
||||||
@@ -64,9 +64,8 @@
|
|||||||
- `tools.discoveryMode = "mcp-only"`: searches hidden MCP tools only.
|
- `tools.discoveryMode = "mcp-only"`: searches hidden MCP tools only.
|
||||||
- legacy `mcp.discoveryMode = true`: same as MCP-only.
|
- legacy `mcp.discoveryMode = true`: same as MCP-only.
|
||||||
- Search-index source:
|
- Search-index source:
|
||||||
- generic cached discoverable index from the session
|
- generic cached discoverable index from the session (`getDiscoverableToolSearchIndex()`)
|
||||||
- legacy cached MCP index, cast to the generic shape
|
- rebuilt ad hoc from the current discoverable-tool list when the cache path fails
|
||||||
- rebuilt ad hoc from the current discoverable-tool list if neither cache path works
|
|
||||||
- Activation backend:
|
- Activation backend:
|
||||||
- generic `activateDiscoveredTools()`
|
- generic `activateDiscoveredTools()`
|
||||||
- legacy `activateDiscoveredMCPTools()` fallback
|
- legacy `activateDiscoveredMCPTools()` fallback
|
||||||
|
|||||||
+9
-9
@@ -14,8 +14,8 @@
|
|||||||
- `packages/coding-agent/src/registry/agent-registry.ts` — process-global agent directory (`running | idle | parked | aborted`).
|
- `packages/coding-agent/src/registry/agent-registry.ts` — process-global agent directory (`running | idle | parked | aborted`).
|
||||||
- `packages/coding-agent/src/async/job-manager.ts` — background job registration, progress, and result delivery.
|
- `packages/coding-agent/src/async/job-manager.ts` — background job registration, progress, and result delivery.
|
||||||
- `packages/coding-agent/src/task/parallel.ts` — `Semaphore` used for the session-scoped concurrency bound.
|
- `packages/coding-agent/src/task/parallel.ts` — `Semaphore` used for the session-scoped concurrency bound.
|
||||||
- `packages/coding-agent/src/task/isolation-backend.ts` — isolation backend resolution and platform fallback.
|
- `@oh-my-pi/pi-natives` (`crates/pi-iso`) — isolation PAL: `isoResolve` / `isoStart` / `isoStop` backend resolution and fallback.
|
||||||
- `packages/coding-agent/src/task/worktree.ts` — worktree / FUSE / ProjFS setup, patch capture, branch merge.
|
- `packages/coding-agent/src/task/worktree.ts` — isolation mode mapping (`parseIsolationMode`) and lifecycle (`ensureIsolation`/`cleanupIsolation`), patch capture, branch merge.
|
||||||
- `packages/coding-agent/src/task/output-manager.ts` — session-scoped `agent://` id allocation.
|
- `packages/coding-agent/src/task/output-manager.ts` — session-scoped `agent://` id allocation.
|
||||||
- `packages/coding-agent/src/task/name-generator.ts` — default AdjectiveNoun agent ids.
|
- `packages/coding-agent/src/task/name-generator.ts` — default AdjectiveNoun agent ids.
|
||||||
- `packages/coding-agent/src/internal-urls/agent-protocol.ts` — resolve `agent://<id>` to saved subagent output.
|
- `packages/coding-agent/src/internal-urls/agent-protocol.ts` — resolve `agent://<id>` to saved subagent output.
|
||||||
@@ -84,11 +84,11 @@ Artifacts and side channels:
|
|||||||
6. It resolves the requested agent, rejects unknown or settings-disabled agents, and enforces parent spawn policy plus `PI_BLOCKED_AGENT` self-recursion prevention.
|
6. It resolves the requested agent, rejects unknown or settings-disabled agents, and enforces parent spawn policy plus `PI_BLOCKED_AGENT` self-recursion prevention.
|
||||||
7. Output schema priority: agent frontmatter `output` → inherited parent session schema (the call itself never carries one).
|
7. Output schema priority: agent frontmatter `output` → inherited parent session schema (the call itself never carries one).
|
||||||
8. Plan mode swaps in an `effectiveAgent` with a read-only tool subset and plan-mode prompt; `runSubprocess(...)` receives the effective agent.
|
8. Plan mode swaps in an `effectiveAgent` with a read-only tool subset and plan-mode prompt; `runSubprocess(...)` receives the effective agent.
|
||||||
9. If `isolated`, it requires a git repo (`getRepoRoot(...)` / `captureBaseline(...)`) and resolves the backend through isolation-backend resolution with platform fallback.
|
9. If `isolated`, it requires a git repo (`getRepoRoot(...)` / `captureBaseline(...)`), maps `task.isolation.mode` to a backend-kind hint (`parseIsolationMode`), and materializes the workspace via the natives PAL (`ensureIsolation` → `isoResolve`/`isoStart`), walking the candidate list when a backend is unavailable.
|
||||||
10. Artifacts dir comes from the parent session file when available, otherwise a temp dir. When the session is executing an approved plan, the plan reference is handed to the subagent.
|
10. Artifacts dir comes from the parent session file when available, otherwise a temp dir. When the session is executing an approved plan, the plan reference is handed to the subagent.
|
||||||
11. Non-isolated spawns call `runSubprocess(...)` directly with parent cwd; isolated spawns run inside the isolation workspace, then commit to a branch (`mergeMode === "branch"`) or capture a patch, and always clean up the workspace.
|
11. Non-isolated spawns call `runSubprocess(...)` directly with parent cwd; isolated spawns run inside the isolation workspace, then commit to a branch (`mergeMode === "branch"`) or capture a patch, and always clean up the workspace.
|
||||||
12. `runSubprocess(...)` creates a child agent session with an isolated settings snapshot (forcing `async.enabled = false` and `bash.autoBackground.enabled = false` — subagents are internally synchronous), child `agentId` equal to the allocated id, child internal URL router/`AgentOutputManager`, output schema, the shared `context` (batch calls) in the system prompt's `CONTEXT` section, and the IRC peer roster in the system prompt.
|
12. `runSubprocess(...)` creates a child agent session with an isolated settings snapshot (forcing `async.enabled = false` and `bash.autoBackground.enabled = false` — subagents are internally synchronous), child `agentId` equal to the allocated id, child internal URL router/`AgentOutputManager`, output schema, the shared `context` (batch calls) in the system prompt's `CONTEXT` section, and the IRC peer roster in the system prompt.
|
||||||
13. Child tool availability: explicit `agent.tools` if provided; auto-add `task` when the agent has `spawns` and depth allows; strip `task` at `task.maxRecursionDepth`; expand `exec` to `eval` + `bash`; strip parent-owned `todo`.
|
13. Child tool availability: explicit `agent.tools` if provided; auto-add `task` when the agent has `spawns` and depth allows; strip `task` at `task.maxRecursionDepth`; ensure `irc` is present in explicit tool lists; expand `exec` to `eval` + `bash`; strip parent-owned `todo`.
|
||||||
14. The child must finish through the hidden `yield` tool; up to 3 reminder prompts, the last forcing `toolChoice = yield` when supported. `finalizeSubprocessOutput(...)` reconciles raw text, `yield` payloads, structured schemas, `report_finding` data, and abort states.
|
14. The child must finish through the hidden `yield` tool; up to 3 reminder prompts, the last forcing `toolChoice = yield` when supported. `finalizeSubprocessOutput(...)` reconciles raw text, `yield` payloads, structured schemas, `report_finding` data, and abort states.
|
||||||
15. End-of-run lifecycle (keep-alive, in `runSubprocess`'s finalizer):
|
15. End-of-run lifecycle (keep-alive, in `runSubprocess`'s finalizer):
|
||||||
- hard abort (caller signal / wall-clock / budget) → registry status `aborted`, session disposed — terminal;
|
- hard abort (caller signal / wall-clock / budget) → registry status `aborted`, session disposed — terminal;
|
||||||
@@ -103,7 +103,7 @@ Artifacts and side channels:
|
|||||||
- Batch mode (`task.batch`, default on)
|
- Batch mode (`task.batch`, default on)
|
||||||
- on — `{ agent, context, tasks[] }`: one independent spawn per item, required `context` shared across the call's spawns, `isolated` per item. Lifecycle, revival, and concurrency semantics match N parallel single calls.
|
- on — `{ agent, context, tasks[] }`: one independent spawn per item, required `context` shared across the call's spawns, `isolated` per item. Lifecycle, revival, and concurrency semantics match N parallel single calls.
|
||||||
- off — single spawn per call; `tasks`/`context` are rejected and removed from the schema.
|
- off — single spawn per call; `tasks`/`context` are rejected and removed from the schema.
|
||||||
- Isolation backend: `none`, `worktree`, `fuse-overlay`, `fuse-projfs`.
|
- Isolation mode (`task.isolation.mode`): `none`, `auto`, `apfs`, `btrfs`, `zfs`, `reflink`, `overlayfs`, `projfs`, `block-clone`, `rcopy` (legacy `worktree`, `fuse-overlay`, `fuse-projfs` accepted for back-compat); the PAL resolves the actual backend with fallback.
|
||||||
- Isolation merge strategy: patch mode (capture/apply root patches) or branch mode (commit to `omp/task/<id>`, cherry-pick into parent).
|
- Isolation merge strategy: patch mode (capture/apply root patches) or branch mode (commit to `omp/task/<id>`, cherry-pick into parent).
|
||||||
- Agent source precedence: project custom agents, then user custom agents, then bundled agents (`explore`, `plan`, `designer`, `reviewer`, `task`, `quick_task`, `librarian`, `oracle`).
|
- Agent source precedence: project custom agents, then user custom agents, then bundled agents (`explore`, `plan`, `designer`, `reviewer`, `task`, `quick_task`, `librarian`, `oracle`).
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ Artifacts and side channels:
|
|||||||
- Child sessions may use whichever networked tools/models their active tool set permits.
|
- Child sessions may use whichever networked tools/models their active tool set permits.
|
||||||
- MCP proxy tools can call existing parent MCP connections with a 60_000 ms timeout.
|
- MCP proxy tools can call existing parent MCP connections with a 60_000 ms timeout.
|
||||||
- Subprocesses / native bindings
|
- Subprocesses / native bindings
|
||||||
- `fuse-overlayfs` and `fusermount`/`fusermount3` for FUSE isolation; ProjFS native bindings on Windows.
|
- Isolation backends run through the `pi-natives` PAL (`crates/pi-iso`): kernel `overlay` with `fuse-overlayfs`/`fusermount[3]` fallback on Linux, APFS/Btrfs/ZFS/reflink clones, ProjFS on Windows, recursive copy as last resort.
|
||||||
- Git operations for baseline capture, patch apply, worktrees, branches, stash, cherry-pick, commits.
|
- Git operations for baseline capture, patch apply, worktrees, branches, stash, cherry-pick, commits.
|
||||||
- Session state (transcript, memory, jobs, checkpoints, registries)
|
- Session state (transcript, memory, jobs, checkpoints, registries)
|
||||||
- Creates child `AgentSession` instances with isolated settings snapshots; finished sessions stay registered in the process-global `AgentRegistry` as `idle`/`parked` until process teardown or explicit release.
|
- Creates child `AgentSession` instances with isolated settings snapshots; finished sessions stay registered in the process-global `AgentRegistry` as `idle`/`parked` until process teardown or explicit release.
|
||||||
@@ -147,7 +147,7 @@ Artifacts and side channels:
|
|||||||
- batch calls: missing/empty `tasks`, an item without `assignment`, duplicate provided ids, missing shared `context`, top-level `assignment` alongside `tasks`
|
- batch calls: missing/empty `tasks`, an item without `assignment`, duplicate provided ids, missing shared `context`, top-level `assignment` alongside `tasks`
|
||||||
- flat calls: missing/empty `assignment`
|
- flat calls: missing/empty `assignment`
|
||||||
- unknown or settings-disabled agent, spawn-policy denial, requesting `isolated` while isolation mode is `none`
|
- unknown or settings-disabled agent, spawn-policy denial, requesting `isolated` while isolation mode is `none`
|
||||||
- Isolated execution without a git repo returns `Isolated task execution requires a git repository. ...`; backend resolution can hard-error (ProjFS init) or warn and fall back to `worktree`.
|
- Isolated execution without a git repo returns `Isolated task execution requires a git repository. ...`; unavailable backends fall back through the PAL candidate list (reported via `fellBack`/`fallbackReason`), other backend errors rethrow, and exhausting every candidate errors with the fallback reason.
|
||||||
- Job registration failure returns `Failed to start background task job(s): ...`; a batch that schedules only some jobs reports the failed ids in the immediate text and keeps the started ones running.
|
- Job registration failure returns `Failed to start background task job(s): ...`; a batch that schedules only some jobs reports the failed ids in the immediate text and keeps the started ones running.
|
||||||
- Child failures surface as `SingleResult.exitCode = 1` with `stderr`/`error` populated; the async job is marked failed but the delivery text still carries the output plus a follow-up/transcript hint.
|
- Child failures surface as `SingleResult.exitCode = 1` with `stderr`/`error` populated; the async job is marked failed but the delivery text still carries the output plus a follow-up/transcript hint.
|
||||||
- If the child omits `yield`, `finalizeSubprocessOutput(...)` injects warnings such as `SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.`
|
- If the child omits `yield`, `finalizeSubprocessOutput(...)` injects warnings such as `SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.`
|
||||||
@@ -159,9 +159,9 @@ Artifacts and side channels:
|
|||||||
- Prefer messaging an existing agent (`irc`) over a fresh spawn for follow-up work: it already holds the relevant context. `irc` op:"list" shows idle/parked candidates; messaging a parked agent revives it. `history://<id>` shows what an agent has done.
|
- Prefer messaging an existing agent (`irc`) over a fresh spawn for follow-up work: it already holds the relevant context. `irc` op:"list" shows idle/parked candidates; messaging a parked agent revives it. `history://<id>` shows what an agent has done.
|
||||||
- `irc` availability is derived, not configured (`isIrcEnabled` in `packages/coding-agent/src/tools/irc.ts`): it exists exactly when there is someone to message — the session can spawn subagents, or it is a subagent itself. Messaging is the only follow-up path to a finished subagent, so task without irc would strand idle agents.
|
- `irc` availability is derived, not configured (`isIrcEnabled` in `packages/coding-agent/src/tools/irc.ts`): it exists exactly when there is someone to message — the session can spawn subagents, or it is a subagent itself. Messaging is the only follow-up path to a finished subagent, so task without irc would strand idle agents.
|
||||||
- Subagents are internally synchronous: the executor forces `async.enabled = false` and `bash.autoBackground.enabled = false` in the child settings snapshot, so there are no fire-and-forget grandchildren.
|
- Subagents are internally synchronous: the executor forces `async.enabled = false` and `bash.autoBackground.enabled = false` in the child settings snapshot, so there are no fire-and-forget grandchildren.
|
||||||
- Agent discovery precedence is first-wins by exact name: project dirs before user dirs within a source family, plugin agent dirs after config dirs, bundled agents last. Create-time discovery is memoized per cwd for the prompt description; execution-time discovery stays fresh.
|
- Agent discovery precedence is first-wins by exact name: project `.omp` agents dir before the user `.omp` dir (task agents only load from `.omp` roots; `.claude`/`.codex`/`.gemini` agent dirs are skipped), Claude plugin agent dirs after config dirs, bundled agents last. Create-time discovery is memoized per cwd for the prompt description; execution-time discovery stays fresh.
|
||||||
- Child sessions do not inherit conversation history. Built-in carry-over is the workspace tree/skills/context files, the shared `local://` root, and the approved-plan reference when one exists.
|
- Child sessions do not inherit conversation history. Built-in carry-over is the workspace tree/skills/context files, the shared `local://` root, and the approved-plan reference when one exists.
|
||||||
- When the parent passes `mcpManager`, child sessions disable standalone MCP discovery and get proxy tools that reuse parent connections.
|
- When the parent passes `mcpManager`, child sessions disable standalone MCP discovery and get proxy tools that reuse parent connections.
|
||||||
- Branch-mode merge temporarily stashes the parent repo before cherry-picking; a stash-pop conflict is treated as merge failure and leaves recovery state behind. Patch mode only applies the combined root patch when `git.patch.canApplyText(...)` succeeds; failures leave the `.patch` artifact for manual handling.
|
- Branch-mode merge temporarily stashes the parent repo before cherry-picking; a stash-pop conflict does not unmerge the cherry-picked commits — they stay on HEAD, the stash entry is preserved, and the conflict is surfaced separately as `stashConflict`. Patch mode only applies the combined root patch when `git.patch.canApplyText(...)` succeeds; failures leave the `.patch` artifact for manual handling.
|
||||||
- Nested git repos are diffed independently inside isolated workspaces and merged separately with `applyNestedPatches(...)`.
|
- Nested git repos are diffed independently inside isolated workspaces and merged separately with `applyNestedPatches(...)`.
|
||||||
- `agent://` ids are name-based (`Task` first, `Task-2`/`Task-3` only when the name repeats, nested like `Parent.Child`) by `AgentOutputManager`; this is what prevents artifact collisions across repeated or nested invocations.
|
- `agent://` ids are name-based (`Task` first, `Task-2`/`Task-3` only when the name repeats, nested like `Parent.Child`) by `AgentOutputManager`; this is what prevents artifact collisions across repeated or nested invocations.
|
||||||
|
|||||||
+13
-12
@@ -28,7 +28,7 @@
|
|||||||
| `drop` | `task` or `phase` or neither | None | Marks the target task, phase, or all tasks `abandoned`. |
|
| `drop` | `task` or `phase` or neither | None | Marks the target task, phase, or all tasks `abandoned`. |
|
||||||
| `rm` | `task` or `phase` or neither | None | Removes the target task, clears the phase's task list, or clears all task lists. |
|
| `rm` | `task` or `phase` or neither | None | Removes the target task, clears the phase's task list, or clears all task lists. |
|
||||||
| `append` | `phase`, `items` | None | Appends new `pending` tasks to a phase; creates the phase if missing. |
|
| `append` | `phase`, `items` | None | Appends new `pending` tasks to a phase; creates the phase if missing. |
|
||||||
| `view` | None | None | Echoes the current list without mutating or normalizing session state. |
|
| `view` | None | None | Echoes the current list. A call whose ops are all `view` is read-only: no normalization, no state write. |
|
||||||
|
|
||||||
### Fields used inside ops
|
### Fields used inside ops
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
The tool returns a single-shot `AgentToolResult`:
|
The tool returns a single-shot `AgentToolResult`:
|
||||||
|
|
||||||
- `content`: one text part containing the summary from `formatSummary(...)`.
|
- `content`: one text part containing the summary from `formatSummary(...)`.
|
||||||
- Empty final state with no errors: `Todo list cleared.`
|
- Empty final state with no errors: `Todo list cleared.` (`Todo list is empty.` for a pure-`view` call).
|
||||||
- Non-empty final state: remaining-item list, current phase progress, then a per-phase tree.
|
- Non-empty final state: remaining-item list, current phase progress, then a per-phase tree.
|
||||||
- If any op produced validation/runtime errors, the summary starts with `Errors: ...`; the returned tool result is marked `isError: true` and still includes the mutated state.
|
- If any op produced validation/runtime errors, the summary starts with `Errors: ...` and the result is marked `isError: true`; the whole batch is discarded — the returned and persisted state stay at the pre-call list.
|
||||||
- `details`:
|
- `details`:
|
||||||
- `phases: TodoPhase[]`
|
- `phases: TodoPhase[]`
|
||||||
- `storage: "session" | "memory"`
|
- `storage: "session" | "memory"`
|
||||||
@@ -68,12 +68,12 @@ The TUI renderer (`todoToolRenderer`) merges call and result into one transcript
|
|||||||
- `done` / `drop` use `getTaskTargets(...)` to target one task, one phase, or every task.
|
- `done` / `drop` use `getTaskTargets(...)` to target one task, one phase, or every task.
|
||||||
- `rm` removes one task, clears one phase's `tasks`, or clears all phases' task arrays.
|
- `rm` removes one task, clears one phase's `tasks`, or clears all phases' task arrays.
|
||||||
- `appendItems(...)` resolves or creates the target phase and pushes new `pending` tasks unless the same task content already exists anywhere.
|
- `appendItems(...)` resolves or creates the target phase and pushes new `pending` tasks unless the same task content already exists anywhere.
|
||||||
4. Missing task/phase references are recorded in an `errors` array by `resolveTaskOrError(...)` / `resolvePhaseOrError(...)`; execution continues through the rest of the batch.
|
4. Missing task/phase references are recorded in an `errors` array by `resolveTaskOrError(...)` / `resolvePhaseOrError(...)`; execution continues through the rest of the batch, but any error discards the batch's mutations at the end.
|
||||||
5. After the full batch, `normalizeInProgressTask(...)` enforces the single-active-task invariant:
|
5. After the full batch, `normalizeInProgressTask(...)` enforces the single-active-task invariant:
|
||||||
- if multiple tasks are `in_progress`, only the first stays active and the rest become `pending`;
|
- if multiple tasks are `in_progress`, only the first stays active and the rest become `pending`;
|
||||||
- if none are `in_progress`, the first `pending` task in phase/task order is auto-promoted to `in_progress`.
|
- if none are `in_progress`, the first `pending` task in phase/task order is auto-promoted to `in_progress`.
|
||||||
6. `execute(...)` stores the normalized phases with `session.setTodoPhases?.(...)` and reports `storage` as `"session"` when `session.getSessionFile()` exists, else `"memory"`.
|
6. `execute(...)` stores the updated phases with `session.setTodoPhases?.(...)` only when the batch produced no errors and was not pure-`view`; a failed batch is discarded wholesale (persisting a half-applied batch would make the natural retry hit "already exists"). `storage` is `"session"` when `session.getSessionFile()` exists, else `"memory"`.
|
||||||
7. `getCompletionTransitions(...)` compares the previous and updated phases; newly completed tasks are returned in `details.completedTasks`.
|
7. `getCompletionTransitions(...)` compares the previous and updated phases (skipped for failed or pure-`view` calls); newly completed tasks are returned in `details.completedTasks`.
|
||||||
8. The agent runtime also watches `todo` tool results in `packages/coding-agent/src/session/agent-session.ts`; successful results refresh cached todos, failed results inject a hidden next-turn reminder telling the model that todo progress is not visible until it retries.
|
8. The agent runtime also watches `todo` tool results in `packages/coding-agent/src/session/agent-session.ts`; successful results refresh cached todos, failed results inject a hidden next-turn reminder telling the model that todo progress is not visible until it retries.
|
||||||
9. The event controller updates the visible todo UI from `result.details.phases` on success, or shows a warning on error (`packages/coding-agent/src/modes/controllers/event-controller.ts`).
|
9. The event controller updates the visible todo UI from `result.details.phases` on success, or shows a warning on error (`packages/coding-agent/src/modes/controllers/event-controller.ts`).
|
||||||
|
|
||||||
@@ -115,37 +115,38 @@ The same file also exposes non-tool helpers used by `/todo`:
|
|||||||
- `event-controller` updates the visible todo panel from successful results.
|
- `event-controller` updates the visible todo panel from successful results.
|
||||||
- On error, `event-controller` shows `Todo update failed...`; the visible panel may stay stale until a later successful call.
|
- On error, `event-controller` shows `Todo update failed...`; the visible panel may stay stale until a later successful call.
|
||||||
- Background work / cancellation
|
- Background work / cancellation
|
||||||
- `AgentSession.setTodoPhases(...)` schedules auto-clear timers for `completed` / `abandoned` tasks via `tasks.todoClearDelay`.
|
- Session-level auto-clear of `completed`/`abandoned` tasks was removed (the timer mutated canonical phases between tool calls); the TUI todo widget still clears closed entries after `tasks.todoClearDelay` (display-only, `packages/coding-agent/src/modes/interactive-mode.ts`).
|
||||||
|
|
||||||
## Limits & Caps
|
## Limits & Caps
|
||||||
- `ops` array: `minItems: 1` (`todoSchema`).
|
- `ops` array: `minItems: 1` (`todoSchema`).
|
||||||
- `init.list[*].items`: `minItems: 1`.
|
- `init.list[*].items`: `minItems: 1`.
|
||||||
- `append.items`: `minItems: 1`.
|
- `append.items`: `minItems: 1`.
|
||||||
- Renderer collapsed preview: `PREVIEW_LIMITS.COLLAPSED_ITEMS = 8` (`packages/coding-agent/src/tools/render-utils.ts`).
|
- Renderer collapsed preview: `PREVIEW_LIMITS.COLLAPSED_ITEMS = 8` (`packages/coding-agent/src/tools/render-utils.ts`).
|
||||||
- Auto-clear delay: `tasks.todoClearDelay` default `60` seconds; `< 0` disables auto-clear, `0` clears on the next microtask (`packages/coding-agent/src/session/agent-session.ts`).
|
- Auto-clear delay: `tasks.todoClearDelay` default `60` seconds; `< 0` disables auto-clear, `0` clears immediately. Display-only — applied by the TUI widget (`packages/coding-agent/src/modes/interactive-mode.ts`); the setting is inert at the session level.
|
||||||
- Tool execution mode: `concurrency = "exclusive"`, `strict = true`, `loadMode = "discoverable"`.
|
- Tool execution mode: `concurrency = "exclusive"`, `strict = true`, `loadMode = "discoverable"`.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
- Ordinary bad op payloads are accumulated as human-readable strings in `errors`; the tool still returns the mutated state, but marks the result `isError: true`.
|
- Ordinary bad op payloads are accumulated as human-readable strings in `errors`; the result is marked `isError: true` and the whole batch is discarded — the returned and persisted state stay at the pre-call list.
|
||||||
- Error strings come from the helpers in `packages/coding-agent/src/tools/todo.ts`, including:
|
- Error strings come from the helpers in `packages/coding-agent/src/tools/todo.ts`, including:
|
||||||
- `Missing list for init operation`
|
- `Missing list for init operation`
|
||||||
- `Missing task content`
|
- `Missing task content`
|
||||||
|
- `Duplicate phase "..." in init list` / `Duplicate task "..." in init list`
|
||||||
- `Task "..." not found` with an extra empty-list hint when applicable
|
- `Task "..." not found` with an extra empty-list hint when applicable
|
||||||
- `Missing phase name`
|
- `Missing phase name`
|
||||||
- `Phase "..." not found`
|
- `Phase "..." not found`
|
||||||
- `Missing phase name for append operation`
|
- `Missing phase name for append operation`
|
||||||
- `Missing items for append operation`
|
- `Missing items for append operation`
|
||||||
- `Task "..." already exists`
|
- `Task "..." already exists`
|
||||||
- Because ops are processed in order, earlier errors do not roll back later ops.
|
- Ops are processed in order and an early error does not stop later ops from being attempted, but any error in the batch discards every mutation the batch made.
|
||||||
- Runtime-level tool failure is handled outside the tool body: `agent-session` injects a hidden reminder and the event controller warns the user that visible progress may be stale.
|
- Runtime-level tool failure is handled outside the tool body: `agent-session` injects a hidden reminder and the event controller warns the user that visible progress may be stale.
|
||||||
- Idempotency is op-specific:
|
- Idempotency is op-specific:
|
||||||
- `init` is a full replacement; replaying the same payload yields the same state.
|
- `init` is a full replacement; replaying the same payload yields the same state.
|
||||||
- `start`, `done`, and `drop` are effectively idempotent on an existing target state, but `start` also demotes any other active task.
|
- `start`, `done`, and `drop` are effectively idempotent on an existing target state, but `start` also demotes any other active task.
|
||||||
- `rm` is not idempotent for targeted removals: the second call errors because the task or phase is gone.
|
- `rm` is not idempotent for targeted removals: the second call errors because the task or phase is gone.
|
||||||
- `append` is not idempotent: duplicate task content is rejected with `Task "..." already exists`.
|
- `append` is not idempotent: duplicate task content is rejected with `Task "..." already exists`; the whole `append` op validates up front, so a batch with any duplicate appends nothing.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Task lookup is exact string equality inside the tool. The model-facing prompt says task content and phase names are identifiers and should stay unique; `append` enforces task uniqueness globally, but `init` does not validate duplicate task or phase names.
|
- Task lookup is exact string equality inside the tool. The model-facing prompt says task content and phase names are identifiers and should stay unique; `append` enforces task uniqueness globally, and `init` rejects duplicate phase names and duplicate task contents in its payload.
|
||||||
- `findTaskByContent(...)` returns the first matching task across phases. Duplicate task contents make later targeted ops ambiguous.
|
- `findTaskByContent(...)` returns the first matching task across phases. Duplicate task contents make later targeted ops ambiguous.
|
||||||
- `normalizeInProgressTask(...)` runs after the whole batch, not after each op. A single call can intentionally build an intermediate invalid state and rely on final normalization.
|
- `normalizeInProgressTask(...)` runs after the whole batch, not after each op. A single call can intentionally build an intermediate invalid state and rely on final normalization.
|
||||||
- `storage: "session"` means the session has a session-file backing; it does not mean this tool wrote a durable custom entry.
|
- `storage: "session"` means the session has a session-file backing; it does not mean this tool wrote a durable custom entry.
|
||||||
|
|||||||
+11
-12
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `query` | `string` | Yes | Search query. `executeSearch()` rewrites any `2020`-`2029` substring to the current year before dispatch. |
|
| `query` | `string` | Yes | Search query, passed to providers unchanged. |
|
||||||
| `recency` | `"day" \| "week" \| "month" \| "year"` | No | Time filter. Only providers that implement it use it. Prompt text says Brave and Perplexity; code also maps it for Tavily and SearXNG. |
|
| `recency` | `"day" \| "week" \| "month" \| "year"` | No | Time filter. Only providers that implement it use it; code maps it for Brave, Perplexity, Tavily, SearXNG, and Kagi. |
|
||||||
| `limit` | `number` | No | Max results to return. Usually becomes the provider request's result-count parameter when `num_search_results` is absent. |
|
| `limit` | `number` | No | Max results to return. Usually becomes the provider request's result-count parameter when `num_search_results` is absent. |
|
||||||
| `max_tokens` | `number` | No | Passed through as `maxOutputTokens` / `max_tokens` only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
|
| `max_tokens` | `number` | No | Passed through as `maxOutputTokens` / `max_tokens` only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
|
||||||
| `temperature` | `number` | No | Passed through only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
|
| `temperature` | `number` | No | Passed through only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
|
||||||
@@ -51,7 +51,7 @@ The tool returns a single text content block plus structured `details`.
|
|||||||
`text` is produced by `formatForLLM()` in `packages/coding-agent/src/web/search/index.ts`:
|
`text` is produced by `formatForLLM()` in `packages/coding-agent/src/web/search/index.ts`:
|
||||||
|
|
||||||
- If `response.answer` exists, it is emitted first.
|
- If `response.answer` exists, it is emitted first.
|
||||||
- If sources exist, a `## Sources` section follows with a source count, then one entry per source:
|
- If sources exist, one entry per source follows (the `## Sources` header with a source count is emitted only when an answer was also produced):
|
||||||
- `[n] <title> (<formatted age or published date>)`
|
- `[n] <title> (<formatted age or published date>)`
|
||||||
- ` <url>`
|
- ` <url>`
|
||||||
- optional snippet line truncated to 240 chars.
|
- optional snippet line truncated to 240 chars.
|
||||||
@@ -70,14 +70,14 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
|
|||||||
## Flow
|
## Flow
|
||||||
1. `WebSearchTool.execute()` in `packages/coding-agent/src/web/search/index.ts` delegates directly to `executeSearch()`.
|
1. `WebSearchTool.execute()` in `packages/coding-agent/src/web/search/index.ts` delegates directly to `executeSearch()`.
|
||||||
2. `executeSearch()` chooses a provider list:
|
2. `executeSearch()` chooses a provider list:
|
||||||
- if `params.provider` is set and not `"auto"`, it loads that provider with `getSearchProvider()`; if `isAvailable()` returns true, the list is `[that provider]`, otherwise it falls back to `resolveProviderChain("auto")`.
|
- if `params.provider` is set and not `"auto"`, it loads that provider with `getSearchProvider()`; if `isExplicitlyAvailable()` returns true, the list is `[that provider]`, otherwise it falls back to `resolveProviderChain(authStorage, "auto")`.
|
||||||
- otherwise it calls `resolveProviderChain()` with the module-global preferred provider from `packages/coding-agent/src/web/search/provider.ts`.
|
- otherwise it calls `resolveProviderChain()` with the module-global preferred provider from `packages/coding-agent/src/web/search/provider.ts`.
|
||||||
3. `resolveProviderChain()` lazily loads each provider module on demand, checks `isAvailable()`, and returns only available providers. If a preferred provider is set, it is tried first, then the static `SEARCH_PROVIDER_ORDER` excluding that provider.
|
3. `resolveProviderChain()` lazily loads each provider module on demand and returns only available providers. If a preferred provider is set, it is tried first (gated by `isExplicitlyAvailable()`), then the static `SEARCH_PROVIDER_ORDER` excluding that provider, each gated by `isAvailable()`.
|
||||||
4. If no providers are available, `executeSearch()` returns `Error: No web search provider configured.` with `details.response.provider = "none"`.
|
4. If no providers are available, `executeSearch()` returns `Error: No web search provider configured.` with `details.response.provider = "none"`.
|
||||||
5. For each provider in order, `executeSearch()` calls `provider.search()` with:
|
5. For each provider in order, `executeSearch()` calls `provider.search()` with:
|
||||||
- `query` after year-rewrite,
|
- `query`,
|
||||||
- `limit`, `recency`, `temperature`, `maxOutputTokens`, `numSearchResults`,
|
- `limit`, `recency`, `temperature`, `maxOutputTokens`, `numSearchResults`,
|
||||||
- `systemPrompt` from `packages/coding-agent/src/prompts/tools/web-search.md`.
|
- `systemPrompt` from `packages/coding-agent/src/prompts/system/web-search.md`.
|
||||||
6. On the first successful `SearchResponse`, `formatForLLM()` renders answer/sources/citations/related/search-queries into one text block and returns it with `details.response`.
|
6. On the first successful `SearchResponse`, `formatForLLM()` renders answer/sources/citations/related/search-queries into one text block and returns it with `details.response`.
|
||||||
7. If a provider throws, `executeSearch()` records the error and tries the next provider. There is no provider-level parallel fan-out; fallback is sequential.
|
7. If a provider throws, `executeSearch()` records the error and tries the next provider. There is no provider-level parallel fan-out; fallback is sequential.
|
||||||
8. After all candidates fail, `formatProviderError()` normalizes each error:
|
8. After all candidates fail, `formatProviderError()` normalizes each error:
|
||||||
@@ -99,8 +99,8 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
|
|||||||
- `limit` / `num_search_results`: adapter uses `params.numSearchResults ?? params.limit`, clamped to `5..20` with default `5`.
|
- `limit` / `num_search_results`: adapter uses `params.numSearchResults ?? params.limit`, clamped to `5..20` with default `5`.
|
||||||
- Output: `answer`, `sources`, `requestId`, `authMode: "api_key"`.
|
- Output: `answer`, `sources`, `requestId`, `authMode: "api_key"`.
|
||||||
- **Perplexity** — `packages/coding-agent/src/web/search/providers/perplexity.ts`
|
- **Perplexity** — `packages/coding-agent/src/web/search/providers/perplexity.ts`
|
||||||
- Availability: auth precedence is `PERPLEXITY_COOKIES` -> OAuth token in `agent.db` -> `PERPLEXITY_API_KEY` / `PPLX_API_KEY`.
|
- Availability: auth precedence is `PERPLEXITY_COOKIES` -> OAuth token in `agent.db` -> `PERPLEXITY_API_KEY` / `PPLX_API_KEY` -> anonymous ask-endpoint fallback. `isAvailable()` gates the auto chain on credentials, but `isExplicitlyAvailable()` is always true, so explicit selection works unauthenticated.
|
||||||
- OAuth/cookie mode: POSTs to `https://www.perplexity.ai/rest/sse/perplexity_ask`, consumes SSE, merges partial events, extracts answer and source URLs, sets `authMode: "oauth"`.
|
- OAuth/cookie/anonymous mode: POSTs to `https://www.perplexity.ai/rest/sse/perplexity_ask`, consumes SSE, merges partial events, extracts answer and source URLs, sets `authMode: "oauth"` (`"anonymous"` for the unauthenticated fallback).
|
||||||
- API-key mode: POSTs to `https://api.perplexity.ai/chat/completions` with `model: "sonar-pro"`, `search_mode: "web"`, `num_search_results`, optional `search_recency_filter`, `max_tokens`, `temperature`.
|
- API-key mode: POSTs to `https://api.perplexity.ai/chat/completions` with `model: "sonar-pro"`, `search_mode: "web"`, `num_search_results`, optional `search_recency_filter`, `max_tokens`, `temperature`.
|
||||||
- `num_search_results` controls upstream API breadth only in API-key mode. `limit` is preserved separately as `num_results` and slices returned `sources` after parsing in both auth modes.
|
- `num_search_results` controls upstream API breadth only in API-key mode. `limit` is preserved separately as `num_results` and slices returned `sources` after parsing in both auth modes.
|
||||||
- Output may include `answer`, `sources`, `citations`, `usage`, `model`, `requestId`, `authMode`.
|
- Output may include `answer`, `sources`, `citations`, `usage`, `model`, `requestId`, `authMode`.
|
||||||
@@ -201,7 +201,7 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
|
|||||||
- Parallel result count: default `10`, max `40`; per-result excerpt cap `10_000` chars (`packages/coding-agent/src/web/search/providers/parallel.ts`, `packages/coding-agent/src/web/parallel.ts`).
|
- Parallel result count: default `10`, max `40`; per-result excerpt cap `10_000` chars (`packages/coding-agent/src/web/search/providers/parallel.ts`, `packages/coding-agent/src/web/parallel.ts`).
|
||||||
- Kagi result count: default `10`, max `40` (`packages/coding-agent/src/web/search/providers/kagi.ts`).
|
- Kagi result count: default `10`, max `40` (`packages/coding-agent/src/web/search/providers/kagi.ts`).
|
||||||
- SearXNG result count: default `10`, max `20` (`packages/coding-agent/src/web/search/providers/searxng.ts`).
|
- SearXNG result count: default `10`, max `20` (`packages/coding-agent/src/web/search/providers/searxng.ts`).
|
||||||
- Perplexity API-key mode defaults: `max_tokens = 8192`, `temperature = 0.2`, `num_search_results = 10` (`packages/coding-agent/src/web/search/providers/perplexity.ts`).
|
- Perplexity API-key mode defaults: `max_tokens = 8192`, `temperature = 0.2`, `num_search_results = 20` (`packages/coding-agent/src/web/search/providers/perplexity.ts`).
|
||||||
- Anthropic defaults: model `claude-haiku-4-5`, `DEFAULT_MAX_TOKENS = 4096` when the provider omits `max_tokens` (`packages/coding-agent/src/web/search/providers/anthropic.ts`).
|
- Anthropic defaults: model `claude-haiku-4-5`, `DEFAULT_MAX_TOKENS = 4096` when the provider omits `max_tokens` (`packages/coding-agent/src/web/search/providers/anthropic.ts`).
|
||||||
- Gemini retries: up to `3` retries per endpoint, base delay `1000` ms, rate-limit delay budget `5 * 60 * 1000` ms (`packages/coding-agent/src/web/search/providers/gemini.ts`).
|
- Gemini retries: up to `3` retries per endpoint, base delay `1000` ms, rate-limit delay budget `5 * 60 * 1000` ms (`packages/coding-agent/src/web/search/providers/gemini.ts`).
|
||||||
|
|
||||||
@@ -222,7 +222,6 @@ Streaming: none. `WebSearchTool.execute()` forwards its `AbortSignal` into `exec
|
|||||||
- The model-facing schema does not expose `provider`, but internal callers can force one through `SearchQueryParams`.
|
- The model-facing schema does not expose `provider`, but internal callers can force one through `SearchQueryParams`.
|
||||||
- `resolveProviderChain()` lazily imports provider modules and caches singleton instances. Just asking for labels via `getSearchProviderLabel()` does not trigger those imports.
|
- `resolveProviderChain()` lazily imports provider modules and caches singleton instances. Just asking for labels via `getSearchProviderLabel()` does not trigger those imports.
|
||||||
- Most providers treat `limit` and `num_search_results` as the same number because adapters pass `params.numSearchResults ?? params.limit`. Perplexity is the only implementation that preserves both concepts.
|
- Most providers treat `limit` and `num_search_results` as the same number because adapters pass `params.numSearchResults ?? params.limit`. Perplexity is the only implementation that preserves both concepts.
|
||||||
- The prompt says `recency` is for Brave and Perplexity, but code also implements it for Tavily and SearXNG.
|
- `recency` is implemented by Brave, Perplexity, Tavily, SearXNG, and Kagi; the model-facing prompt does not name specific providers.
|
||||||
- The year rewrite in `executeSearch()` is blunt: any `2020`-`2029` substring is replaced with the current year.
|
|
||||||
- `packages/coding-agent/src/config/settings-schema.ts` uses the shared `SEARCH_PROVIDER_PREFERENCES` / `SEARCH_PROVIDER_OPTIONS` metadata, so the settings selector and setup wizard expose `auto` plus every provider in the auto chain.
|
- `packages/coding-agent/src/config/settings-schema.ts` uses the shared `SEARCH_PROVIDER_PREFERENCES` / `SEARCH_PROVIDER_OPTIONS` metadata, so the settings selector and setup wizard expose `auto` plus every provider in the auto chain.
|
||||||
- Exa uses `authStorage.getApiKey("exa")`, then `EXA_API_KEY`, then unauthenticated `https://mcp.exa.ai/mcp` fallback.
|
- Exa uses `authStorage.getApiKey("exa")`, then `EXA_API_KEY`, then unauthenticated `https://mcp.exa.ai/mcp` fallback.
|
||||||
|
|||||||
+6
-6
@@ -16,7 +16,7 @@
|
|||||||
## Inputs
|
## Inputs
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `path` | `string` | Yes | Target path. Plain file path writes a filesystem file. Writable internal URLs are delegated to their handler. `archive.ext:inner/path` writes an archive entry for `.tar`, `.tar.gz`, `.tgz`, or `.zip`. `db.sqlite:table` inserts a row. `db.sqlite:table:key` updates or deletes a row. `conflict://<id>` resolves a recorded merge conflict. |
|
| `path` | `string` | Yes | Target path. Plain file path writes a filesystem file. Writable internal URLs are delegated to their handler. `archive.ext:inner/path` writes an archive entry for `.tar`, `.tar.gz`, `.tgz`, or `.zip`. `db.sqlite:table` inserts a row. `db.sqlite:table:key` updates or deletes a row. `conflict://<id>` resolves a recorded merge conflict; `conflict://*` bulk-resolves every registered conflict. |
|
||||||
| `content` | `string` | Yes | Full replacement file content, archive entry content, internal-resource content, conflict replacement, or SQLite row payload. SQLite non-delete writes must parse as a JSON5 object. Empty or whitespace-only content deletes a SQLite row when `path` includes a row key. |
|
| `content` | `string` | Yes | Full replacement file content, archive entry content, internal-resource content, conflict replacement, or SQLite row payload. SQLite non-delete writes must parse as a JSON5 object. Empty or whitespace-only content deletes a SQLite row when `path` includes a row key. |
|
||||||
|
|
||||||
Worked examples:
|
Worked examples:
|
||||||
@@ -46,13 +46,13 @@ Single-shot result.
|
|||||||
- SQLite write: one of `Inserted row into <table>`, `Updated row '<key>' in <table>`, `No row updated ...`, `Deleted row ...`, `No row deleted ...`.
|
- SQLite write: one of `Inserted row into <table>`, `Updated row '<key>' in <table>`, `No row updated ...`, `Deleted row ...`, `No row deleted ...`.
|
||||||
- Conflict resolution: conflict-specific success text, with fresh hashline snapshot headers when applicable.
|
- Conflict resolution: conflict-specific success text, with fresh hashline snapshot headers when applicable.
|
||||||
- If hashline prefixes were copied from `read` output and stripped first, the first text block gets an extra note.
|
- If hashline prefixes were copied from `read` output and stripped first, the first text block gets an extra note.
|
||||||
- In hashline display mode, plain file writes (including ACP bridge writes) and conflict resolutions prepend a fresh `¶<relative-path>#TAG` header so the next `edit` has a current snapshot tag without an extra `read`. Bulk conflict resolutions append a `Snapshots:` block listing one header per successfully written file.
|
- In hashline display mode, plain file writes (including ACP bridge writes) and conflict resolutions prepend a fresh `[<relative-path>#TAG]` header so the next `edit` has a current snapshot tag without an extra `read`. Bulk conflict resolutions append a `Snapshots:` block listing one header per successfully written file.
|
||||||
- Plain file writes may also return `details.diagnostics` plus `details.meta.diagnostics` when LSP diagnostics-on-write is enabled, and `details.madeExecutable` when a newly written shebang file is chmodded executable.
|
- Plain file writes may also return `details.diagnostics` plus `details.meta.diagnostics` when LSP diagnostics-on-write is enabled, and `details.madeExecutable` when a newly written shebang file is chmodded executable.
|
||||||
- SQLite writes use `toolResult(...).sourcePath(...)`, so `details.meta.sourcePath` points at the database file.
|
- SQLite writes use `toolResult(...).sourcePath(...)`, so `details.meta.sourcePath` points at the database file.
|
||||||
- Archive and internal URL writes return empty `details`.
|
- Archive writes set `details.resolvedPath` to the archive's absolute path; internal URL writes return empty `details`.
|
||||||
|
|
||||||
## Flow
|
## Flow
|
||||||
1. `WriteTool.execute()` in `packages/coding-agent/src/tools/write.ts` strips pasted `¶PATH#HASH` headers and `LINE:` hashline prefixes from `content` when the session is in hashline display mode.
|
1. `WriteTool.execute()` in `packages/coding-agent/src/tools/write.ts` strips pasted `[PATH#HASH]` headers and `LINE:` hashline prefixes from `content` when the session is in hashline display mode.
|
||||||
2. If `path` is an internal URL whose handler exposes `write`, the tool delegates directly to `handler.write(...)` and returns.
|
2. If `path` is an internal URL whose handler exposes `write`, the tool delegates directly to `handler.write(...)` and returns.
|
||||||
3. `conflict://...` paths are handled next by the merge-conflict resolver. Scope reads such as `conflict://<id>/ours` are rejected as read-only; writable conflict URIs must omit the scope.
|
3. `conflict://...` paths are handled next by the merge-conflict resolver. Scope reads such as `conflict://<id>/ours` are rejected as read-only; writable conflict URIs must omit the scope.
|
||||||
4. It calls `#resolveArchiveWritePath()` next. That uses `parseArchivePathCandidates()` from `packages/coding-agent/src/tools/archive-reader.ts`, checks candidate archive files on disk, and falls back to the longest matching archive suffix even when the archive file does not exist yet.
|
4. It calls `#resolveArchiveWritePath()` next. That uses `parseArchivePathCandidates()` from `packages/coding-agent/src/tools/archive-reader.ts`, checks candidate archive files on disk, and falls back to the longest matching archive suffix even when the archive file does not exist yet.
|
||||||
@@ -80,7 +80,7 @@ Single-shot result.
|
|||||||
### Plain file path
|
### Plain file path
|
||||||
- Target is any path that does not resolve as an archive selector and does not resolve as an existing-or-new SQLite selector.
|
- Target is any path that does not resolve as an archive selector and does not resolve as an existing-or-new SQLite selector.
|
||||||
- Existing files are overwritten.
|
- Existing files are overwritten.
|
||||||
- `write.ts` does not call `fs.mkdir()` on this path; parent-directory creation is only implemented in the archive branch.
|
- `write.ts` does not call `fs.mkdir()` on this path; explicit parent-directory creation only exists in the archive branch, but `Bun.write()` itself creates missing parent directories for plain file writes.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ content: ""
|
|||||||
- Filesystem
|
- Filesystem
|
||||||
- Creates or overwrites plain files.
|
- Creates or overwrites plain files.
|
||||||
- Rewrites entire archive files when writing an archive entry.
|
- Rewrites entire archive files when writing an archive entry.
|
||||||
- Creates parent directories for archive files only.
|
- Explicitly creates parent directories (via `fs.mkdir`) for archive files only; plain file writes get parent directories from `Bun.write()`.
|
||||||
- Mutates existing SQLite databases; never creates a new SQLite DB.
|
- Mutates existing SQLite databases; never creates a new SQLite DB.
|
||||||
- Resolves conflict markers in files for `conflict://...` writes.
|
- Resolves conflict markers in files for `conflict://...` writes.
|
||||||
- May chmod a shebang file executable after a successful plain-file write.
|
- May chmod a shebang file executable after a successful plain-file write.
|
||||||
|
|||||||
+3
-2
@@ -14,7 +14,8 @@ This is an in-file leaf move, not a new session export.
|
|||||||
|
|
||||||
Primary implementation:
|
Primary implementation:
|
||||||
|
|
||||||
- `src/modes/controllers/input-controller.ts` (`/tree`, keybinding wiring, double-escape behavior)
|
- `src/slash-commands/builtin-registry.ts` (`/tree`, `/branch` command routing)
|
||||||
|
- `src/modes/controllers/input-controller.ts` (keybinding wiring, double-escape behavior)
|
||||||
- `src/modes/controllers/selector-controller.ts` (tree UI launch + summary prompt flow)
|
- `src/modes/controllers/selector-controller.ts` (tree UI launch + summary prompt flow)
|
||||||
- `src/modes/components/tree-selector.ts` (navigation, filters, search, labels, rendering)
|
- `src/modes/components/tree-selector.ts` (navigation, filters, search, labels, rendering)
|
||||||
- `src/session/agent-session.ts` (`navigateTree` leaf switching + optional summary)
|
- `src/session/agent-session.ts` (`navigateTree` leaf switching + optional summary)
|
||||||
@@ -25,7 +26,7 @@ Primary implementation:
|
|||||||
Any of the following opens the same selector:
|
Any of the following opens the same selector:
|
||||||
|
|
||||||
- `/tree`
|
- `/tree`
|
||||||
- configured keybinding action `tree`
|
- configured keybinding for the `app.session.tree` action
|
||||||
- double-escape on empty editor when `doubleEscapeAction = "tree"` (default)
|
- double-escape on empty editor when `doubleEscapeAction = "tree"` (default)
|
||||||
- `/branch` when `doubleEscapeAction = "tree"` (routes to tree selector instead of user-only branch picker)
|
- `/branch` when `doubleEscapeAction = "tree"` (routes to tree selector instead of user-only branch picker)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const { rulebookRules, alwaysApplyRules } = bucketRules(
|
|||||||
|
|
||||||
Registration is skipped when:
|
Registration is skipped when:
|
||||||
|
|
||||||
|
- TTSR is disabled (`ttsr.enabled === false`)
|
||||||
- both `rule.condition` (regex) and `rule.astCondition` (ast-grep patterns) are absent, or every regex condition fails to compile and there are no AST conditions
|
- both `rule.condition` (regex) and `rule.astCondition` (ast-grep patterns) are absent, or every regex condition fails to compile and there are no AST conditions
|
||||||
- a rule with the same `rule.name` was already registered in this manager
|
- a rule with the same `rule.name` was already registered in this manager
|
||||||
- the rule scope excludes all monitored streams
|
- the rule scope excludes all monitored streams
|
||||||
@@ -55,9 +56,9 @@ A rule may carry `astCondition`: a list of [ast-grep](https://ast-grep.github.io
|
|||||||
|
|
||||||
AST conditions only evaluate on **edit/write tool-argument streams** — they need a language, which is inferred from the file extension on the tool's path argument, and they match against the tool's reconstructed source snapshot (`matcherDigest`), not the raw wire delta. Matching is performed in memory by the native `astMatch` engine (no temp files) with Smart strictness. Streams without a usable file path (prose, thinking, path-less tool calls) skip AST conditions entirely. A rule may mix `condition` and `astCondition`; the regex paths keep working on every scope while AST paths apply only to those tool streams.
|
AST conditions only evaluate on **edit/write tool-argument streams** — they need a language, which is inferred from the file extension on the tool's path argument, and they match against the tool's reconstructed source snapshot (`matcherDigest`), not the raw wire delta. Matching is performed in memory by the native `astMatch` engine (no temp files) with Smart strictness. Streams without a usable file path (prose, thinking, path-less tool calls) skip AST conditions entirely. A rule may mix `condition` and `astCondition`; the regex paths keep working on every scope while AST paths apply only to those tool streams.
|
||||||
|
|
||||||
### Setting caveat
|
### Setting gating
|
||||||
|
|
||||||
`TtsrSettings.enabled` is loaded into the manager but is not currently checked in runtime gating. If TTSR rules exist, matching still runs.
|
`TtsrSettings.enabled` gates the manager: when `ttsr.enabled === false`, `addRule()` refuses registration and `checkDelta()`/`checkSnapshot()`/`checkAstSnapshot()`/`hasRules()`/`hasAstRules()` all return empty/false, so no matching runs.
|
||||||
|
|
||||||
## 2. Streaming monitor lifecycle
|
## 2. Streaming monitor lifecycle
|
||||||
|
|
||||||
@@ -74,11 +75,10 @@ On `turn_start`, the stream buffer is reset:
|
|||||||
When assistant updates arrive and rules exist:
|
When assistant updates arrive and rules exist:
|
||||||
|
|
||||||
- monitor `text_delta`, `thinking_delta`, and `toolcall_delta`
|
- monitor `text_delta`, `thinking_delta`, and `toolcall_delta`
|
||||||
- append delta into a source/tool scoped manager buffer
|
- for tools exposing `matcherDigest` (edit/write), replace the scoped buffer with the reconstructed source snapshot and call `checkSnapshot(snapshot, matchContext)`; otherwise append the delta into a source/tool scoped manager buffer and call `checkDelta(delta, matchContext)` (synchronous regex matching either way)
|
||||||
- call `checkDelta(delta, matchContext)` (synchronous regex matching)
|
|
||||||
- for edit/write tool streams, when `hasAstRules()` is true, `await checkAstSnapshot(snapshot, matchContext)` (asynchronous AST matching)
|
- for edit/write tool streams, when `hasAstRules()` is true, `await checkAstSnapshot(snapshot, matchContext)` (asynchronous AST matching)
|
||||||
|
|
||||||
`checkDelta()` iterates registered rules and returns all matching rules that pass scope, global path-glob, regex condition, and repeat policy checks. `checkAstSnapshot()` applies the same scope/path/repeat gates, then runs each candidate rule's `astCondition` patterns against the snapshot via the native `astMatch` engine. It is throttled per stream key: an identical consecutive snapshot (common when only non-source arguments change between deltas) is skipped without re-running the matcher. Both paths feed their matches through the same trigger-decision handler.
|
`checkDelta()`/`checkSnapshot()` iterate registered rules and return all matching rules that pass scope, global path-glob, regex condition, and repeat policy checks. `checkAstSnapshot()` applies the same scope/path/repeat gates, then runs each candidate rule's `astCondition` patterns against the snapshot via the native `astMatch` engine. It is throttled per stream key: an identical consecutive snapshot (common when only non-source arguments change between deltas) is skipped without re-running the matcher. Both paths feed their matches through the same trigger-decision handler.
|
||||||
|
|
||||||
## 3. Trigger decision and immediate abort path
|
## 3. Trigger decision and immediate abort path
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ Net effect: injected-rule suppression is persisted/restored across session reloa
|
|||||||
|
|
||||||
### Between abort and continue
|
### Between abort and continue
|
||||||
|
|
||||||
During the timer window, state can change (user interruption, mode actions, additional events). The retry call is best-effort: `agent.continue().catch(() => {})` swallows follow-up errors.
|
During the timer window, state can change (user interruption, mode actions, additional events). The retry call is best-effort: `agent.continue()` is awaited in a try/catch; on failure the error is swallowed and the TTSR resume gate is resolved.
|
||||||
|
|
||||||
## 9. Edge cases summary
|
## 9. Edge cases summary
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ Boundary rule: the TUI engine is message-agnostic. It only knows `Component.rend
|
|||||||
- `pendingMessagesContainer`
|
- `pendingMessagesContainer`
|
||||||
- `statusContainer`
|
- `statusContainer`
|
||||||
- `todoContainer`
|
- `todoContainer`
|
||||||
|
- `subagentContainer`
|
||||||
- `btwContainer`
|
- `btwContainer`
|
||||||
- `omfgContainer`
|
- `omfgContainer`
|
||||||
- `errorBannerContainer`
|
- `errorBannerContainer`
|
||||||
@@ -165,7 +166,7 @@ Status lane ownership:
|
|||||||
|
|
||||||
Loader behavior:
|
Loader behavior:
|
||||||
|
|
||||||
- `Loader` updates every 80ms via interval and requests a component-scoped render each frame (`requestComponentRender`), so idle spinner ticks repaint without re-walking the transcript.
|
- `Loader` advances its spinner every 80ms (animated message colorizers redraw at ~30fps) and requests a component-scoped render each frame (`requestComponentRender`), so idle spinner ticks repaint without re-walking the transcript.
|
||||||
- Escape handlers are temporarily overridden during auto-compaction and auto-retry to cancel those operations.
|
- Escape handlers are temporarily overridden during auto-compaction and auto-retry to cancel those operations.
|
||||||
- On end/cancel paths, controllers restore prior escape handlers and stop/clear loader components.
|
- On end/cancel paths, controllers restore prior escape handlers and stop/clear loader components.
|
||||||
|
|
||||||
@@ -215,7 +216,7 @@ Event-driven updates:
|
|||||||
Throttled/debounced paths:
|
Throttled/debounced paths:
|
||||||
|
|
||||||
- TUI rendering is tick-debounced (`requestRender` coalescing).
|
- TUI rendering is tick-debounced (`requestRender` coalescing).
|
||||||
- Loader animation is fixed-interval (80ms), each frame requesting a component-scoped render.
|
- Loader animation is interval-driven (80ms spinner advance; ~30fps when the message colorizer is animated), each frame requesting a component-scoped render.
|
||||||
- Editor autocomplete updates (inside `Editor`) use debounce timers, reducing recompute churn during typing.
|
- Editor autocomplete updates (inside `Editor`) use debounce timers, reducing recompute churn during typing.
|
||||||
|
|
||||||
The runtime therefore mixes event-driven state transitions with bounded render cadence to keep interactivity responsive without repaint storms.
|
The runtime therefore mixes event-driven state transitions with bounded render cadence to keep interactivity responsive without repaint storms.
|
||||||
|
|||||||
+5
-4
@@ -29,6 +29,7 @@ export interface Component {
|
|||||||
handleInput?(data: string): void;
|
handleInput?(data: string): void;
|
||||||
wantsKeyRelease?: boolean;
|
wantsKeyRelease?: boolean;
|
||||||
invalidate?(): void;
|
invalidate?(): void;
|
||||||
|
dispose?(): void;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -69,12 +70,12 @@ render(width: number): readonly string[] {
|
|||||||
|
|
||||||
Use `matchesKey(data, "...")` for navigation keys and combos.
|
Use `matchesKey(data, "...")` for navigation keys and combos.
|
||||||
|
|
||||||
### Respect user-configured app keybindings
|
### Match app keybinding actions
|
||||||
|
|
||||||
Extension UI factories receive a `KeybindingsManager` (interactive mode) so you can honor mapped actions instead of hardcoding keys:
|
Extension UI factories receive a `KeybindingsManager` (interactive mode; an in-memory instance carrying the default bindings, not the user's `keybindings.yml`) so you can match action ids instead of hardcoding keys:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
if (keybindings.matches(data, "interrupt")) {
|
if (keybindings.matches(data, "app.interrupt")) {
|
||||||
done(undefined);
|
done(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -213,7 +214,7 @@ class Picker implements Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
if (this.keybindings.matches(data, "interrupt")) {
|
if (this.keybindings.matches(data, "app.interrupt")) {
|
||||||
this.done(undefined);
|
this.done(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user