coze_lab 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -5393,6 +5393,12 @@ function getCloudCozeloopApiBaseUrl() {
5393
5393
  function normalizeCozeloopApiBaseUrl(raw) {
5394
5394
  const base = raw.trim().replace(/\/+$/, '');
5395
5395
  if (!base) return '';
5396
+ if (base.endsWith('/v1/loop/traces/ingest')) {
5397
+ return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
5398
+ }
5399
+ if (base.endsWith('/api/v1/loop/traces/ingest')) {
5400
+ return base.slice(0, -'/v1/loop/traces/ingest'.length).replace(/\/+$/, '');
5401
+ }
5396
5402
  if (base.endsWith('/v1/loop/opentelemetry/v1/traces')) {
5397
5403
  return base.slice(0, -'/v1/loop/opentelemetry/v1/traces'.length).replace(/\/+$/, '');
5398
5404
  }
@@ -5431,6 +5437,14 @@ function getOtelTracesUrl(cloud) {
5431
5437
  return `${getOtelEndpointBase(cloud).replace(/\/+$/, '')}/v1/traces`;
5432
5438
  }
5433
5439
 
5440
+ function getCozeloopIngestUrlFromBase(apiBase) {
5441
+ return `${String(apiBase || COZE_API).replace(/\/+$/, '')}/v1/loop/traces/ingest`;
5442
+ }
5443
+
5444
+ function getCozeloopIngestUrl(cloud) {
5445
+ return getCozeloopIngestUrlFromBase(getCozeloopApiBaseUrl(cloud));
5446
+ }
5447
+
5434
5448
  function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud) {
5435
5449
  if (!existing.plugins) existing.plugins = {};
5436
5450
  if (!existing.plugins.allow) existing.plugins.allow = [];
@@ -5450,7 +5464,9 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
5450
5464
  if (!existing.plugins.entries[PLUGIN].config) existing.plugins.entries[PLUGIN].config = {};
5451
5465
  const pcfg = existing.plugins.entries[PLUGIN].config;
5452
5466
  pcfg.authorization = `Bearer ${token}`;
5453
- pcfg.endpoint = getOtelEndpointBase(cloud);
5467
+ // OpenClaw runtime uses CozeLoop ingest protocol, not OTLP. Keep accepting
5468
+ // OTEL_ENDPOINT in cloud, but normalize it to the API base URL before writing.
5469
+ pcfg.endpoint = getCozeloopApiBaseUrl(cloud);
5454
5470
  pcfg.workspaceId = workspaceId;
5455
5471
  pcfg.debug = true;
5456
5472
  pcfg.disableLocalCredentials = !!cloud;
@@ -5848,48 +5864,50 @@ async function verifyOpenClawTraceLink(cloud) {
5848
5864
  return { success: false, status: 0, body: 'plugin authorization missing' };
5849
5865
  }
5850
5866
 
5851
- // 1) 用插件【实际配置的】token 打一条最小 OTLP trace ——这才是运行时真实用的那个 token。
5852
- // 不能发空 resourceSpans,CozeLoop 会返回 "unknown event type",导致自检假失败。
5867
+ // 1) 用插件【实际配置的】token 打一条最小 CozeLoop ingest trace ——这才是运行时真实用的那个 token。
5853
5868
  const authHeader = pcfg.authorization; // 形如 "Bearer czu_xxx"
5854
- const tracesUrl = (pcfg.endpoint
5855
- ? `${String(pcfg.endpoint).replace(/\/+$/, '')}/v1/traces`
5856
- : getOtelTracesUrl(cloud));
5869
+ const tracesUrl = getCozeloopIngestUrlFromBase(normalizeCozeloopApiBaseUrl(pcfg.endpoint || getCozeloopApiBaseUrl(cloud)));
5857
5870
  const workspaceId = pcfg.workspaceId || WORKSPACE_ID;
5858
5871
  const traceId = crypto.randomBytes(16).toString('hex');
5859
5872
  const spanId = crypto.randomBytes(8).toString('hex');
5860
- const nowNs = String(Date.now() * 1_000_000);
5873
+ const nowMicros = Date.now() * 1000;
5861
5874
  const pair = crypto.randomBytes(6).toString('hex');
5862
- const otlpBody = {
5863
- resourceSpans: [{
5864
- resource: {
5865
- attributes: [
5866
- { key: 'service.name', value: { stringValue: 'cozelab-onboard-openclaw' } },
5867
- ],
5875
+ const ingestBody = {
5876
+ spans: [{
5877
+ started_at_micros: nowMicros,
5878
+ log_id: '',
5879
+ span_id: spanId,
5880
+ parent_id: '0',
5881
+ trace_id: traceId,
5882
+ duration_micros: 1,
5883
+ service_name: 'cozelab-onboard-openclaw',
5884
+ workspace_id: String(workspaceId),
5885
+ span_name: 'cozelab-onboard-openclaw-selfcheck',
5886
+ span_type: 'main',
5887
+ status_code: 0,
5888
+ input: 'cozelab-onboard-openclaw selfcheck',
5889
+ output: 'ok',
5890
+ object_storage: '',
5891
+ system_tags_string: {
5892
+ runtime: JSON.stringify({ language: 'nodejs', library: 'openclaw', scene: process.env.COZELOOP_SCENE || 'custom' }),
5868
5893
  },
5869
- scopeSpans: [{
5870
- scope: { name: 'cozelab-onboard-openclaw-selfcheck' },
5871
- spans: [{
5872
- traceId,
5873
- spanId,
5874
- name: 'cozelab-onboard-openclaw-selfcheck',
5875
- kind: 1,
5876
- startTimeUnixNano: nowNs,
5877
- endTimeUnixNano: nowNs,
5878
- status: { code: 1 },
5879
- attributes: [
5880
- { key: 'pair_code', value: { stringValue: pair } },
5881
- { key: 'source', value: { stringValue: 'cozelab-onboard-openclaw' } },
5882
- ],
5883
- }],
5884
- }],
5894
+ system_tags_long: {},
5895
+ system_tags_double: {},
5896
+ tags_string: {
5897
+ pair_code: pair,
5898
+ source: 'cozelab-onboard-openclaw',
5899
+ },
5900
+ tags_long: {},
5901
+ tags_double: {},
5902
+ tags_bool: {},
5885
5903
  }],
5886
5904
  };
5887
5905
  let res;
5888
5906
  try {
5889
5907
  res = await httpsPost(
5890
5908
  tracesUrl,
5891
- otlpBody,
5892
- { Authorization: authHeader, 'cozeloop-workspace-id': String(workspaceId) },
5909
+ ingestBody,
5910
+ { Authorization: authHeader },
5893
5911
  );
5894
5912
  } catch (e) {
5895
5913
  warn(`openclaw 插件 token 上报探测失败: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -80,125 +80,150 @@ async function getRefreshedToken(currentAuthorization, opts = {}) {
80
80
 
81
81
  const EXPORT_SUCCESS = 0;
82
82
  const EXPORT_FAILED = 1;
83
+ const INGEST_TRACE_PATH = "/v1/loop/traces/ingest";
83
84
 
84
- function normalizeTraceUrl(endpoint) {
85
+ function normalizeApiBaseUrl(endpoint) {
85
86
  const base = String(endpoint || "").replace(/\/+$/, "");
86
- return base.endsWith("/v1/traces") ? base : `${base}/v1/traces`;
87
+ if (!base)
88
+ return _COZE_API;
89
+ const suffixes = [
90
+ "/v1/loop/traces/ingest",
91
+ "/api/v1/loop/traces/ingest",
92
+ "/v1/loop/opentelemetry/v1/traces",
93
+ "/api/v1/loop/opentelemetry/v1/traces",
94
+ "/v1/loop/opentelemetry",
95
+ "/api/v1/loop/opentelemetry",
96
+ "/v1/traces",
97
+ "/api/v1",
98
+ ];
99
+ for (const suffix of suffixes) {
100
+ if (base.endsWith(suffix)) {
101
+ const trimSuffix = suffix.startsWith("/api/") ? suffix.slice("/api".length) : suffix;
102
+ return base.slice(0, -trimSuffix.length).replace(/\/+$/, "");
103
+ }
104
+ }
105
+ return base;
87
106
  }
88
107
 
89
- function hrTimeToUnixNano(time) {
108
+ function normalizeIngestUrl(endpoint) {
109
+ const base = String(endpoint || "").replace(/\/+$/, "");
110
+ if (base.endsWith(INGEST_TRACE_PATH))
111
+ return base;
112
+ return `${normalizeApiBaseUrl(base)}${INGEST_TRACE_PATH}`;
113
+ }
114
+
115
+ function hrTimeToUnixMicros(time) {
90
116
  if (Array.isArray(time)) {
91
- return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
117
+ return Number(BigInt(time[0]) * 1000000n + BigInt(Math.trunc(time[1] / 1000)));
92
118
  }
93
119
  const millis = time instanceof Date ? time.getTime() : Number(time || Date.now());
94
- return (BigInt(Math.trunc(millis)) * 1000000n).toString();
120
+ return Math.trunc(millis * 1000);
95
121
  }
96
122
 
97
- function spanKindToOtlp(kind) {
98
- switch (kind) {
99
- case SpanKind.INTERNAL: return 1;
100
- case SpanKind.SERVER: return 2;
101
- case SpanKind.CLIENT: return 3;
102
- case SpanKind.PRODUCER: return 4;
103
- case SpanKind.CONSUMER: return 5;
104
- default: return 0;
123
+ function hrDurationToMicros(duration) {
124
+ if (Array.isArray(duration)) {
125
+ return Number(BigInt(duration[0]) * 1000000n + BigInt(Math.trunc(duration[1] / 1000)));
105
126
  }
127
+ return 0;
106
128
  }
107
129
 
108
- function scalarToAnyValue(value) {
130
+ function safeStringify(value) {
109
131
  if (value === undefined || value === null)
110
- return undefined;
132
+ return "";
111
133
  if (typeof value === "string")
112
- return { stringValue: value };
113
- if (typeof value === "boolean")
114
- return { boolValue: value };
115
- if (typeof value === "number") {
116
- if (Number.isInteger(value))
117
- return { intValue: String(value) };
118
- return { doubleValue: value };
119
- }
134
+ return value;
120
135
  try {
121
- return { stringValue: JSON.stringify(value) };
136
+ return JSON.stringify(value);
122
137
  }
123
138
  catch {
124
- return { stringValue: String(value) };
139
+ return String(value);
125
140
  }
126
141
  }
127
142
 
128
- function valueToAnyValue(value) {
129
- if (Array.isArray(value)) {
130
- const values = value.map(scalarToAnyValue).filter(Boolean);
131
- return { arrayValue: { values } };
143
+ function mapSpanType(type) {
144
+ switch (type) {
145
+ case "entry":
146
+ case "gateway":
147
+ case "message":
148
+ case "session":
149
+ return "main";
150
+ case "agent":
151
+ case "model":
152
+ case "tool":
153
+ return type;
154
+ default:
155
+ return type || "main";
132
156
  }
133
- return scalarToAnyValue(value);
134
157
  }
135
158
 
136
- function attributesToOtlp(attributes) {
137
- const out = [];
138
- for (const [key, value] of Object.entries(attributes || {})) {
139
- const otlpValue = valueToAnyValue(value);
140
- if (otlpValue) {
141
- out.push({ key, value: otlpValue });
142
- }
159
+ function putTypedTag(maps, key, value) {
160
+ if (value === undefined || value === null || key === "")
161
+ return;
162
+ if (key === "cozeloop.span_type" || key === "cozeloop.input" || key === "cozeloop.output" ||
163
+ key === "cozeloop.system_tag_runtime") {
164
+ return;
143
165
  }
144
- return out;
145
- }
146
-
147
- function spanToOtlp(span) {
148
- const spanContext = span.spanContext();
149
- const otlpSpan = {
150
- traceId: spanContext.traceId,
151
- spanId: spanContext.spanId,
152
- name: span.name,
153
- kind: spanKindToOtlp(span.kind),
154
- startTimeUnixNano: hrTimeToUnixNano(span.startTime),
155
- endTimeUnixNano: hrTimeToUnixNano(span.endTime),
156
- attributes: attributesToOtlp(span.attributes),
157
- status: { code: span.status?.code ?? 0 },
158
- };
159
- if (span.parentSpanId) {
160
- otlpSpan.parentSpanId = span.parentSpanId;
166
+ if (typeof value === "boolean") {
167
+ maps.bool[key] = value;
168
+ }
169
+ else if (typeof value === "number" && Number.isInteger(value)) {
170
+ maps.long[key] = value;
161
171
  }
162
- if (span.status?.message) {
163
- otlpSpan.status.message = span.status.message;
172
+ else if (typeof value === "number") {
173
+ maps.double[key] = value;
174
+ }
175
+ else {
176
+ maps.string[key] = typeof value === "string" ? value : safeStringify(value);
164
177
  }
165
- return otlpSpan;
166
178
  }
167
179
 
168
- function buildOtlpTraceRequest(spans) {
169
- const resourceGroups = new Map();
170
- for (const span of spans) {
171
- const resourceAttrs = attributesToOtlp(span.resource?.attributes || {});
172
- const scope = span.instrumentationLibrary || {};
173
- const scopeData = {
174
- name: scope.name || "openclaw-cozeloop-trace",
175
- version: scope.version || PLUGIN_VERSION,
176
- };
177
- if (scope.schemaUrl) {
178
- scopeData.schemaUrl = scope.schemaUrl;
179
- }
180
- const resourceKey = JSON.stringify(resourceAttrs);
181
- let resourceGroup = resourceGroups.get(resourceKey);
182
- if (!resourceGroup) {
183
- resourceGroup = {
184
- resource: { attributes: resourceAttrs },
185
- scopeGroups: new Map(),
186
- };
187
- resourceGroups.set(resourceKey, resourceGroup);
188
- }
189
- const scopeKey = JSON.stringify(scopeData);
190
- let scopeGroup = resourceGroup.scopeGroups.get(scopeKey);
191
- if (!scopeGroup) {
192
- scopeGroup = { scope: scopeData, spans: [] };
193
- resourceGroup.scopeGroups.set(scopeKey, scopeGroup);
180
+ function splitAttributes(attributes) {
181
+ const tags = { string: {}, long: {}, double: {}, bool: {} };
182
+ const systemTags = { string: {}, long: {}, double: {} };
183
+ for (const [key, value] of Object.entries(attributes || {})) {
184
+ if (key === "cozeloop.system_tag_runtime") {
185
+ systemTags.string.runtime = safeStringify(value);
186
+ continue;
194
187
  }
195
- scopeGroup.spans.push(spanToOtlp(span));
188
+ putTypedTag(tags, key, value);
196
189
  }
190
+ return { tags, systemTags };
191
+ }
192
+
193
+ function spanToUploadSpan(span, config) {
194
+ const spanContext = span.spanContext();
195
+ const attrs = span.attributes || {};
196
+ const { tags, systemTags } = splitAttributes(attrs);
197
+ const resourceAttrs = span.resource?.attributes || {};
198
+ const serviceName = resourceAttrs["service.name"] || config.serviceName || "openclaw-agent";
199
+ const spanType = mapSpanType(attrs["cozeloop.span_type"]);
200
+ const input = attrs["cozeloop.input"] !== undefined ? safeStringify(attrs["cozeloop.input"]) : "";
201
+ const output = attrs["cozeloop.output"] !== undefined ? safeStringify(attrs["cozeloop.output"]) : "";
202
+ const hasError = span.status?.code === SpanStatusCode.ERROR || attrs.error === true || attrs["tool.error"] === true;
203
+
197
204
  return {
198
- resourceSpans: Array.from(resourceGroups.values()).map((resourceGroup) => ({
199
- resource: resourceGroup.resource,
200
- scopeSpans: Array.from(resourceGroup.scopeGroups.values()),
201
- })),
205
+ started_at_micros: hrTimeToUnixMicros(span.startTime),
206
+ log_id: "",
207
+ traceId: spanContext.traceId,
208
+ trace_id: spanContext.traceId,
209
+ span_id: spanContext.spanId,
210
+ parent_id: span.parentSpanId || "0",
211
+ duration_micros: hrDurationToMicros(span.duration),
212
+ service_name: String(serviceName),
213
+ workspace_id: String(config.workspaceId || ""),
214
+ span_name: String(span.name || "unknown"),
215
+ span_type: spanType,
216
+ status_code: hasError ? -1 : 0,
217
+ input,
218
+ output,
219
+ object_storage: "",
220
+ system_tags_string: systemTags.string,
221
+ system_tags_long: systemTags.long,
222
+ system_tags_double: systemTags.double,
223
+ tags_string: tags.string,
224
+ tags_long: tags.long,
225
+ tags_double: tags.double,
226
+ tags_bool: tags.bool,
202
227
  };
203
228
  }
204
229
 
@@ -224,17 +249,19 @@ function postJson(url, body, headers) {
224
249
  res.on("end", () => resolve({ status: res.statusCode || 0, body: buf }));
225
250
  });
226
251
  req.on("error", reject);
227
- req.setTimeout(15000, () => req.destroy(new Error("OTLP JSON export timed out")));
252
+ req.setTimeout(15000, () => req.destroy(new Error("CozeLoop ingest export timed out")));
228
253
  req.write(data);
229
254
  req.end();
230
255
  });
231
256
  }
232
257
 
233
- class JsonOtlpTraceExporter {
258
+ class CozeloopIngestExporter {
234
259
  constructor(config) {
235
- this.url = normalizeTraceUrl(config.url);
260
+ this.url = normalizeIngestUrl(config.endpoint || config.url);
236
261
  this.headers = config.headers || {};
237
262
  this.logger = config.logger;
263
+ this.workspaceId = config.workspaceId;
264
+ this.serviceName = config.serviceName;
238
265
  this.shutdownRequested = false;
239
266
  }
240
267
  export(spans, resultCallback) {
@@ -245,17 +272,35 @@ class JsonOtlpTraceExporter {
245
272
  this.postSpans(spans)
246
273
  .then(() => resultCallback({ code: EXPORT_SUCCESS }))
247
274
  .catch((err) => {
248
- this.logger?.error?.(`[CozeloopTrace] OTLP JSON export failed: ${err?.message || err}`);
275
+ this.logger?.error?.(`[CozeloopTrace] CozeLoop ingest export failed: ${err?.message || err}`);
249
276
  resultCallback({ code: EXPORT_FAILED, error: err });
250
277
  });
251
278
  }
252
279
  async postSpans(spans) {
253
- const body = buildOtlpTraceRequest(spans);
280
+ const body = {
281
+ spans: spans.map((span) => spanToUploadSpan(span, {
282
+ workspaceId: this.workspaceId,
283
+ serviceName: this.serviceName,
284
+ })),
285
+ };
254
286
  const res = await postJson(this.url, body, this.headers);
255
287
  if (res.status < 200 || res.status >= 300) {
256
288
  const snippet = String(res.body || "").slice(0, 300);
257
289
  throw new Error(`HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`);
258
290
  }
291
+ if (res.body) {
292
+ try {
293
+ const parsed = JSON.parse(res.body);
294
+ if (parsed && parsed.code !== undefined && parsed.code !== 0) {
295
+ throw new Error(`code ${parsed.code}: ${parsed.msg || res.body.slice(0, 300)}`);
296
+ }
297
+ }
298
+ catch (err) {
299
+ if (/^code\s/.test(String(err?.message || ""))) {
300
+ throw err;
301
+ }
302
+ }
303
+ }
259
304
  }
260
305
  async forceFlush() {
261
306
  return;
@@ -360,12 +405,13 @@ export class CozeloopExporter {
360
405
  const authorization = this.config.authorization;
361
406
  const workspaceId = this.config.workspaceId;
362
407
  this.api.logger.info(`[CozeloopTrace] Using authorization, workspaceId=${workspaceId}, tokenLength=${authorization?.length}`);
363
- const exporter = new JsonOtlpTraceExporter({
364
- url: `${this.config.endpoint}/v1/traces`,
408
+ const exporter = new CozeloopIngestExporter({
409
+ endpoint: this.config.endpoint,
365
410
  logger: this.api.logger,
411
+ workspaceId,
412
+ serviceName: this.config.serviceName,
366
413
  headers: {
367
414
  "Authorization": authorization,
368
- "cozeloop-workspace-id": workspaceId,
369
415
  "x-tt-env": "ppe_cozelab",
370
416
  "x-use-ppe": "1",
371
417
  },
@@ -476,6 +476,19 @@ function resolveCozeContext(input, sessionId) {
476
476
  return cozeContextBySession.get(sessionId);
477
477
  return lastCozeContext || {};
478
478
  }
479
+ function resetRuntimeState() {
480
+ lastUserChannelId = undefined;
481
+ lastUserTraceContext = undefined;
482
+ lastOpenclawSessionId = undefined;
483
+ cozeContextBySession.clear();
484
+ lastCozeContext = undefined;
485
+ activeAgentCtx = undefined;
486
+ activeAgentChannelId = undefined;
487
+ activeAgentContextByKey.clear();
488
+ lastUserInput = undefined;
489
+ lastUserContextByKey.clear();
490
+ pendingToolCallsByKey.clear();
491
+ }
479
492
  // Active agent context: set in before_agent_start, cleared in agent_end.
480
493
  // All hooks between these two (llm_input, llm_output, tool calls, messages)
481
494
  // use this to ensure every span lands in the same Trace.
@@ -503,6 +516,7 @@ const cozeloopTracePlugin = {
503
516
  version: PLUGIN_VERSION,
504
517
  description: "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
505
518
  activate(api) {
519
+ resetRuntimeState();
506
520
  const pluginConfig = api.pluginConfig || {};
507
521
  const authorization = pluginConfig.authorization;
508
522
  const workspaceId = pluginConfig.workspaceId;
@@ -511,7 +525,7 @@ const cozeloopTracePlugin = {
511
525
  return;
512
526
  }
513
527
  const config = {
514
- endpoint: pluginConfig.endpoint || "https://api.coze.cn/v1/loop/opentelemetry",
528
+ endpoint: pluginConfig.endpoint || "https://api.coze.cn",
515
529
  authorization,
516
530
  workspaceId,
517
531
  serviceName: pluginConfig.serviceName || "openclaw-agent",
@@ -573,6 +587,21 @@ const cozeloopTracePlugin = {
573
587
  const lastSessionId = lastUserTraceContext?.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
574
588
  return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
575
589
  };
590
+ const canFallbackToActiveAgentContext = (rawChannelId) => {
591
+ const rawSessionId = sessionIdFromChannelId(rawChannelId);
592
+ const activeSessionId = activeAgentCtx?.openclawSessionId || sessionIdFromChannelId(activeAgentChannelId);
593
+ return !rawSessionId || !activeSessionId || rawSessionId === activeSessionId;
594
+ };
595
+ const canUseLastUserFallbackFor = (ctx, rawChannelId) => {
596
+ if (!lastUserTraceContext || lastUserTraceContext === ctx)
597
+ return true;
598
+ const rawSessionId = ctx?.openclawSessionId || sessionIdFromChannelId(rawChannelId);
599
+ const lastSessionId = lastUserTraceContext.openclawSessionId || sessionIdFromChannelId(lastUserChannelId);
600
+ return !rawSessionId || !lastSessionId || rawSessionId === lastSessionId;
601
+ };
602
+ const lastUserFallbackFor = (ctx, rawChannelId) => {
603
+ return canUseLastUserFallbackFor(ctx, rawChannelId) ? lastUserTraceContext : undefined;
604
+ };
576
605
  const startTurn = (runId, channelId, originalChannelId, openclawSessionId) => {
577
606
  const traceId = generateId(32);
578
607
  const ctx = {
@@ -598,7 +627,7 @@ const cozeloopTracePlugin = {
598
627
  const getOrCreateContext = (rawChannelId, runId, hookName) => {
599
628
  let channelId = rawChannelId;
600
629
  let activeCtx = getContextByChannel(rawChannelId);
601
- const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}`;
630
+ const effectiveRunId = runId || activeCtx?.runId || `run-${Date.now()}-${generateId(8)}`;
602
631
  if (rawChannelId.startsWith("agent/") && effectiveRunId) {
603
632
  const originalChannelId = getOriginalChannelId(effectiveRunId);
604
633
  if (originalChannelId) {
@@ -656,7 +685,9 @@ const cozeloopTracePlugin = {
656
685
  return found;
657
686
  }
658
687
  if (lastUserTraceContext) {
659
- return { ctx: lastUserTraceContext, channelId: lastUserChannelId || channelId };
688
+ if (canFallbackToLastUserContext(channelId)) {
689
+ return { ctx: lastUserTraceContext, channelId: lastUserChannelId || channelId };
690
+ }
660
691
  }
661
692
  return undefined;
662
693
  };
@@ -674,7 +705,9 @@ const cozeloopTracePlugin = {
674
705
  return found;
675
706
  }
676
707
  if (activeAgentCtx) {
677
- return { ctx: activeAgentCtx, channelId: activeAgentChannelId || channelId };
708
+ if (canFallbackToActiveAgentContext(channelId)) {
709
+ return { ctx: activeAgentCtx, channelId: activeAgentChannelId || channelId };
710
+ }
678
711
  }
679
712
  return undefined;
680
713
  };
@@ -1338,14 +1371,15 @@ const cozeloopTracePlugin = {
1338
1371
  }
1339
1372
  const rootSpanId = ctx.rootSpanId;
1340
1373
  const rootSpanStartTime = ctx.rootSpanStartTime;
1341
- const userInput = ctx.userInput || (lastUserTraceContext ? lastUserTraceContext.userInput : undefined) || lastUserInput;
1374
+ const lastUserFallback = lastUserFallbackFor(ctx, channelId);
1375
+ const userInput = ctx.userInput || lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
1342
1376
  const traceId = ctx.traceId;
1343
1377
  const hasRootSpan = !!rootSpanStartTime;
1344
1378
  const savedLastUserChannelId = lastUserChannelId;
1345
1379
  const originalChannelId = ctx.originalChannelId || channelId;
1346
1380
  setTimeout(async () => {
1347
1381
  if (hasRootSpan) {
1348
- const finalOutput = ctx.lastOutput || (lastUserTraceContext ? lastUserTraceContext.lastOutput : undefined);
1382
+ const finalOutput = ctx.lastOutput || lastUserFallback?.lastOutput;
1349
1383
  if (config.debug) {
1350
1384
  api.logger.info(`[CozeloopTrace] Ending root span with input=${userInput ? 'present' : 'missing'}, output=${finalOutput ? 'present' : 'missing'}`);
1351
1385
  }
@@ -1390,8 +1424,9 @@ const cozeloopTracePlugin = {
1390
1424
  // currentRootContext is set and the agent span becomes a proper child.
1391
1425
  const ensureRootSpan = async (ctx, channelId) => {
1392
1426
  // 心跳轮询消息:不是真实对话,整条 trace 不上报(沿用下方“无 coze-context 即 return”的同款范式)。
1427
+ const lastUserFallback = lastUserFallbackFor(ctx, channelId);
1393
1428
  const heartbeatInput = ctx.userInput
1394
- || lastUserTraceContext?.userInput || lastUserInput;
1429
+ || lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
1395
1430
  if (isHeartbeatInput(heartbeatInput)) {
1396
1431
  if (config.debug) {
1397
1432
  api.logger.info(`[CozeloopTrace] skip heartbeat poll trace, traceId=${ctx.traceId}`);
@@ -1408,7 +1443,7 @@ const cozeloopTracePlugin = {
1408
1443
  let cozeCtx = resolveCozeContext(ctx.userInput, ocSessionId);
1409
1444
  if (Object.keys(cozeCtx).length === 0) {
1410
1445
  // Try the fallback user inputs too before giving up.
1411
- const fallbackInput = lastUserTraceContext?.userInput || lastUserInput;
1446
+ const fallbackInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
1412
1447
  cozeCtx = resolveCozeContext(fallbackInput, ocSessionId);
1413
1448
  }
1414
1449
  if (Object.keys(cozeCtx).length === 0) {
@@ -1439,7 +1474,7 @@ const cozeloopTracePlugin = {
1439
1474
  // Resolve user input: prefer ctx.userInput set by this turn's
1440
1475
  // message_received, fall back to lastUserTraceContext, then lastUserInput.
1441
1476
  if (!ctx.userInput) {
1442
- ctx.userInput = lastUserTraceContext?.userInput || lastUserInput;
1477
+ ctx.userInput = lastUserFallback?.userInput || (lastUserFallback ? lastUserInput : undefined);
1443
1478
  }
1444
1479
  const rootSpanData = {
1445
1480
  name: "openclaw_request",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-cozeloop-trace",
3
3
  "name": "OpenClaw CozeLoop Trace",
4
- "version": "0.1.15",
4
+ "version": "0.1.16",
5
5
  "description": "Report OpenClaw execution traces to CozeLoop via OpenTelemetry",
6
6
  "type": "plugin",
7
7
  "entry": "./dist/index.js",
@@ -10,8 +10,8 @@
10
10
  "properties": {
11
11
  "endpoint": {
12
12
  "type": "string",
13
- "default": "https://api.coze.cn/v1/loop/opentelemetry",
14
- "description": "CozeLoop OTLP endpoint URL"
13
+ "default": "https://api.coze.cn",
14
+ "description": "CozeLoop API base URL"
15
15
  },
16
16
  "authorization": {
17
17
  "type": "string",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cozeloop/openclaw-cozeloop-trace",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "OpenClaw Plugin for reporting traces to CozeLoop via OpenTelemetry",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",