Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c80215f
feat(telemetry): configure native OTLP exports
alexsoyes Nov 4, 2026
3be60d3
feat(telemetry): verify native export capture and split setup actions
alexsoyes Nov 4, 2026
5d87bcc
feat(cli): create a stable installation identifier
alexsoyes Nov 4, 2026
315c93f
feat(cli): normalize native telemetry observations
alexsoyes Nov 4, 2026
7f51b7e
test(cli): isolate installation identity fixtures
alexsoyes Nov 4, 2026
526e30d
feat(telemetry): sync local sessions to Gouvernail
alexsoyes Nov 4, 2026
82eae33
test(telemetry): isolate collector fixtures
alexsoyes Nov 4, 2026
b8b7e50
test(telemetry): compare initialization contents directly
alexsoyes Nov 4, 2026
ed6f7e6
test(telemetry): cover platform and command behavior
alexsoyes Nov 4, 2026
ffbe27e
docs(telemetry): explain local collector startup
alexsoyes Nov 4, 2026
f49ba8e
docs(telemetry): explain consent, data and identity
alexsoyes Nov 4, 2026
c343f82
fix(telemetry): protect sync accuracy and add session titles
alexsoyes Nov 5, 2026
bad1f0a
fix(telemetry): reject ambiguous OTLP data
alexsoyes Nov 5, 2026
0a702ed
feat(telemetry): sync verified native session projects
alexsoyes Nov 5, 2026
f7b3e10
test(telemetry): verify session metadata end to end
alexsoyes Nov 5, 2026
28b9b62
test(telemetry): register native Codex smoke test
alexsoyes Nov 5, 2026
5bd7950
test(telemetry): align sync coverage with platform support
alexsoyes Nov 5, 2026
70a4d2f
fix(ci): isolate mutation shard reports
alexsoyes Nov 5, 2026
0a15aeb
test(telemetry): verify session consent intervals
alexsoyes Nov 5, 2026
1d96237
fix(telemetry): clarify identity and consent status
alexsoyes Nov 5, 2026
7100d42
fix(telemetry): describe recorded consent closure precisely
alexsoyes Nov 5, 2026
50bc899
test(telemetry): verify recorded consent closure
alexsoyes Nov 5, 2026
629163f
test(telemetry): avoid maintenance during clone copy
alexsoyes Nov 5, 2026
7c61d81
fix(telemetry): guard consent and enrich codex metadata
alexsoyes Nov 5, 2026
a82e5de
fix(telemetry): isolate revoked sessions and verify identity provenance
alexsoyes Nov 5, 2026
d79afa7
fix(telemetry): preserve uncertain session usage
alexsoyes Nov 5, 2026
c1dcad9
test(telemetry): align report observation timeline
alexsoyes Nov 5, 2026
2c86560
Merge remote-tracking branch 'origin/next' into codex/otel-initializa…
alexsoyes Nov 5, 2026
fd35066
fix(telemetry): forward observed Claude effort
alexsoyes Nov 5, 2026
af42e1c
refactor(telemetry): remove non-OTel session metadata
alexsoyes Nov 6, 2026
c9866cd
refactor(telemetry): remove unused session comparator
alexsoyes Nov 6, 2026
1517d40
feat(telemetry): run isolated provider OTLP sessions
alexsoyes Nov 6, 2026
2057191
docs(telemetry): explain OpenCode OTLP coverage
alexsoyes Nov 6, 2026
4907069
docs(telemetry): clarify OpenCode trace correlation
alexsoyes Nov 6, 2026
643bef3
docs(telemetry): map provider OTLP coverage
alexsoyes Nov 6, 2026
ec0b2f5
feat(telemetry): export OpenCode session events as OTLP
alexsoyes Nov 6, 2026
775594d
fix(telemetry): reject special plugin files without blocking
alexsoyes Nov 6, 2026
5645331
fix(telemetry): reject conflicting OpenCode plugin entries
alexsoyes Nov 6, 2026
83b7ccf
fix(telemetry): guard all OpenCode plugin entry variants
alexsoyes Nov 6, 2026
ea5c727
docs(telemetry): clarify OpenCode token attribution
alexsoyes Nov 6, 2026
fbeddb9
feat(telemetry): observe two turns in one session
alexsoyes Nov 7, 2026
99da51f
test(telemetry): refresh command help snapshot
alexsoyes Nov 7, 2026
0c38c27
feat(telemetry): opt in to installation attribution
alexsoyes Nov 7, 2026
f189cba
docs(telemetry): include installation in local examples
alexsoyes Nov 7, 2026
f6f749d
feat(telemetry): forward Codex turn traces
alexsoyes Nov 7, 2026
691e013
docs(telemetry): explain assigned local tokens
alexsoyes Nov 7, 2026
b354b17
docs(telemetry): clarify forwarded usage and identity
alexsoyes Nov 7, 2026
e75b9cc
docs(telemetry): record Claude metric interval gap
alexsoyes Nov 7, 2026
ea429ac
feat(telemetry): forward Claude request evidence
alexsoyes Nov 7, 2026
3455147
docs(telemetry): document administrator token setup
alexsoyes Nov 7, 2026
022c3b0
docs(telemetry): clarify temporary capture retention
alexsoyes Nov 7, 2026
cc02e5e
docs(telemetry): clarify OpenCode Claude hook limits
alexsoyes Nov 7, 2026
415c7f3
docs(telemetry): remove unconfirmed provider assumption
alexsoyes Nov 7, 2026
96c6ffa
docs(telemetry): explain proxy cache ambiguity
alexsoyes Nov 7, 2026
a9c7bde
docs(telemetry): qualify proxy input counts
alexsoyes Nov 7, 2026
9bbda0a
docs(telemetry): record repository attribute coverage
alexsoyes Nov 7, 2026
8b2eb05
docs(telemetry): qualify Codex cache counters
alexsoyes Nov 7, 2026
253d204
Merge remote-tracking branch 'origin/next' into HEAD
alexsoyes Nov 8, 2026
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
54 changes: 54 additions & 0 deletions aidd_docs/recipes/observe-agent-telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Observe agent telemetry locally

Run Claude Code, Codex and OpenCode v2 with OpenTelemetry (OTel), then inspect the local Gouvernail dashboard. For OpenCode, an opted-in AIDD plugin converts public session events to OTLP; the standard exporter does not supply the session-linked usage observed by this plugin. The framework's separate local usage ledger is not the source for this view.

## Steps to observe agent telemetry locally

### 1) ⚙️ Prepare the collector destination

Start local Gouvernail and configure each tool's connector. A passkey-verified administrator generates tokens in Sources for an explicit team, member and installation UUID. Rotating a token replaces the previous token for that tool and organization. The member is the token's declared assignee, not proof of the person using it. Existing shared tokens retain their previous declarative attribution. For each tool, set `AIDD_OTEL_TOKEN` to its connector token in your shell without publishing it. The examples use the local tenant `ai-driven-dev` and loopback port `55857`; adjust them for your setup.

| Tool | Connector token |
| --- | --- |
| Claude Code | Claude Code OTLP connector |
| Codex | Codex OTLP connector |
| OpenCode v2 | OpenCode OTLP connector for the selected team |

`--yes` opts in to capturing and forwarding telemetry from that one execution. `--keep-capture` retains a private local file whose path is printed by the command, including when an execution fails after capture. The file remains in the system temporary directory and can be removed by system cleanup; copy it to a protected location if it must be retained. Raw captures can contain prompts, responses, commands, tool output and secrets: restrict access and delete them when no longer needed. Without this option the temporary capture is removed.

The new assigned tokens require `--include-installation-id`, which sends the existing AIDD installation UUID as an AIDD-added `aidd.installation.id` resource attribute for that execution. It is an installation identifier, not an authenticated person. This flag fails if no installation UUID exists. Captures remain provider data and do not contain this added attribute; the global `otel init` configuration does not add it.

### 2) 🧪 Send two messages in each session

Set `AIDD_OTEL_TOKEN` to the corresponding connector token before each command. Choose models available to your accounts. These examples opt in to sending the installation UUID.

```sh
# Claude connector token
aidd telemetry otel run --config claude --model haiku --tenant ai-driven-dev \
--endpoint http://127.0.0.1:55857 --yes --keep-capture --include-installation-id \
--follow-up "What word did I ask you to remember?" -- "Remember ORION72. Reply READY."

# Codex connector token
aidd telemetry otel run --config codex --model gpt-6-luna --tenant ai-driven-dev \
--endpoint http://127.0.0.1:55857 --yes --keep-capture --include-installation-id \
--follow-up "What word did I ask you to remember?" -- "Remember ORION72. Reply READY."

# OpenCode connector token
aidd telemetry otel run --config opencode --model claude-code/claude-haiku-4-5-20251001 \
--tenant ai-driven-dev --endpoint http://127.0.0.1:55857 --yes --keep-capture --include-installation-id \
--follow-up "What word did I ask you to remember?" -- "Remember ORION72. Reply READY."
```

Each command uses an isolated collector and resumes the first session for the follow-up. Claude and Codex persist these conversations in their usual local session history; OpenCode uses a temporary database removed after the run. This history is used only by the tools to resume; Gouvernail receives OTLP, not the tools' session files. Verify both responses when checking continuity: a shared session ID alone does not prove the model used the first message.

The command waits for an OTLP usage observation after each execution. Claude identifies the matching prompt explicitly; OpenCode supplies a completed step and message ID. Codex turn spans observed with CLI 0.166.0 carry session, turn and usage together. Turn usage can cover multiple requests or models; zero cache and output values can be provider defaults. Do not add the same turn's span and response-event counters. The run sends logs before spans and excludes spans from a different session without inferring a parent relationship. An emitted observation does not guarantee that every requested field is available. `otel init` and the global development collector are a separate configuration path; generic `otel sync` does not yet forward Codex turn spans.

### 3) ✅ Check receipt

Open the local AI KPI page and check the session IDs and observed fields. Delivery does not prove that every event was emitted or received. For OpenCode, the plugin reports the selected model and positive normalized token counts in separate events; an ambiguous zero stays unavailable. Missing or uncertain values remain unavailable. See the [field inventory](../../plugins/aidd-telemetry/OBSERVABILITY.md).

```sh
open 'http://localhost:55857/enterprise/ai-driven-dev/kpi/sessions'
```

Sources accessed 2026-11-06: [Claude monitoring](https://code.claude.com/docs/en/monitoring-usage), [Codex telemetry](https://learn.chatgpt.com/docs/config-file/config-advanced#what-gets-emitted), [OpenCode v2 CLI](https://dev.opencode.ai/v2/docs/cli) and [OTLP exporter](https://github.com/anomalyco/opencode/blob/v2.2.5/packages/util/src/observability/otlp.ts).
3 changes: 3 additions & 0 deletions cli/aidd_docs/memory/codebase-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ src/
│ │ │ ├── consent/ # a clone's identity, and when it consented
│ │ │ ├── identity/ # the identifier a person chose to be named by
│ │ │ ├── legacy/ # what the previous version wrote, by name and location, and its key and id rules
│ │ │ ├── observations/ # native telemetry fields before display or aggregation
│ │ │ ├── ports/
│ │ │ │ ├── bindings/ # the declarations kept beside the ledger, and the lock their writers share
│ │ │ │ ├── forget/
Expand All @@ -72,6 +73,8 @@ src/
│ │ │ ├── report/ # attribution of a call, the days of a report, the axes it splits by
│ │ │ └── switch/ # the previous version's config block, Claude's retention
│ │ └── infrastructure/
│ │ ├── otel/ # shared native OTLP settings; JSON templates in config/
│ │ │ └── actions/ # initialize configuration, launch export, verify receipt
│ │ ├── consent/ # reading a clone's identity from the file system, and the consent history file
│ │ ├── declaration/ # where a declaration is kept: git config and the telemetry dir
│ │ ├── forget/ # erasing the telemetry dir, the previous version's files, git config keys
Expand Down
125 changes: 125 additions & 0 deletions cli/assets/configs/otel-opencode-plugin.js.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// OpenCode server plugin v2.2.5+. The framework enables it only for an opted-in run.
// Public event stream: https://dev.opencode.ai/v2/docs/build/plugins/server (2026-11-06).
const types = ["session.created", "session.step.started", "session.step.ended", "session.step.failed"];

const text = (key, value) => ({ key, value: { stringValue: value } });
const integer = (key, value) => ({ key, value: { intValue: String(value) } });
const id = (value, prefix) =>
typeof value === "string" && new RegExp(`^${prefix}_[A-Za-z0-9]{1,64}$`).test(value);
const label = (value) =>
typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/@+#-]{0,199}$/.test(value);

function destination() {
const endpoint = process.env.AIDD_OPENCODE_OTEL_LOGS_ENDPOINT;
const authorization = /^Authorization=Bearer ([A-Za-z0-9_-]+)$/.exec(
process.env.AIDD_OPENCODE_OTEL_HEADERS ?? "",
);
if (!endpoint || !authorization) return;
try {
const url = new URL(endpoint);
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
url.username || url.password || url.search || url.hash || !url.pathname.endsWith("/v1/logs")) return;
return { url: url.href, authorization: `Bearer ${authorization[1]}` };
} catch {
return;
}
}

// Source: https://github.com/anomalyco/opencode/blob/v2.2.5/packages/schema/src/session-event.ts
// Usage's absent-to-zero normalization: https://github.com/anomalyco/opencode/blob/v2.2.5/packages/core/src/session/usage.ts (2026-11-06).
function record(event) {
const data = event?.data;
const durable = event?.durable;
if (!types.includes(event?.type) || !id(event.id, "evt") || !id(data?.sessionID, "ses") ||
!Number.isSafeInteger(event.created) || event.created <= 0 ||
!Number.isSafeInteger(durable?.seq) || durable.seq < 0 ||
!Number.isSafeInteger(durable?.version) || durable.version < 1 ||
durable.aggregateID !== data.sessionID) return;
if (event.type !== "session.created" && !id(data.assistantMessageID, "msg")) return;
const nanos = BigInt(event.created) * 1000000n;
if (nanos > 18446744073709551615n) return;
const attributes = [
text("event.name", event.type),
text("event.id", event.id),
text("session.id", data.sessionID),
integer("opencode.event.sequence", durable.seq),
integer("opencode.event.version", durable.version),
];
if (event.type === "session.created") {
if (label(data.projectID)) attributes.push(text("opencode.project.id", data.projectID));
if (id(data.parentID, "ses")) attributes.push(text("opencode.parent.session.id", data.parentID));
} else {
attributes.push(text("assistant.message.id", data.assistantMessageID));
if (event.type === "session.step.started") {
if (label(data.model?.providerID)) attributes.push(text("opencode.model.provider", data.model.providerID));
if (label(data.model?.id)) attributes.push(text("opencode.model.name", data.model.id));
if (label(data.model?.variant)) attributes.push(text("opencode.model.variant", data.model.variant));
if (label(data.agent)) attributes.push(text("opencode.agent.name", data.agent));
} else {
const counters = [
["opencode.usage.input", data.tokens?.input],
["opencode.usage.output", data.tokens?.output],
["opencode.usage.reasoning", data.tokens?.reasoning],
["opencode.usage.cache_read", data.tokens?.cache?.read],
["opencode.usage.cache_write", data.tokens?.cache?.write],
];
for (const [key, value] of counters) {
// A zero may mean the upstream provider omitted the value.
if (Number.isSafeInteger(value) && value > 0) attributes.push(integer(key, value));
}
}
}
return { timeUnixNano: String(nanos), attributes };
}

export default {
id: "aidd.otel",
setup(ctx) {
const target = destination();
if (!target) {
if (process.env.AIDD_OPENCODE_OTEL_LOGS_ENDPOINT || process.env.AIDD_OPENCODE_OTEL_HEADERS)
console.warn("AIDD OpenTelemetry: the OpenCode exporter configuration is invalid.");
return;
}
const controller = new AbortController();
const pending = new Set();
let warned = false;
const warn = () => {
if (!warned) console.warn("AIDD OpenTelemetry: an OpenCode observation could not be exported.");
warned = true;
};
const version = label(ctx.app?.version) ? ctx.app.version : undefined;
const resource = [text("service.name", "opencode")];
if (version) resource.push(text("service.version", version));
void (async () => {
try {
for await (const event of ctx.event.subscribe({ types, signal: controller.signal })) {
if (controller.signal.aborted) break;
const log = record(event);
if (!log) continue;
if (pending.size >= 64) { warn(); continue; }
const body = JSON.stringify({ resourceLogs: [{
resource: { attributes: resource },
scopeLogs: [{ scope: { name: "aidd.opencode.otel", version: "1" }, logRecords: [log] }],
}] });
const work = fetch(target.url, {
method: "POST",
headers: { "content-type": "application/json", authorization: target.authorization },
body,
redirect: "error",
signal: AbortSignal.timeout(1500),
}).then((response) => { if (!response.ok) warn(); }).catch(warn);
pending.add(work);
void work.finally(() => pending.delete(work));
}
} catch {
if (!controller.signal.aborted) warn();
}
})();
return async () => {
controller.abort();
await Promise.allSettled([...pending]);
};
},
};
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"node": ">=22.12"
},
"packageManager": "pnpm@12.16.1",
"bundleBudgetKB": 747,
"bundleBudgetKB": 839,
"scripts": {
"build": "tsup && node scripts/check-bundle-size.mjs",
"build:check-size": "node scripts/check-bundle-size.mjs",
Expand Down
21 changes: 21 additions & 0 deletions cli/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,29 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
// (+5.2 KB over its 712.9 KB skills profile); 0.3% measured headroom.
// 722 KB: 720.2 KB, Antigravity portable agents and direct-local installation
// (+2.1 KB over its 718.1 KB hooks build); 0.3% measured headroom.
// 728 KB: 726.7 KB, explicit native OTLP configuration for three tools and the
// OpenCode 1 launcher (+15.9 KB over the 710.8 KB build above); 0.2% measured headroom.
// 746 KB: 744.9 KB, provider observation transformers and a persistent installation ID
// (+18.2 KB over the 726.7 KB build above); 0.1% measured headroom.
// 784 KB: 783.7 KB, local OTLP collector and consent-aware resumable provider sync,
// explicit metric sanitization, retry and durable feature invalidation (+38.8 KB over 744.9 KB);
// 0.1% measured headroom.
// 799 KB: 798.8 KB with opt-in Codex titles, one-pass title synchronization and
// telemetry integrity guards (+15.1 KB over 783.7 KB); less than 0.1% measured headroom.
// 804 KB: 803.3 KB with strict OTLP attribute and metric validation (+4.5 KB);
// 0.1% measured headroom.
// 807 KB: 806.6 KB with opt-in native Codex project metadata and shared metadata publishing;
// +3.3 KB, less than 0.1% measured headroom.
// 808 KB: 807.49 KB with bounded, event-scoped Codex reasoning-effort forwarding;
// +0.53 KB over the preceding 806.96 KB build, less than 0.1% measured headroom.
// 810 KB: 809.79 KB with separately opted-in, sanitized Codex repository metadata.
// 813 KB: 812.66 KB with consent and session-admission checks on every delivery attempt.
// 819 KB: 818.39 KB with per-identifier provenance and isolated pending session ranges.
// 829 KB: 828.93 KB with repository uncertainty and clone-proven resume after origin changes.
// 839 KB: 838.97 KB with Claude token-point effort and merged Antigravity support.
// 747 KB: 732.4 KB, the machine-wide install of `setup --global` (+12.2 KB over the 720.2 KB
// above); 2.0% measured headroom.
// Merged with next: 823.5 KB, within the existing 839 KB budget.
const budgetKB = pkg.bundleBudgetKB ?? 500;
const budgetBytes = budgetKB * 1024;

Expand Down
Loading
Loading