Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ prerequisites; Ollama installation remains outside this flow.

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

Profiles supply per-project or named-profile overrides for `model` and `systemPromptExtensions` (the only allowed keys; any other key is rejected on load).
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.

- Project profile: `.corbits/profile.json` in the repo root — committed, credential-free.
- 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.
Expand Down
39 changes: 23 additions & 16 deletions docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,34 @@ env kill switches (see Intentional feedback below).

Each event carries a small set of properties:

| Event | When | Properties |
| ------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_start` | Once per used session (see First-run disclosure) | (none beyond common properties) |
| `session_end` | When a TUI session finishes | `status`, `turn_count`, `duration_ms`, `session_mode`, `exit_reason` |
| `$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` |
| `$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` |
| `slash_command` | A slash command is dispatched (shared product-event path) | `command_name` |
| `skill_used` | `use_skill` loads a skill that resolved | (none beyond common properties) |
| `plugin_loaded` | First successful load of a plugin identity in this process | `origin` |
| `subagent_start` | A `spawn_agent` dispatch begins | `agent_name` |
| `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` |
| `permission_prompt` | An approval prompt is answered (or abandoned) | `decision`, `permission_kind` |
| `compaction` | The compactor actually folds turns away | `mode`, `duration_ms`, `turns_before`, `turns_after` |
| `crash` | A fatal error reaches the process-level handler | `kind`, `error_class` |
| `auth_failure` | A provider rejects the stored credentials | `auth_provider` |
| `survey sent` | User submits intentional feedback via `/feedback` | `$survey_id`, `$survey_response`, `$survey_questions`, `turn_trace_id` |
| Event | When | Properties |
| -------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cli_start` | Once per used session (see First-run disclosure) | (none beyond common properties) |
| `session_end` | When a TUI session finishes | `status`, `turn_count`, `duration_ms`, `session_mode`, `exit_reason` |
| `$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` |
| `$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` |
| `slash_command` | A slash command is dispatched (shared product-event path) | `command_name` |
| `skill_used` | `use_skill` loads a skill that resolved | (none beyond common properties) |
| `plugin_loaded` | First successful load of a plugin identity in this process | `origin` |
| `subagent_start` | A `spawn_agent` dispatch begins | `agent_name` |
| `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` |
| `permission_prompt` | An approval prompt is answered (or abandoned) | `decision`, `permission_kind` |
| `compaction` | The compactor actually folds turns away | `mode`, `duration_ms`, `turns_before`, `turns_after` |
| `summarizer_failure` | The compaction summary call fails after its retry budget is spent | `provider`, `model`, `error_kind`, `duration_ms` |
| `crash` | A fatal error reaches the process-level handler | `kind`, `error_class` |
| `auth_failure` | A provider rejects the stored credentials | `auth_provider` |
| `survey sent` | User submits intentional feedback via `/feedback` | `$survey_id`, `$survey_response`, `$survey_questions`, `turn_trace_id` |

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

`summarizer_failure` fires once per failed summary call, not per attempt.
`error_kind` is a first-party enum (`auth`, `provider`, `timeout`, `aborted`,
`empty`, `failed`) — the provider's error text is never sent. `provider` and
`model` are the canonical runtime ids, the same trust class as
`$ai_provider`/`$ai_model`.

Common properties attached to every event: a random installation UUID
(`distinct_id`), `session_id`, `$app_version` (PostHog's standard Version
property, the running package version), `service_version` (same value, kept
Expand Down
6 changes: 6 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ export interface Config {
inactivityTimeoutMs?: number;
// Per-call total wall-clock cap in ms (default 600_000 in the harness).
totalTimeoutMs?: number;
// Per-call wall-clock cap for the compaction summary call in ms
// (default 90_000 in the summarizer).
summarizerTimeoutMs?: number;
reasoningEffort?: ReasoningEffort;
mcpServers?: ResolvedMCPServerConfig[];
/** Local project MCP lists replace global lists and require project trust. */
Expand Down Expand Up @@ -1005,6 +1008,9 @@ export async function loadConfig(
...(profile.totalTimeoutMs !== undefined
? { totalTimeoutMs: profile.totalTimeoutMs }
: {}),
...(profile.summarizerTimeoutMs !== undefined
? { summarizerTimeoutMs: profile.summarizerTimeoutMs }
: {}),
...(local?.reasoningEffort !== undefined
? { reasoningEffort: local.reasoningEffort }
: {}),
Expand Down
6 changes: 6 additions & 0 deletions src/config/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const ProfileSchema = type({
// Default in the inference harness is 600_000 (10 min). Backstop for
// streams that keep emitting forever without terminating.
"totalTimeoutMs?": "number >= 1",
// Per-call cap for the compaction summary call in milliseconds. Default
// 90_000 — well under totalTimeoutMs because compaction runs inline on the
// reactor and a stuck summary call freezes the session.
"summarizerTimeoutMs?": "number >= 1",
"+": "reject",
});

Expand Down Expand Up @@ -100,6 +104,8 @@ export async function resolveProfile(
merged.inactivityTimeoutMs = projectProfile.inactivityTimeoutMs;
if (projectProfile.totalTimeoutMs !== undefined)
merged.totalTimeoutMs = projectProfile.totalTimeoutMs;
if (projectProfile.summarizerTimeoutMs !== undefined)
merged.summarizerTimeoutMs = projectProfile.summarizerTimeoutMs;
}

const resolvedName = profileName ?? projectProfile?.profile;
Expand Down
15 changes: 15 additions & 0 deletions src/exec/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import { createModelSummarizer } from "../session/summarizer.js";
import { ID_PREFIX, LOG_NAMESPACE_ROOT } from "../branding.js";
import type { ReactorEmittedEvent } from "@intx/inference";
import { setAgentSourceUnlessClosed } from "../tui/agent-source-sync.js";
import { ensureFreshInferenceSource } from "../subagent/refresh-inference-source.js";
import { getToolApprovalBudget } from "../tui/tool-execution-watchdog.js";
import { WorkflowHost } from "../workflows/host.js";

Expand Down Expand Up @@ -678,6 +679,20 @@ export async function runExec(config: Config): Promise<ExecResult> {
getSource: () => liveSource,
deps: inferenceDeps,
getArchive: () => evidenceArchiveHolder.current,
timeoutMs: config.summarizerTimeoutMs,
telemetry: liveTelemetry,
// A 401 here usually means the shared OAuth file rotated under another
// process; re-read it so the retry runs on the fresh token.
refreshAuth: async () => {
const fresh = await ensureFreshInferenceSource(
liveSource,
config.providers,
);
if (fresh.apiKey === liveSource.apiKey) return;
liveSource = fresh;
if (currentAgent !== null)
setAgentSourceUnlessClosed(currentAgent, fresh);
},
});

const { activated: activatedToolNames, computeAdvertised } =
Expand Down
Loading
Loading