From 1e7141aeaedc29d0bb79555e2799434e57c9c020 Mon Sep 17 00:00:00 2001 From: Joao Guerreiro Date: Mon, 11 May 2026 16:07:17 +0100 Subject: [PATCH] chore(otel): attribute operand identifiers to opencode spans Adds semantic attributes (plugin.hook, file.path, session.id, message.id, part.id, provider.id, model.id, snapshot.hash, ...) to the Effect.withSpan and Effect.fn sites in FileSystem, Plugin, Auth, Provider, Session, SessionSummary, SessionCompaction, SessionProcessor, Snapshot, Truncate, and Instruction so operators can filter and aggregate by operand instead of just span name. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/core/src/filesystem.ts | 3 +++ packages/opencode/src/auth/index.ts | 2 ++ packages/opencode/src/plugin/index.ts | 1 + packages/opencode/src/provider/provider.ts | 8 +++++--- packages/opencode/src/session/compaction.ts | 1 + packages/opencode/src/session/instruction.ts | 1 + packages/opencode/src/session/processor.ts | 8 ++++++++ packages/opencode/src/session/session.ts | 19 +++++++++++++++++-- packages/opencode/src/session/summary.ts | 2 ++ packages/opencode/src/snapshot/index.ts | 4 +++- packages/opencode/src/tool/truncate.ts | 4 ++++ 11 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 8a1cc3a08fc3..9d68f3ff5b65 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -80,6 +80,7 @@ export namespace AppFileSystem { }) const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) { + yield* Effect.annotateCurrentSpan({ "file.path": path }) const text = yield* fs.readFileString(path) return JSON.parse(text) }) @@ -99,6 +100,7 @@ export namespace AppFileSystem { content: string | Uint8Array, mode?: number, ) { + yield* Effect.annotateCurrentSpan({ "file.path": path }) const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content) yield* write.pipe( @@ -122,6 +124,7 @@ export namespace AppFileSystem { }) const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) { + yield* Effect.annotateCurrentSpan({ "file.target": target, "file.start": start }) const result: string[] = [] let current = start while (true) { diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index f7c6319357a3..c5dee878f1c1 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -57,6 +57,7 @@ export const layer = Layer.effect( const decode = Schema.decodeUnknownOption(Info) const all = Effect.fn("Auth.all")(function* () { + yield* Effect.annotateCurrentSpan({ "auth.file": file }) if (process.env.OPENCODE_AUTH_CONTENT) { try { return JSON.parse(process.env.OPENCODE_AUTH_CONTENT) @@ -68,6 +69,7 @@ export const layer = Layer.effect( }) const get = Effect.fn("Auth.get")(function* (providerID: string) { + yield* Effect.annotateCurrentSpan({ "provider.id": providerID }) return (yield* all())[providerID] }) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 7a7f260df897..127de3595f9b 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -260,6 +260,7 @@ export const layer = Layer.effect( Input = Parameters[Name]>[0], Output = Parameters[Name]>[1], >(name: Name, input: Input, output: Output) { + yield* Effect.annotateCurrentSpan({ "plugin.hook": name }) if (!name) return output const s = yield* InstanceState.get(state) for (const hook of s.hooks) { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index c27b69b6a208..319eb6a58328 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1564,9 +1564,10 @@ const layer: Layer.Layer< } } - const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) => - InstanceState.use(state, (s) => s.providers[providerID]), - ) + const getProvider = Effect.fn("Provider.getProvider")(function* (providerID: ProviderID) { + yield* Effect.annotateCurrentSpan({ "provider.id": providerID }) + return yield* InstanceState.use(state, (s) => s.providers[providerID]) + }) const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) { const s = yield* InstanceState.get(state) @@ -1587,6 +1588,7 @@ const layer: Layer.Layer< }) const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) { + yield* Effect.annotateCurrentSpan({ "provider.id": model.providerID, "model.id": model.id }) const s = yield* InstanceState.get(state) const envs = yield* env.all() const key = `${model.providerID}/${model.id}` diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3ca4f074f9e4..7c6ae50c8feb 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -319,6 +319,7 @@ export const layer: Layer.Layer< // goes backwards through parts until there are PRUNE_PROTECT tokens worth of tool // calls, then erases output of older tool calls to free context space const prune = Effect.fn("SessionCompaction.prune")(function* (input: { sessionID: SessionID }) { + yield* Effect.annotateCurrentSpan({ "session.id": input.sessionID }) const cfg = yield* config.get() if (!cfg.compaction?.prune) return log.info("pruning") diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 6629ce67bc9f..dd706ba9eac0 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -99,6 +99,7 @@ export const layer: Layer.Layer< }) const clear = Effect.fn("Instruction.clear")(function* (messageID: MessageID) { + yield* Effect.annotateCurrentSpan({ "message.id": messageID }) const s = yield* InstanceState.get(state) s.claims.delete(messageID) }) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 579c4cc42c54..7d2f1593846d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -636,6 +636,10 @@ export const layer: Layer.Layer< }) const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () { + yield* Effect.annotateCurrentSpan({ + "session.id": ctx.sessionID, + "message.id": ctx.assistantMessage.id, + }) if (ctx.snapshot) { const patch = yield* snapshot.patch(ctx.snapshot) if (patch.files.length) { @@ -725,6 +729,10 @@ export const layer: Layer.Layer< }) const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { + yield* Effect.annotateCurrentSpan({ + "session.id": ctx.sessionID, + "message.id": ctx.assistantMessage.id, + }) slog.info("process") ctx.needsCompaction = false ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index f50f8750b32b..62c072fefc07 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -583,7 +583,11 @@ export const layer: Layer.Layer(part: T): Effect.Effect => Effect.gen(function* () { @@ -593,7 +597,15 @@ export const layer: Layer.Layer @@ -719,6 +731,7 @@ export const layer: Layer.Layer boolean, ) { + yield* Effect.annotateCurrentSpan({ "session.id": sessionID }) for (const item of MessageV2.stream(sessionID)) { if (predicate(item)) return Option.some(item) } diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index e39bd85e9ac9..09308b386bf2 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -81,6 +81,7 @@ export const layer = Layer.effect( const bus = yield* Bus.Service const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: MessageV2.WithParts[] }) { + yield* Effect.annotateCurrentSpan({ "messages.count": input.messages.length }) let from: string | undefined let to: string | undefined for (const item of input.messages) { @@ -104,6 +105,7 @@ export const layer = Layer.effect( sessionID: SessionID messageID: MessageID }) { + yield* Effect.annotateCurrentSpan({ "session.id": input.sessionID, "message.id": input.messageID }) const all = yield* sessions.messages({ sessionID: input.sessionID }) if (!all.length) return diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 848a067c3d11..4e01c051695b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -750,7 +750,9 @@ export const layer: Layer.Layer< return yield* InstanceState.useEffect(state, (s) => s.cleanup()) }), track: Effect.fn("Snapshot.track")(function* () { - return yield* InstanceState.useEffect(state, (s) => s.track()) + const hash = yield* InstanceState.useEffect(state, (s) => s.track()) + if (hash) yield* Effect.annotateCurrentSpan({ "snapshot.hash": hash }) + return hash }), patch: Effect.fn("Snapshot.patch")(function* (hash: string) { return yield* InstanceState.useEffect(state, (s) => s.patch(hash)) diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index ffc16c0b9f99..2b1d5a207cd7 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -53,6 +53,7 @@ export const layer = Layer.effect( const fs = yield* AppFileSystem.Service const cleanup = Effect.fn("Truncate.cleanup")(function* () { + yield* Effect.annotateCurrentSpan({ "truncate.dir": TRUNCATION_DIR }) const cutoff = Identifier.timestamp( Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)), ) @@ -60,10 +61,13 @@ export const layer = Layer.effect( Effect.map((all) => all.filter((name) => name.startsWith("tool_"))), Effect.catch(() => Effect.succeed([])), ) + let removed = 0 for (const entry of entries) { if (Identifier.timestamp(entry) >= cutoff) continue yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void)) + removed++ } + yield* Effect.annotateCurrentSpan({ "truncate.scanned": entries.length, "truncate.removed": removed }) }) const write = Effect.fn("Truncate.write")(function* (text: string) {