Conversation
| ? resolvePromptStory(ctx, rawPrompt) | ||
| : { storyId: ctx.story?.storyId ?? '', storySource: ctx.story?.storySource ?? '' }; | ||
|
|
||
| const { AgentRegistry } = await import('../../../../../../agents/registry.js'); |
There was a problem hiding this comment.
Why do we need this dynamic import? Do we actually have problems with circular dependencies?
import AgentRegistry from '@/agents/registry.js'
There was a problem hiding this comment.
It is not really needed. Updated.
There was a problem hiding this comment.
Reverted due to CI issues.
| } catch (err) { | ||
| // OtlpAgentAdapter.prepareAnalyticsFields's "must never throw" contract is only a doc | ||
| // comment — a future/alternate adapter implementation that violates it must not abort | ||
| // every remaining record in this forward tick. | ||
| const msg = err instanceof Error ? err.message : String(err); | ||
| logger.debug( | ||
| '[otlp-forwarder] prepareAnalyticsFields threw', | ||
| ...sanitizeLogArgs({ agentName: spoolData.agentName, err: msg }) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Instead of this ugly catch, we can swallow all errors inside the prepareAnalyticsFields
There was a problem hiding this comment.
Such approach guarantees that new implementations (new plugins) will not break this execution in case of any exceptions.
| // Read the record's OWN untruncated prompt text here, before | ||
| // `limitHookPayload()` below produces its own truncated `limited` copy. | ||
| // `limitHookPayload` never mutates `hookEvent` itself (it builds a fresh | ||
| // `{ ...hookEvent }` copy), so this is still the full original string — | ||
| // used ONLY to feed the marker/mention regex tiers below; the matched | ||
| // ticket id (a short string) is all that ever reaches the output, never | ||
| // this raw text itself. | ||
| const rawPrompt = typeof hookEvent['prompt'] === 'string' ? hookEvent['prompt'] : ''; | ||
| const promptStory = | ||
| hookName === 'UserPromptSubmit' | ||
| ? resolvePromptStory(ctx, rawPrompt) | ||
| : { storyId: ctx.story?.storyId ?? '', storySource: ctx.story?.storySource ?? '' }; |
There was a problem hiding this comment.
Let's extract it into something like resolveStory(ctx, hookEvent). Like we do for the resolveStoryOnce and resolveIdentityOnce.
There was a problem hiding this comment.
hmm, I don't think I understand why do we need both resolveStoryOnce + resolvePromptStory + these snippet's logic. Everything looks related to the story resolution. Can't we combine/simplify the approach?
There was a problem hiding this comment.
I made some renamings.
The idea of this logic is to send the most actual info about story if it is overridden in another user prompt (e.g., a developer started work on story A, but in the middle of a session switched to story B and mentioned it in a prompt). This is how I understand this use case from the requirements.
| userEmail: resolveUserEmail(credentials), | ||
| git: {}, | ||
| identity: {}, | ||
| story: {}, |
There was a problem hiding this comment.
these empty objects (including git) looks sus. Can we do all
await resolveGitInfo(ctx, cwd);
await resolveIdentityOnce(ctx, cwd);
await resolveStoryOnce(ctx, cwd);
right here?
There was a problem hiding this comment.
cwd is not known here and call order of three methods is important.
| } | ||
|
|
||
| if (event.hookEventName === 'SessionEnd') { | ||
| void (async () => { |
There was a problem hiding this comment.
why do you prefer void over plain await drop? like we do in existing forwardToSpool?
There was a problem hiding this comment.
The comment might be not actual after rework. Please, double check and let me know if you have concerns.
|
|
||
| private async evaluate(rawEvent: string): Promise<ForwardDecision> { | ||
| const event = toBaseClaudeCodeHookEvent(JSON.parse(rawEvent)); | ||
| private async evaluate(rawEvent: string): Promise<ForwardDecision> { |
There was a problem hiding this comment.
the idea behind this evaluate pipeline is to make a ForwardDecision. Block the event or forward it to spool. So we have two consequences:
- I'd better stick to the following approach
if (event.hookEventName === '_EventName_') {
return await on_EventName_(event); // <- decision to forward or not + (create a synthetic event OR enhance the existing one)
}
- Use one handler per event name instead of the
event.hookEventName === 'Stop' || event.hookEventName === 'PreCompact' || ...chains.
| if (!event.sessionId) { | ||
| return { decision: 'forward', payload: [rawEvent] }; | ||
| } |
There was a problem hiding this comment.
Is this scenario possible? and why do we want to process such sessions?
Replace the mixed if/switch dispatch with one if-chain and one handler per hook event, each returning its own ForwardDecision directly. Drop the shared fallback sink, collapse the raw/parsed object duplication by extending BaseClaudeCodeHookEvent, rename the orchestrator collectors, and split client-version caching from resolution. Generated with AI Co-Authored-By: codemie-ai <codemie.ai@gmail.com>
Add docs/ARCHITECTURE-OTLP-PLUGIN.md: the agent-agnostic OtlpAgentAdapter contract, the evaluate()/ForwardDecision dispatch pattern, how to add a new hook event or a new adapter, and the current otlp-spool completeness-gating/draining behavior (with planned extensions clearly flagged as not-yet-implemented). Index it from AGENTS.md's Agent Plugins table and add a top-level OTel (OTLP) Ingestion section pointing new contributors at it. Generated with AI Co-Authored-By: codemie-ai <codemie.ai@gmail.com>
Summary
Changes
Impact
Checklist