feat(telemetry): label subagent metrics with root session - #113
feat(telemetry): label subagent metrics with root session#113sigkillsdottir wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds session hierarchy tracking so subagent telemetry can be labeled with a root session identifier, enabling query-time aggregation across a root session and its spawned sub-sessions.
Changes:
- Introduces
sessionMetadataand helpers (sessionAttrs,metricAttrs,setSessionMetadata) to propagateroot.session.id/parent.*attributes into metrics, logs, and spans. - Updates message + session handlers to apply the new hierarchy-aware attributes and adds subtask de-duplication via
seenSubtasks. - Expands test coverage for hierarchy propagation and subtask de-duplication behavior, and updates README metric labeling guidance.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/helpers.ts | Extends test HandlerContext with sessionMetadata and seenSubtasks. |
| tests/handlers/session.test.ts | Adds tests for agentType derivation and root session propagation through session lifecycle. |
| tests/handlers/message.test.ts | Adds tests for subtask attribution/dedupe and subagent root attribution in token/cost metrics. |
| src/util.ts | Adds hierarchy-aware attribute builders and setSessionMetadata. |
| src/types.ts | Introduces SessionMetadata and new HandlerContext fields. |
| src/index.ts | Initializes new maps and adds prepareSessionForMessage to preserve hierarchy/agent metadata across message rebuilds. |
| src/handlers/session.ts | Stores hierarchy metadata on session.created and applies hierarchy-aware metric/log/span attributes. |
| src/handlers/message.ts | Adds hierarchy-aware metric/log/span attributes and subtask update de-duplication. |
| README.md | Documents how root.session.id should be used for aggregating session-scoped metrics. |
| const key = subtask.id | ||
| ? `${subtask.sessionID}:${subtask.id}` | ||
| : `${subtask.sessionID}:${subtask.messageID}:${subtask.agent}:${subtask.description}:${subtask.prompt}` | ||
| if (ctx.seenSubtasks.has(key)) return |
| | `opencode.model.usage` | Counter | Messages per model and provider | | ||
| | `opencode.retry.count` | Counter | API retries observed via `session.status` events | | ||
|
|
||
| All session-scoped metrics include `session.id`. Sessions created by subagents also include `root.session.id`, which identifies the top-level session, plus `is_subagent=true`. Costs and tokens remain attributed to their emitting `session.id`; aggregate by `root.session.id` in your metrics backend to include subagent usage in a root-session total. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/util.ts:166
setBoundedMapdoes not refresh insertion order when updating an existing key, so long-lived sessions can have theirsessionMetadataevicted onceMAX_PENDINGis exceeded even if they're still active. That would silently droproot.session.id/parent attribution from subsequent metrics/logs. Consider refreshing the entry before re-inserting so eviction behaves closer to LRU for session hierarchy metadata.
/** Stores session hierarchy identity, evicting the oldest entry first when the map is at capacity. */
export function setSessionMetadata(sessionID: string, value: SessionMetadata, ctx: HandlerContext) {
setBoundedMap(ctx.sessionMetadata, sessionID, value)
}
src/index.ts:380
prepareSessionForMessagecurrently setsrootSessionIDtosessionIDeven when no hierarchy metadata exists yet. If the session later turns out to be a subagent (oncesession.createdarrives withparentID), any metrics emitted in-between would be labeled with an incorrectroot.session.id(the child session), fragmenting aggregation. LeavingrootSessionIDunset until it is known (or previously recorded) aligns withmetricAttrs's behavior of only addingroot.session.idwhen the chain is known.
setSessionMetadata(sessionID, {
sessionID,
parentSessionID: priorMetadata?.parentSessionID,
parentMessageID: priorMetadata?.parentMessageID,
rootSessionID: priorMetadata?.rootSessionID ?? sessionID,
agentType,
}, ctx)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/handlers/message.ts:256
ctx.seenSubtasksis bounded by insertion order, but repeated subtask updates currently short-circuit onhas(key)without refreshing recency. That means a frequently-updated subtask can still be evicted as “old”, and later duplicates will be counted again, inflatingsubtask.count. Refresh the key (delete + reinsert) before returning so active subtasks stay protected from bounded eviction.
if (ctx.seenSubtasks.has(key)) return
setBoundedMap(ctx.seenSubtasks, key, Date.now())
const { agentName, agentType } = getSessionAgentMeta(subtask.sessionID, ctx)
src/index.ts:233
- Session spans are now created with
agentAttrs(...), which includes bothagentandagent.name. This update path only setsAGENT_NAME/agent.type, leaving theagentattribute stuck at its initial value (often"unknown") and making span agent labeling inconsistent. Set all agent attributes together viaagentAttrswhen refreshing the span.
const { agentType } = getSessionAgentMeta(input.sessionID, ctx)
const sessionSpan = sessionSpans.get(input.sessionID)
if (sessionSpan) sessionSpan.setAttributes({ [AGENT_NAME]: agent, "agent.type": agentType })
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/index.ts:365
prepareSessionForMessagederivesagentTypefrompriorMetadata.agentType, which can be"unknown"perSessionMetadata. That value then gets written intosessionTotals(typed asSessionAgentType), which should stay limited to"primary" | "subagent".
Consider mapping unknown → primary when populating sessionTotals to keep types and downstream metric labels consistent.
const agentType = existingTotals?.agentType ?? priorMetadata?.agentType ?? "primary"
README.md:53
- The metrics table doesn’t list
opencode.subtask.count, but the new paragraph below referencessubtask.count. Add the missing metric entry so the docs remain self-consistent.
`session.count`, `token.usage`, `cost.usage`, `cache.count`, `message.count`, `model.usage`, `subtask.count`, `session.duration`, `session.token.total`, and `session.cost.total` include hierarchy attributes when session metadata is available. Root sessions then have `root.session.id` equal to `session.id`; subagent metrics use their top-level session ID and include `is_subagent=true`. Cost and token measurements remain attributed to their emitting `session.id`; aggregate `token.usage` and `cost.usage` by `root.session.id` in your metrics backend to include subagent usage in root-session totals.
Description
Labels subagent telemetry with root session IDs, enabling backend query-time aggregation.
Type of change
Checklist
bun run lintpasses with no errorsbun run check:jsdoc-coveragepasses with no errorsbun run typecheckpasses with no errorsbun testpasses with no errorsRelated issues
Additional context