You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENTS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -229,7 +229,7 @@ step-by-step checklist.
229
229
- **Hardened dashboard request surface** (issue #397): three independent layers, none individually load-bearing. (1) `web/auth.py::OriginHostGuard`, a pure-ASGI middleware (`app.add_middleware(...)`, not `BaseHTTPMiddleware`/`@app.middleware("http")` — neither sees WebSocket scopes) validating `Host` (required, must name the bound machine) and `Origin` (only checked when present — httpx/curl/`conductor gate respond` send none, and that path must keep working with no extra setup) on every HTTP and WebSocket request. `CONDUCTOR_WEB_ALLOW_ORIGINS` (comma-separated full origins) is the dev-server escape hatch, additive only. (2) A per-run token, minted automatically (`mint_token()`) so the protected configuration is the default; `CONDUCTOR_GATE_TOKEN` overrides it (`resolve_expected_token`). Required on every mutating route (`/api/stop`, `/api/kill`, `/api/resume`, `/api/gate-respond`, `/api/guidance`) and on the `/ws` handshake — the single auth point for the socket, since an unauthenticated connection is closed (`websocket.close`, code 1008) in reply to `websocket.connect`, before `accept()`, so it can never send *any* message type. This is strictly stronger than the pre-#397 per-message check, which browsers could never satisfy at all. Read-only routes (`/api/state`, `/api/info`, `/api/logs`, `/api/gate-status`, `/api/files/*`, the whole replay app) stay unauthenticated, protected by origin/host only. (3) `Content-Type: application/json` required on every mutating route (415 otherwise), including the bodyless control POSTs. **Token discovery:** `WebDashboard.start()` writes a `0600` file (POSIX; on Windows the mode is not honoured and the file relies on the user-profile NTFS ACL instead) at `~/.conductor/runs/dashboard-<port>.token` once the port resolves (works for both `--web` and `--web-bg`, since `cli/run.py` calls `start()`/`stop()` on both the run and resume paths); `stop()` removes it. `conductor gate respond`, `conductor guide`, and `conductor stop`'s graceful-kill rung all resolve a token via the shared `resolve_cli_token(port, token)`: `--token` > `CONDUCTOR_GATE_TOKEN` > the token file. See `docs/cli-reference.md` (Environment Variables, and the Authentication sections under `conductor gate respond` / `conductor guide`) and the `web/` bullets above for the full mechanism.
230
230
- **MCP server exposure** (`mcp:` workflow block, `conductor mcp serve`, issue #432): `workflow.mcp` (`config/schema.py::McpConfig`) is a typed, `extra="forbid"` block — `expose` (default `true`; every workflow is a candidate for MCP tool exposure with no editing required, DD4), `mode` (`async`/`sync`/`auto`; the default a generated tool's omitted `_wait_seconds` resolves to), `read_only` / `destructive` (surfaced as the generated tool's `readOnlyHint`/`destructiveHint` annotations), and `estimated_minutes` (a client-side hint, must be positive). `conductor validate` reports an unknown key inside it as a schema error, not silence (FR11) — it cannot ride on the existing untyped `metadata: dict`. See `examples/mcp-serve.yaml` and `docs/mcp-server.md` (the user-facing guide: host configuration, the exposure ladder, toolsets, the run lifecycle, and a dedicated *Limits* section for DD5/DD9/DD11/DD12/R4) and `src/conductor/mcp/serve/` above for the server that reads it. **R1 — this feature's terminal run record is a scope change to `conductor status` and `conductor fleet list`, not an MCP-only addition**: every run — MCP-launched or not — now writes a completion tombstone, so both commands (and the Fleet TUI's History screen) gained a completed-runs section as a side effect, with `--live` restoring the exact pre-change scope. See the `fleet/records.py` (`TerminalRunRecord`) bullet above and the `CHANGELOG.md` entry for the full description of what changed.
231
231
232
-
- **Context compaction**: Always-on client-side context window compaction for the `claude` and `openai` providers. Compaction is triggered proactively using the reserve-based formula `trigger = window - (output_limit + buffer)` and targets a clamped 55% hysteresis ceiling. In this formula, the `output_limit` resolves to the minimum of the effective `max_tokens` sent to the API (from settings or defaults) and the model output cap (from provider-cap). The tool-output-derived `buffer` is calculated as `2 * ceil(max_chars / 4) + 15,000` tokens as a heuristic for worst-case tool results. Resolution prioritizes cascades: provider-advertised metadata (with full pagination for Anthropic and a vendor-field parser for OpenAI-compatible endpoints), public registry, and conservative fallback. The wrapper operates in a fail-open manner, disabling itself for the rest of the agent execution upon outer failure. Compaction events are emitted to the dashboard and console. A summarizing compaction step runs a nested model call that consumes a request slot from the agent's `max_agent_iterations` budget, which doesn't get refunded. The dashboard context bar relies on provider-only limits and may disagree with the compaction window.
232
+
- **Context compaction**: Always-on client-side context window compaction for the `claude` and `openai` providers. Compaction is triggered proactively using the reserve-based formula `trigger = window - (output_limit + buffer)` and targets a clamped 55% hysteresis ceiling. In this formula, the `output_limit` resolves to the minimum of the effective `max_tokens` sent to the API (from settings or defaults) and the model output cap (from provider-cap). The tool-output-derived `buffer` is calculated as `2 * ceil(max_chars / 4) + 15,000` tokens as a heuristic for worst-case tool results. Resolution prioritizes cascades: provider-advertised metadata (with full pagination for Anthropic and a vendor-field parser for OpenAI-compatible endpoints), public registry, and conservative fallback. A second, density-calibrated estimate guards the hard window against token-dense content (CJK, base64, minified data) the ~4-chars-per-token heuristic undercounts: it matches the heuristic on ordinary prose, so it never compacts a history that is merely large, and when it fires the tier chain is driven directly against that measurement rather than delegated to the inner strategy's own (heuristic) gate. Token telemetry stays on the primary scale — the density value travels as `density_tokens` and `trigger_reason` on the start event, with `degraded_estimators` / `still_over_window` on the complete event and an `agent_compaction_skipped` event when both estimators fail. The wrapper operates in a fail-open manner, disabling itself for the rest of the agent execution upon outer failure. Compaction events are emitted to the dashboard and console. A summarizing compaction step runs a nested model call that consumes a request slot from the agent's `max_agent_iterations` budget, which doesn't get refunded. The dashboard context bar relies on provider-only limits and may disagree with the compaction window.
Copy file name to clipboardExpand all lines: docs/workflow-syntax.md
+13-4Lines changed: 13 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2426,6 +2426,14 @@ Below is how these values resolve in practice for different configurations using
2426
2426
2427
2427
When the reserve (output limit plus effective tool buffer) leaves no viable headroom below the window, compaction is disabled for the agent execution rather than armed with a degenerate threshold. The `agent_compaction_config` event then carries `enabled: false` and a `disabled_reason`, so the condition is visible per run instead of surfacing as a one-shot log warning. To resolve this, lower `runtime.max_tokens` or `tool_output.max_chars`.
2428
2428
2429
+
#### Window Guard Against Token-Dense Content
2430
+
2431
+
The trigger is measured by the primary estimator: the provider's reported token usage for the history up to the most recent response, plus a ~4-characters-per-token heuristic for everything after it. That heuristic undercounts token-dense content — CJK and other non-Latin scripts, base64, hex, or minified data — by 2-4x, so a dense suffix can grow the real request past the known context window while the trigger estimate stays below the threshold.
2432
+
2433
+
A second, density-calibrated estimate guards the hard window. It matches the primary heuristic on ordinary prose, counts text with a substantial non-ASCII share at ~1 token per character, and whitespace-poor ASCII blobs at ~2 characters per token. When that estimate reaches the known window, compaction runs even if the trigger never fired, and the tier chain is driven against the density-calibrated measurement until the history fits the target. Because the two estimates agree on ordinary text, the guard never compacts a history that is merely large.
2434
+
2435
+
The start event reports which gate fired via `trigger_reason` (`"trigger"` or `"window_guard"`) and carries the density-calibrated value separately as `density_tokens`; `tokens_before` always stays the primary token estimate.
2436
+
2429
2437
### Compaction Tiers
2430
2438
2431
2439
Conductor uses three sequential tiers to compress the history down to the target:
@@ -2463,12 +2471,13 @@ All tokens consumed by summarizing compaction are added to the workflow's total
2463
2471
2464
2472
### Observability and Events
2465
2473
2466
-
Compaction operates in a fail-open manner. If an error occurs during compaction, Conductor logs a warning, disables compaction for the rest of that agent's execution, and continues with the uncompacted history.
2474
+
Compaction operates in a fail-open manner. If an error occurs during compaction, Conductor logs a warning, disables compaction for the rest of that agent's execution, and continues with the uncompacted history. A failed context measurement never disables anything: the primary estimate falls back to an independent density-calibrated one, and only when both fail is compaction skipped for that request alone, reported as `agent_compaction_skipped` with `reason: "estimate_unavailable"`.
2467
2475
2468
-
Conductor emits three event types to track compaction:
2476
+
Conductor emits four event types to track compaction:
2469
2477
* `agent_compaction_config`: Emitted once at the start of agent execution to log resolved window and limit values.
2470
-
* `agent_compaction_start`: Emitted when context size exceeds the trigger threshold and compaction begins.
2471
-
* `agent_compaction_complete`: Emitted when compaction completes, detailing token savings or errors.
2478
+
* `agent_compaction_start`: Emitted when compaction begins. `trigger_reason` names the gate that fired (`"trigger"` or `"window_guard"`), and `density_tokens` carries the density-calibrated estimate alongside the primary-scale `tokens_before`.
2479
+
* `agent_compaction_complete`: Emitted when compaction completes, detailing token savings or errors. Degraded outcomes are named rather than hidden: `degraded_tiers` for recovered tier failures, `degraded_estimators` for lost measurements, `still_over_trigger` when the history remains above the trigger, and `still_over_window` when a window-guard compaction could not get back below the known window.
2480
+
* `agent_compaction_skipped`: Emitted when compaction did not run because the context size could not be measured at all.
0 commit comments