Propagate correlation_id/causation_id through execution events, add API version header

Execution events (started/failed/succeeded) never carried the
correlation/causation IDs from the ExecuteRequest, even though the
fields existed on Execution. Added CausationID to ExecuteRequest and
Execution, and split emitEvent into a correlated variant used
throughout the execution lifecycle. handleExecute falls back to
X-Correlation-ID/X-Causation-ID headers when the body omits them.
Also add X-Hexis-Version negotiation on /api/v1/*, matching Nexus.

Part of Vikunja #273.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
This commit is contained in:
kami
2026-07-20 11:11:49 +04:00
parent 89d8433d17
commit ed593efb71
5 changed files with 390 additions and 14 deletions
+15 -8
View File
@@ -76,6 +76,7 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
ConfirmationID: req.ConfirmationID,
Status: domain.ExecutionStarted,
CorrelationID: req.CorrelationID,
CausationID: req.CausationID,
ResolutionEvidence: req.ResolutionEvidence,
CreatedAt: now,
UpdatedAt: now,
@@ -94,7 +95,7 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
return nil, err
}
e.emitEvent(domain.EventExecutionStarted, exec.ID, map[string]any{
e.emitEventCorrelated(domain.EventExecutionStarted, exec.ID, exec.CorrelationID, exec.CausationID, map[string]any{
"capability_id": req.CapabilityID,
"target_entity_id": req.TargetEntityID,
"provider": capability.Provider,
@@ -116,7 +117,7 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
exec.Status = domain.ExecutionUnknown
exec.Error = "execution timed out after " + timeout.String()
exec.Result = map[string]any{"error": exec.Error}
e.emitEvent(domain.EventExecutionFailed, exec.ID, map[string]any{
e.emitEventCorrelated(domain.EventExecutionFailed, exec.ID, exec.CorrelationID, exec.CausationID, map[string]any{
"capability_id": req.CapabilityID,
"outcome": "unknown",
"reason": "timeout",
@@ -125,14 +126,14 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
exec.Status = domain.ExecutionFailed
exec.Error = execErr.Error()
exec.Result = map[string]any{"error": execErr.Error()}
e.emitEvent(domain.EventExecutionFailed, exec.ID, map[string]any{
e.emitEventCorrelated(domain.EventExecutionFailed, exec.ID, exec.CorrelationID, exec.CausationID, map[string]any{
"capability_id": req.CapabilityID,
"error": execErr.Error(),
})
default:
exec.Status = domain.ExecutionSucceeded
exec.Result = result
e.emitEvent(domain.EventExecutionSucceeded, exec.ID, map[string]any{
e.emitEventCorrelated(domain.EventExecutionSucceeded, exec.ID, exec.CorrelationID, exec.CausationID, map[string]any{
"capability_id": req.CapabilityID,
})
}
@@ -250,11 +251,17 @@ func (e *Engine) ValidateTarget(capabilityID, entityID string) error {
}
func (e *Engine) emitEvent(evtType domain.HexisEventType, entityID string, payload map[string]any) {
e.emitEventCorrelated(evtType, entityID, "", "", payload)
}
func (e *Engine) emitEventCorrelated(evtType domain.HexisEventType, entityID, correlationID, causationID string, payload map[string]any) {
e.store.AppendEvent(&domain.Event{
ID: domain.NewEventID(),
Type: evtType,
Timestamp: time.Now().UTC(),
Payload: payload,
ID: domain.NewEventID(),
Type: evtType,
Timestamp: time.Now().UTC(),
CorrelationID: correlationID,
CausationID: causationID,
Payload: payload,
})
}