Skip to content

Commit f0cadc0

Browse files
Bound the compaction summarizer and retry it once per failure class (#907)
1 parent 88325d6 commit f0cadc0

9 files changed

Lines changed: 490 additions & 52 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ prerequisites; Ollama installation remains outside this flow.
334334

335335
### Profiles (`src/config/profiles.ts`)
336336

337-
Profiles supply per-project or named-profile overrides for `model` and `systemPromptExtensions` (the only allowed keys; any other key is rejected on load).
337+
Profiles supply per-project or named-profile overrides for `model`, `systemPromptExtensions`, `inactivityTimeoutMs`, `totalTimeoutMs`, and `summarizerTimeoutMs` (any other key is rejected on load). `summarizerTimeoutMs` caps the compaction summary call per inference round-trip and defaults to 90 s — well under the director's `totalTimeoutMs`, because compaction runs inline on the reactor and a stalled summary call freezes the session.
338338

339339
- Project profile: `.corbits/profile.json` in the repo root — committed, credential-free.
340340
- Named profiles: `~/.corbits/profiles/<name>.json` — user-level overrides, inherited via the `profile` key or the `--profile` flag. A missing named file fails closed. A missing project `profile.json` overlay is optional.

docs/TELEMETRY.md

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,34 @@ env kill switches (see Intentional feedback below).
1111

1212
Each event carries a small set of properties:
1313

14-
| Event | When | Properties |
15-
| ------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16-
| `cli_start` | Once per used session (see First-run disclosure) | (none beyond common properties) |
17-
| `session_end` | When a TUI session finishes | `status`, `turn_count`, `duration_ms`, `session_mode`, `exit_reason` |
18-
| `$ai_generation` | Once per completed turn (may be sampled); always on turn failure | `$ai_trace_id`, `$ai_provider`, `$ai_model`, `$ai_input_tokens`, `$ai_output_tokens`, `$ai_latency`, `$ai_is_error`, `$ai_error`, `$ai_cache_read_input_tokens`, `$ai_cache_creation_input_tokens`, `$ai_reasoning_tokens`, `tool_call_count`, `tool_error_count`, `subagent_call_count` |
19-
| `$ai_span` | Opt-in only — once per top-level tool call when `CORBITS_TELEMETRY_AI_SPANS` is set | `$ai_trace_id`, `$ai_span_id`, `$ai_parent_id`, `$ai_span_name`, `$ai_is_error` |
20-
| `slash_command` | A slash command is dispatched (shared product-event path) | `command_name` |
21-
| `skill_used` | `use_skill` loads a skill that resolved | (none beyond common properties) |
22-
| `plugin_loaded` | First successful load of a plugin identity in this process | `origin` |
23-
| `subagent_start` | A `spawn_agent` dispatch begins | `agent_name` |
24-
| `subagent_end` | A `spawn_agent` dispatch finishes | `agent_name`, `status`, `duration_ms`, `model`, `turn_count`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `tool_call_count`, `tool_error_count`, `stop_reason`, `parent_trace_id` |
25-
| `permission_prompt` | An approval prompt is answered (or abandoned) | `decision`, `permission_kind` |
26-
| `compaction` | The compactor actually folds turns away | `mode`, `duration_ms`, `turns_before`, `turns_after` |
27-
| `crash` | A fatal error reaches the process-level handler | `kind`, `error_class` |
28-
| `auth_failure` | A provider rejects the stored credentials | `auth_provider` |
29-
| `survey sent` | User submits intentional feedback via `/feedback` | `$survey_id`, `$survey_response`, `$survey_questions`, `turn_trace_id` |
14+
| Event | When | Properties |
15+
| -------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16+
| `cli_start` | Once per used session (see First-run disclosure) | (none beyond common properties) |
17+
| `session_end` | When a TUI session finishes | `status`, `turn_count`, `duration_ms`, `session_mode`, `exit_reason` |
18+
| `$ai_generation` | Once per completed turn (may be sampled); always on turn failure | `$ai_trace_id`, `$ai_provider`, `$ai_model`, `$ai_input_tokens`, `$ai_output_tokens`, `$ai_latency`, `$ai_is_error`, `$ai_error`, `$ai_cache_read_input_tokens`, `$ai_cache_creation_input_tokens`, `$ai_reasoning_tokens`, `tool_call_count`, `tool_error_count`, `subagent_call_count` |
19+
| `$ai_span` | Opt-in only — once per top-level tool call when `CORBITS_TELEMETRY_AI_SPANS` is set | `$ai_trace_id`, `$ai_span_id`, `$ai_parent_id`, `$ai_span_name`, `$ai_is_error` |
20+
| `slash_command` | A slash command is dispatched (shared product-event path) | `command_name` |
21+
| `skill_used` | `use_skill` loads a skill that resolved | (none beyond common properties) |
22+
| `plugin_loaded` | First successful load of a plugin identity in this process | `origin` |
23+
| `subagent_start` | A `spawn_agent` dispatch begins | `agent_name` |
24+
| `subagent_end` | A `spawn_agent` dispatch finishes | `agent_name`, `status`, `duration_ms`, `model`, `turn_count`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `tool_call_count`, `tool_error_count`, `stop_reason`, `parent_trace_id` |
25+
| `permission_prompt` | An approval prompt is answered (or abandoned) | `decision`, `permission_kind` |
26+
| `compaction` | The compactor actually folds turns away | `mode`, `duration_ms`, `turns_before`, `turns_after` |
27+
| `summarizer_failure` | The compaction summary call fails after its retry budget is spent | `provider`, `model`, `error_kind`, `duration_ms` |
28+
| `crash` | A fatal error reaches the process-level handler | `kind`, `error_class` |
29+
| `auth_failure` | A provider rejects the stored credentials | `auth_provider` |
30+
| `survey sent` | User submits intentional feedback via `/feedback` | `$survey_id`, `$survey_response`, `$survey_questions`, `turn_trace_id` |
3031

3132
`compaction` is deliberately silent on the runs where the compactor decides
3233
there is nothing to compact — an event that also fires on no-ops makes its own
3334
duration and turn-count averages meaningless.
3435

36+
`summarizer_failure` fires once per failed summary call, not per attempt.
37+
`error_kind` is a first-party enum (`auth`, `provider`, `timeout`, `aborted`,
38+
`empty`, `failed`) — the provider's error text is never sent. `provider` and
39+
`model` are the canonical runtime ids, the same trust class as
40+
`$ai_provider`/`$ai_model`.
41+
3542
Common properties attached to every event: a random installation UUID
3643
(`distinct_id`), `session_id`, `$app_version` (PostHog's standard Version
3744
property, the running package version), `service_version` (same value, kept

src/config/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,9 @@ export interface Config {
486486
inactivityTimeoutMs?: number;
487487
// Per-call total wall-clock cap in ms (default 600_000 in the harness).
488488
totalTimeoutMs?: number;
489+
// Per-call wall-clock cap for the compaction summary call in ms
490+
// (default 90_000 in the summarizer).
491+
summarizerTimeoutMs?: number;
489492
reasoningEffort?: ReasoningEffort;
490493
mcpServers?: ResolvedMCPServerConfig[];
491494
/** Local project MCP lists replace global lists and require project trust. */
@@ -1005,6 +1008,9 @@ export async function loadConfig(
10051008
...(profile.totalTimeoutMs !== undefined
10061009
? { totalTimeoutMs: profile.totalTimeoutMs }
10071010
: {}),
1011+
...(profile.summarizerTimeoutMs !== undefined
1012+
? { summarizerTimeoutMs: profile.summarizerTimeoutMs }
1013+
: {}),
10081014
...(local?.reasoningEffort !== undefined
10091015
? { reasoningEffort: local.reasoningEffort }
10101016
: {}),

src/config/profiles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const ProfileSchema = type({
1919
// Default in the inference harness is 600_000 (10 min). Backstop for
2020
// streams that keep emitting forever without terminating.
2121
"totalTimeoutMs?": "number >= 1",
22+
// Per-call cap for the compaction summary call in milliseconds. Default
23+
// 90_000 — well under totalTimeoutMs because compaction runs inline on the
24+
// reactor and a stuck summary call freezes the session.
25+
"summarizerTimeoutMs?": "number >= 1",
2226
"+": "reject",
2327
});
2428

@@ -100,6 +104,8 @@ export async function resolveProfile(
100104
merged.inactivityTimeoutMs = projectProfile.inactivityTimeoutMs;
101105
if (projectProfile.totalTimeoutMs !== undefined)
102106
merged.totalTimeoutMs = projectProfile.totalTimeoutMs;
107+
if (projectProfile.summarizerTimeoutMs !== undefined)
108+
merged.summarizerTimeoutMs = projectProfile.summarizerTimeoutMs;
103109
}
104110

105111
const resolvedName = profileName ?? projectProfile?.profile;

src/exec/runner.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import { createModelSummarizer } from "../session/summarizer.js";
113113
import { ID_PREFIX, LOG_NAMESPACE_ROOT } from "../branding.js";
114114
import type { ReactorEmittedEvent } from "@intx/inference";
115115
import { setAgentSourceUnlessClosed } from "../tui/agent-source-sync.js";
116+
import { ensureFreshInferenceSource } from "../subagent/refresh-inference-source.js";
116117
import { getToolApprovalBudget } from "../tui/tool-execution-watchdog.js";
117118
import { WorkflowHost } from "../workflows/host.js";
118119

@@ -678,6 +679,20 @@ export async function runExec(config: Config): Promise<ExecResult> {
678679
getSource: () => liveSource,
679680
deps: inferenceDeps,
680681
getArchive: () => evidenceArchiveHolder.current,
682+
timeoutMs: config.summarizerTimeoutMs,
683+
telemetry: liveTelemetry,
684+
// A 401 here usually means the shared OAuth file rotated under another
685+
// process; re-read it so the retry runs on the fresh token.
686+
refreshAuth: async () => {
687+
const fresh = await ensureFreshInferenceSource(
688+
liveSource,
689+
config.providers,
690+
);
691+
if (fresh.apiKey === liveSource.apiKey) return;
692+
liveSource = fresh;
693+
if (currentAgent !== null)
694+
setAgentSourceUnlessClosed(currentAgent, fresh);
695+
},
681696
});
682697

683698
const { activated: activatedToolNames, computeAdvertised } =

0 commit comments

Comments
 (0)