Skip to content

Commit ae314bd

Browse files
authored
Merge pull request #14 from cloudsigma/feat/openclaw-correlation-telemetry
feat: add OpenClaw correlation telemetry
2 parents 03ba2ca + 50dc73f commit ae314bd

5 files changed

Lines changed: 95 additions & 8 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ For requests routed through the `cloudsigma` or `cloudsigma-staging` provider ID
1313
- injects `metadata.sticky_key` when absent
1414
- injects a sanitized `metadata.requester_runtime` envelope when absent
1515
- injects transport header `X-Session-Id`
16-
- captures TaaS autorouter response headers
16+
- injects correlation headers `X-OpenClaw-Session-Id`, `X-OpenClaw-Turn-Id`, `X-OpenClaw-Attempt`, and `X-OpenClaw-Agent-Id` (when available)
17+
- injects `metadata.openclaw_correlation` for request/run tracing
18+
- captures TaaS autorouter + request/trace response headers
1719
- exposes the latest route capture via gateway method `taas.autorouter.lastRoute`
1820

1921
## Startup compatibility
@@ -59,6 +61,17 @@ Example injected metadata:
5961
"local_paths": "omitted_by_default"
6062
},
6163
"redaction_policy": "no_secrets;no_raw_local_paths;no_env_values;no_git_remotes;no_status_or_diffs;no_extra_params"
64+
},
65+
"openclaw_correlation": {
66+
"schema_version": "2026-06-05",
67+
"source": "openclaw-taas-affinity",
68+
"plugin_version": "0.5.2",
69+
"session_id": "oc:0123456789abcdef",
70+
"sticky_key": "oc:0123456789abcdef",
71+
"session_source_hint": "source:1a2b3c4d5e6f7890",
72+
"agent_id": "new-agent-2",
73+
"provider": "cloudsigma",
74+
"model_id": "cloudsigma/auto"
6275
}
6376
}
6477
}

index.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const REQUESTER_RUNTIME_SCHEMA_VERSION = "2026-06-04"
2929
const REQUESTER_RUNTIME_SOURCE = "openclaw-taas-affinity"
3030
const GIT_PROBE_TIMEOUT_MS = 250
3131
const LAST_ROUTE_LIMIT = 256
32+
const PLUGIN_VERSION = "0.5.2"
3233

3334
// OpenClaw stores active registry state (including workspaceDir) on globalThis
3435
// under this well-known symbol key.
@@ -40,6 +41,10 @@ type RequesterRuntime = Record<string, unknown>
4041
type AutorouterCapture = {
4142
sessionId: string
4243
capturedAt: number
44+
taasRequestId: string | null
45+
taasTraceId: string | null
46+
openclawTurnId: string | null
47+
openclawAttempt: string | null
4348
autorouterModel: string | null
4449
autorouterAlgo: string | null
4550
autorouterAlgoSource: string | null
@@ -162,6 +167,49 @@ function deriveAgentIdForCapture(ctx: { agentDir?: string; workspaceDir?: string
162167
return seg
163168
}
164169

170+
function buildCorrelationMetadata(
171+
sessionId: string,
172+
source: string,
173+
ctx: ProviderWrapStreamFnContext,
174+
agentId: string | null
175+
): Record<string, unknown> {
176+
const ctxRecord = ctx as unknown as Record<string, unknown>
177+
const modelRecord = asRecord(ctxRecord.model)
178+
const modelId = safeString(ctxRecord.modelId) ?? safeString(modelRecord?.id)
179+
const provider = safeString(ctxRecord.provider)
180+
return {
181+
schema_version: "2026-06-05",
182+
source: REQUESTER_RUNTIME_SOURCE,
183+
plugin_version: PLUGIN_VERSION,
184+
session_id: sessionId,
185+
sticky_key: sessionId,
186+
session_source_hint: stableHash(source, "source"),
187+
...(agentId && { agent_id: agentId }),
188+
...(provider && { provider }),
189+
...(modelId && { model_id: modelId }),
190+
}
191+
}
192+
193+
function buildCorrelationHeaders(args: {
194+
sessionId: string
195+
turnId?: unknown
196+
attempt?: unknown
197+
agentId?: string | null
198+
}): Record<string, string> {
199+
const headers: Record<string, string> = {
200+
"X-Session-Id": args.sessionId,
201+
"X-OpenClaw-Session-Id": args.sessionId,
202+
"X-OpenClaw-Plugin-Version": PLUGIN_VERSION,
203+
}
204+
const turnId = safeString(args.turnId)
205+
const attempt = args.attempt === undefined || args.attempt === null ? undefined : String(args.attempt)
206+
const agentId = safeString(args.agentId)
207+
if (turnId) headers["X-OpenClaw-Turn-Id"] = turnId
208+
if (attempt) headers["X-OpenClaw-Attempt"] = attempt
209+
if (agentId) headers["X-OpenClaw-Agent-Id"] = agentId
210+
return headers
211+
}
212+
165213
function buildRequesterRuntime(
166214
ctx: ProviderWrapStreamFnContext,
167215
sessionId: string,
@@ -199,20 +247,23 @@ function buildRequesterRuntime(
199247
function patchPayloadMetadata(
200248
payload: Record<string, unknown>,
201249
sessionId: string,
202-
requesterRuntime?: RequesterRuntime
250+
requesterRuntime?: RequesterRuntime,
251+
correlation?: Record<string, unknown>
203252
): Record<string, unknown> {
204253
const existingMeta = asRecord(payload.metadata) ?? {}
205254
const needsSessionId = !existingMeta.session_id
206255
const needsStickyKey = !existingMeta.sticky_key
207256
const needsRequesterRuntime = requesterRuntime && !existingMeta.requester_runtime
208-
if (!needsSessionId && !needsStickyKey && !needsRequesterRuntime) return payload
257+
const needsCorrelation = correlation && !existingMeta.openclaw_correlation
258+
if (!needsSessionId && !needsStickyKey && !needsRequesterRuntime && !needsCorrelation) return payload
209259
return {
210260
...payload,
211261
metadata: {
212262
...existingMeta,
213263
...(needsSessionId && { session_id: sessionId }),
214264
...(needsStickyKey && { sticky_key: sessionId }),
215265
...(needsRequesterRuntime && { requester_runtime: requesterRuntime }),
266+
...(needsCorrelation && { openclaw_correlation: correlation }),
216267
},
217268
}
218269
}
@@ -258,6 +309,10 @@ function captureAutorouterFromHeaders(
258309
autorouterModel: lowered["x-taas-autorouter-model"] ?? null,
259310
autorouterAlgo: lowered["x-taas-autorouter-mode"] ?? null,
260311
autorouterAlgoSource: lowered["x-taas-autorouter-algorithm-source"] ?? null,
312+
taasRequestId: lowered["x-request-id"] ?? lowered["x-taas-request-id"] ?? null,
313+
taasTraceId: lowered["x-trace-id"] ?? lowered["x-taas-trace-id"] ?? null,
314+
openclawTurnId: lowered["x-openclaw-turn-id"] ?? null,
315+
openclawAttempt: lowered["x-openclaw-attempt"] ?? null,
261316
thinkingApplied: lowered["x-taas-thinking-applied"] ?? null,
262317
routedContextWindow:
263318
parsedContextWindow && Number.isFinite(parsedContextWindow) && parsedContextWindow > 0
@@ -294,6 +349,7 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) {
294349
const { sessionId, source } = resolveSessionId(ctx.workspaceDir)
295350
const agentIdForCapture = deriveAgentIdForCapture(ctx as { agentDir?: string; workspaceDir?: string })
296351
const requesterRuntime = buildRequesterRuntime(ctx, sessionId, source)
352+
const correlation = buildCorrelationMetadata(sessionId, source, ctx, agentIdForCapture)
297353

298354
if (isDev) console.debug(`[taas-affinity] wrapStreamFn sessionId=${sessionId} source=${source}`)
299355

@@ -304,7 +360,7 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) {
304360
const onPayload: NonNullable<typeof options>["onPayload"] = async (payload, payloadModel) => {
305361
const payloadRecord = asRecord(payload)
306362
if (!payloadRecord) return prevOnPayload ? prevOnPayload(payload, payloadModel) : payload
307-
const patched = patchPayloadMetadata(payloadRecord, sessionId, requesterRuntime)
363+
const patched = patchPayloadMetadata(payloadRecord, sessionId, requesterRuntime, correlation)
308364
return prevOnPayload ? prevOnPayload(patched, payloadModel) : patched
309365
}
310366
const prevOnResponse = options?.onResponse
@@ -323,13 +379,14 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) {
323379
function buildTransportTurnState(ctx: ProviderResolveTransportTurnStateContext): ProviderTransportTurnState | null {
324380
const activeSource = getActiveSessionSource() ?? fallbackSessionSource()
325381
const sessionId = deriveSessionId(activeSource)
382+
const agentId = deriveAgentIdForCapture(ctx as unknown as { agentDir?: string; workspaceDir?: string })
326383
if (isDev) {
327384
console.debug(
328385
`[taas-affinity] resolveTransportTurnState sessionId=${sessionId} ` +
329386
`source=${activeSource} turnId=${ctx.turnId} attempt=${ctx.attempt}`
330387
)
331388
}
332-
return { headers: { "X-Session-Id": sessionId } }
389+
return { headers: buildCorrelationHeaders({ sessionId, turnId: ctx.turnId, attempt: ctx.attempt, agentId }) }
333390
}
334391

335392
export default {
@@ -382,6 +439,8 @@ export default {
382439
patchPayloadMetadata,
383440
resolveSessionId,
384441
captureAutorouterFromHeaders,
442+
buildCorrelationHeaders,
443+
buildCorrelationMetadata,
385444
getLastRouteForAgent,
386445
getLastRouteForSession,
387446
},

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openclaw-taas-affinity",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "OpenClaw provider plugin — CloudSigma TaaS session affinity. Injects a stable X-Session-Id header per conversation so TaaS can pin the session to the same OAuth token / Bedrock region / Claude Code node, maximising prompt-cache hit rates.",
55
"type": "module",
66
"main": "dist/index.js",

test/smoke.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ assert.equal(
7070
"direction_2_gateway"
7171
)
7272
assert.equal("available_bridges" in capturedPayload.metadata.requester_runtime, false)
73+
assert.equal(capturedPayload.metadata.openclaw_correlation.schema_version, "2026-06-05")
74+
assert.equal(capturedPayload.metadata.openclaw_correlation.source, "openclaw-taas-affinity")
75+
assert.equal(capturedPayload.metadata.openclaw_correlation.plugin_version, "0.5.2")
76+
assert.equal(capturedPayload.metadata.openclaw_correlation.session_id, capturedPayload.metadata.session_id)
77+
assert.equal(capturedPayload.metadata.openclaw_correlation.sticky_key, capturedPayload.metadata.session_id)
78+
assert.equal(capturedPayload.metadata.openclaw_correlation.provider, "cloudsigma")
79+
assert.equal(capturedPayload.metadata.openclaw_correlation.model_id, "cloudsigma/test-model")
7380

7481
const transportState = provider.resolveTransportTurnState({
7582
provider: "cloudsigma",
@@ -80,6 +87,10 @@ const transportState = provider.resolveTransportTurnState({
8087
})
8188

8289
assert.match(transportState.headers["X-Session-Id"], /^oc:[a-f0-9]{16}$/)
90+
assert.equal(transportState.headers["X-OpenClaw-Session-Id"], transportState.headers["X-Session-Id"])
91+
assert.equal(transportState.headers["X-OpenClaw-Plugin-Version"], "0.5.2")
92+
assert.equal(transportState.headers["X-OpenClaw-Turn-Id"], "turn-smoke")
93+
assert.equal(transportState.headers["X-OpenClaw-Attempt"], "1")
8394

8495
console.log("smoke ok")
8596

@@ -117,6 +128,8 @@ const captureStreamFn = async (_model, _context, options = {}) => {
117128
"x-taas-autorouter-algorithm-source": "api_key_default",
118129
"x-taas-thinking-applied": "medium",
119130
"x-taas-routed-context-window": "128000",
131+
"x-request-id": "taas-req-123",
132+
"x-trace-id": "taas-trace-456",
120133
},
121134
},
122135
_model
@@ -155,6 +168,8 @@ assert.equal(respondedPayload.capture.autorouterAlgo, "best_fit")
155168
assert.equal(respondedPayload.capture.autorouterAlgoSource, "api_key_default")
156169
assert.equal(respondedPayload.capture.thinkingApplied, "medium")
157170
assert.equal(respondedPayload.capture.routedContextWindow, 128000)
171+
assert.equal(respondedPayload.capture.taasRequestId, "taas-req-123")
172+
assert.equal(respondedPayload.capture.taasTraceId, "taas-trace-456")
158173

159174
// Non-autorouted response should NOT overwrite (we explicitly drop it)
160175
await captureWrapped("model", { messages: [] }, {})

0 commit comments

Comments
 (0)