diff --git a/README.md b/README.md index 8ae4db2..5a662f2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ For requests routed through the `cloudsigma` or `cloudsigma-staging` provider ID - injects `metadata.sticky_key` when absent - injects a sanitized `metadata.requester_runtime` envelope when absent - injects transport header `X-Session-Id` -- captures TaaS autorouter response headers +- injects correlation headers `X-OpenClaw-Session-Id`, `X-OpenClaw-Turn-Id`, `X-OpenClaw-Attempt`, and `X-OpenClaw-Agent-Id` (when available) +- injects `metadata.openclaw_correlation` for request/run tracing +- captures TaaS autorouter + request/trace response headers - exposes the latest route capture via gateway method `taas.autorouter.lastRoute` ## Startup compatibility @@ -59,6 +61,17 @@ Example injected metadata: "local_paths": "omitted_by_default" }, "redaction_policy": "no_secrets;no_raw_local_paths;no_env_values;no_git_remotes;no_status_or_diffs;no_extra_params" + }, + "openclaw_correlation": { + "schema_version": "2026-06-05", + "source": "openclaw-taas-affinity", + "plugin_version": "0.5.2", + "session_id": "oc:0123456789abcdef", + "sticky_key": "oc:0123456789abcdef", + "session_source_hint": "source:1a2b3c4d5e6f7890", + "agent_id": "new-agent-2", + "provider": "cloudsigma", + "model_id": "cloudsigma/auto" } } } diff --git a/index.ts b/index.ts index 963b221..378c05e 100644 --- a/index.ts +++ b/index.ts @@ -29,6 +29,7 @@ const REQUESTER_RUNTIME_SCHEMA_VERSION = "2026-06-04" const REQUESTER_RUNTIME_SOURCE = "openclaw-taas-affinity" const GIT_PROBE_TIMEOUT_MS = 250 const LAST_ROUTE_LIMIT = 256 +const PLUGIN_VERSION = "0.5.2" // OpenClaw stores active registry state (including workspaceDir) on globalThis // under this well-known symbol key. @@ -40,6 +41,10 @@ type RequesterRuntime = Record type AutorouterCapture = { sessionId: string capturedAt: number + taasRequestId: string | null + taasTraceId: string | null + openclawTurnId: string | null + openclawAttempt: string | null autorouterModel: string | null autorouterAlgo: string | null autorouterAlgoSource: string | null @@ -162,6 +167,49 @@ function deriveAgentIdForCapture(ctx: { agentDir?: string; workspaceDir?: string return seg } +function buildCorrelationMetadata( + sessionId: string, + source: string, + ctx: ProviderWrapStreamFnContext, + agentId: string | null +): Record { + const ctxRecord = ctx as unknown as Record + const modelRecord = asRecord(ctxRecord.model) + const modelId = safeString(ctxRecord.modelId) ?? safeString(modelRecord?.id) + const provider = safeString(ctxRecord.provider) + return { + schema_version: "2026-06-05", + source: REQUESTER_RUNTIME_SOURCE, + plugin_version: PLUGIN_VERSION, + session_id: sessionId, + sticky_key: sessionId, + session_source_hint: stableHash(source, "source"), + ...(agentId && { agent_id: agentId }), + ...(provider && { provider }), + ...(modelId && { model_id: modelId }), + } +} + +function buildCorrelationHeaders(args: { + sessionId: string + turnId?: unknown + attempt?: unknown + agentId?: string | null +}): Record { + const headers: Record = { + "X-Session-Id": args.sessionId, + "X-OpenClaw-Session-Id": args.sessionId, + "X-OpenClaw-Plugin-Version": PLUGIN_VERSION, + } + const turnId = safeString(args.turnId) + const attempt = args.attempt === undefined || args.attempt === null ? undefined : String(args.attempt) + const agentId = safeString(args.agentId) + if (turnId) headers["X-OpenClaw-Turn-Id"] = turnId + if (attempt) headers["X-OpenClaw-Attempt"] = attempt + if (agentId) headers["X-OpenClaw-Agent-Id"] = agentId + return headers +} + function buildRequesterRuntime( ctx: ProviderWrapStreamFnContext, sessionId: string, @@ -199,13 +247,15 @@ function buildRequesterRuntime( function patchPayloadMetadata( payload: Record, sessionId: string, - requesterRuntime?: RequesterRuntime + requesterRuntime?: RequesterRuntime, + correlation?: Record ): Record { const existingMeta = asRecord(payload.metadata) ?? {} const needsSessionId = !existingMeta.session_id const needsStickyKey = !existingMeta.sticky_key const needsRequesterRuntime = requesterRuntime && !existingMeta.requester_runtime - if (!needsSessionId && !needsStickyKey && !needsRequesterRuntime) return payload + const needsCorrelation = correlation && !existingMeta.openclaw_correlation + if (!needsSessionId && !needsStickyKey && !needsRequesterRuntime && !needsCorrelation) return payload return { ...payload, metadata: { @@ -213,6 +263,7 @@ function patchPayloadMetadata( ...(needsSessionId && { session_id: sessionId }), ...(needsStickyKey && { sticky_key: sessionId }), ...(needsRequesterRuntime && { requester_runtime: requesterRuntime }), + ...(needsCorrelation && { openclaw_correlation: correlation }), }, } } @@ -258,6 +309,10 @@ function captureAutorouterFromHeaders( autorouterModel: lowered["x-taas-autorouter-model"] ?? null, autorouterAlgo: lowered["x-taas-autorouter-mode"] ?? null, autorouterAlgoSource: lowered["x-taas-autorouter-algorithm-source"] ?? null, + taasRequestId: lowered["x-request-id"] ?? lowered["x-taas-request-id"] ?? null, + taasTraceId: lowered["x-trace-id"] ?? lowered["x-taas-trace-id"] ?? null, + openclawTurnId: lowered["x-openclaw-turn-id"] ?? null, + openclawAttempt: lowered["x-openclaw-attempt"] ?? null, thinkingApplied: lowered["x-taas-thinking-applied"] ?? null, routedContextWindow: parsedContextWindow && Number.isFinite(parsedContextWindow) && parsedContextWindow > 0 @@ -294,6 +349,7 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) { const { sessionId, source } = resolveSessionId(ctx.workspaceDir) const agentIdForCapture = deriveAgentIdForCapture(ctx as { agentDir?: string; workspaceDir?: string }) const requesterRuntime = buildRequesterRuntime(ctx, sessionId, source) + const correlation = buildCorrelationMetadata(sessionId, source, ctx, agentIdForCapture) if (isDev) console.debug(`[taas-affinity] wrapStreamFn sessionId=${sessionId} source=${source}`) @@ -304,7 +360,7 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) { const onPayload: NonNullable["onPayload"] = async (payload, payloadModel) => { const payloadRecord = asRecord(payload) if (!payloadRecord) return prevOnPayload ? prevOnPayload(payload, payloadModel) : payload - const patched = patchPayloadMetadata(payloadRecord, sessionId, requesterRuntime) + const patched = patchPayloadMetadata(payloadRecord, sessionId, requesterRuntime, correlation) return prevOnPayload ? prevOnPayload(patched, payloadModel) : patched } const prevOnResponse = options?.onResponse @@ -323,13 +379,14 @@ function buildWrapper(ctx: ProviderWrapStreamFnContext) { function buildTransportTurnState(ctx: ProviderResolveTransportTurnStateContext): ProviderTransportTurnState | null { const activeSource = getActiveSessionSource() ?? fallbackSessionSource() const sessionId = deriveSessionId(activeSource) + const agentId = deriveAgentIdForCapture(ctx as unknown as { agentDir?: string; workspaceDir?: string }) if (isDev) { console.debug( `[taas-affinity] resolveTransportTurnState sessionId=${sessionId} ` + `source=${activeSource} turnId=${ctx.turnId} attempt=${ctx.attempt}` ) } - return { headers: { "X-Session-Id": sessionId } } + return { headers: buildCorrelationHeaders({ sessionId, turnId: ctx.turnId, attempt: ctx.attempt, agentId }) } } export default { @@ -382,6 +439,8 @@ export default { patchPayloadMetadata, resolveSessionId, captureAutorouterFromHeaders, + buildCorrelationHeaders, + buildCorrelationMetadata, getLastRouteForAgent, getLastRouteForSession, }, diff --git a/package-lock.json b/package-lock.json index c6e50e1..f0f56ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openclaw-taas-affinity", - "version": "0.2.1", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openclaw-taas-affinity", - "version": "0.2.1", + "version": "0.5.2", "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", diff --git a/package.json b/package.json index 4101a3a..c9f42f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw-taas-affinity", - "version": "0.5.1", + "version": "0.5.2", "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.", "type": "module", "main": "dist/index.js", diff --git a/test/smoke.mjs b/test/smoke.mjs index a7b1ae2..f67d0ce 100644 --- a/test/smoke.mjs +++ b/test/smoke.mjs @@ -70,6 +70,13 @@ assert.equal( "direction_2_gateway" ) assert.equal("available_bridges" in capturedPayload.metadata.requester_runtime, false) +assert.equal(capturedPayload.metadata.openclaw_correlation.schema_version, "2026-06-05") +assert.equal(capturedPayload.metadata.openclaw_correlation.source, "openclaw-taas-affinity") +assert.equal(capturedPayload.metadata.openclaw_correlation.plugin_version, "0.5.2") +assert.equal(capturedPayload.metadata.openclaw_correlation.session_id, capturedPayload.metadata.session_id) +assert.equal(capturedPayload.metadata.openclaw_correlation.sticky_key, capturedPayload.metadata.session_id) +assert.equal(capturedPayload.metadata.openclaw_correlation.provider, "cloudsigma") +assert.equal(capturedPayload.metadata.openclaw_correlation.model_id, "cloudsigma/test-model") const transportState = provider.resolveTransportTurnState({ provider: "cloudsigma", @@ -80,6 +87,10 @@ const transportState = provider.resolveTransportTurnState({ }) assert.match(transportState.headers["X-Session-Id"], /^oc:[a-f0-9]{16}$/) +assert.equal(transportState.headers["X-OpenClaw-Session-Id"], transportState.headers["X-Session-Id"]) +assert.equal(transportState.headers["X-OpenClaw-Plugin-Version"], "0.5.2") +assert.equal(transportState.headers["X-OpenClaw-Turn-Id"], "turn-smoke") +assert.equal(transportState.headers["X-OpenClaw-Attempt"], "1") console.log("smoke ok") @@ -117,6 +128,8 @@ const captureStreamFn = async (_model, _context, options = {}) => { "x-taas-autorouter-algorithm-source": "api_key_default", "x-taas-thinking-applied": "medium", "x-taas-routed-context-window": "128000", + "x-request-id": "taas-req-123", + "x-trace-id": "taas-trace-456", }, }, _model @@ -155,6 +168,8 @@ assert.equal(respondedPayload.capture.autorouterAlgo, "best_fit") assert.equal(respondedPayload.capture.autorouterAlgoSource, "api_key_default") assert.equal(respondedPayload.capture.thinkingApplied, "medium") assert.equal(respondedPayload.capture.routedContextWindow, 128000) +assert.equal(respondedPayload.capture.taasRequestId, "taas-req-123") +assert.equal(respondedPayload.capture.taasTraceId, "taas-trace-456") // Non-autorouted response should NOT overwrite (we explicitly drop it) await captureWrapped("model", { messages: [] }, {})