@mastra/clickhouse 1.26.0-alpha.2 → 1.26.0-alpha.4

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/dist/index.js CHANGED
@@ -6841,6 +6841,14 @@ async function getScorePercentiles(client, args) {
6841
6841
  //#endregion
6842
6842
  //#region src/storage/domains/observability/v-next/trace-query.ts
6843
6843
  const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6844
+ /**
6845
+ * Core versions without the field reject it in strict query results, so it is only returned when core knows it.
6846
+ * Core before 1.68 has no field registry at all.
6847
+ */
6848
+ const timeToFirstChunkSupported = "timeToFirstChunkMs" in (coreStorage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
6849
+ /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
6850
+ const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
6851
+ const timeToFirstChunkFeatures = timeToFirstChunkSupported ? timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk", "trace-query-time-to-first-chunk-sort"] : ["trace-query-time-to-first-chunk"] : [];
6844
6852
  function durationMsSql(startedAt, endedAt) {
6845
6853
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6846
6854
  }
@@ -6889,6 +6897,10 @@ const TRACE_FIELDS = {
6889
6897
  sql: durationMsSql("r.startedAt", "r.endedAt"),
6890
6898
  parameterType: "Float64"
6891
6899
  },
6900
+ timeToFirstChunkMs: {
6901
+ sql: "r.timeToFirstChunkMs",
6902
+ parameterType: "Float64"
6903
+ },
6892
6904
  entityName: {
6893
6905
  sql: "r.entityName",
6894
6906
  parameterType: "String"
@@ -6897,6 +6909,14 @@ const TRACE_FIELDS = {
6897
6909
  sql: "r.entityType",
6898
6910
  parameterType: "String"
6899
6911
  },
6912
+ name: {
6913
+ sql: "r.name",
6914
+ parameterType: "String"
6915
+ },
6916
+ spanType: {
6917
+ sql: "r.spanType",
6918
+ parameterType: "String"
6919
+ },
6900
6920
  environment: {
6901
6921
  sql: "r.environment",
6902
6922
  parameterType: "String"
@@ -6939,6 +6959,10 @@ const SPAN_FIELDS = {
6939
6959
  sql: "s.durationMs",
6940
6960
  parameterType: "Float64"
6941
6961
  },
6962
+ timeToFirstChunkMs: {
6963
+ sql: "s.timeToFirstChunkMs",
6964
+ parameterType: "Float64"
6965
+ },
6942
6966
  status: {
6943
6967
  sql: "s.status",
6944
6968
  parameterType: "String"
@@ -7150,6 +7174,7 @@ const TRACE_SELECT = `
7150
7174
  r.entityName AS entityName,
7151
7175
  r.entityType AS entityType,
7152
7176
  r.environment AS environment,
7177
+ r.timeToFirstChunkMs AS timeToFirstChunkMs,
7153
7178
  ${TRACE_STATUS_SQL} AS status`;
7154
7179
  var ParameterBuilder = class {
7155
7180
  params = {};
@@ -7166,7 +7191,7 @@ function fieldDefinition(registry, field) {
7166
7191
  return definition;
7167
7192
  }
7168
7193
  function resolveOrderField(field) {
7169
- if (field === "startedAt" || field === "endedAt") return field;
7194
+ if (field === "startedAt" || field === "endedAt" || field === "timeToFirstChunkMs") return field;
7170
7195
  throw new Error(`Unsupported trusted trace-query field: ${field}`);
7171
7196
  }
7172
7197
  function isMetadataField(field) {
@@ -7325,6 +7350,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7325
7350
  startedAt,
7326
7351
  endedAt,
7327
7352
  ${durationMsSql("startedAt", "endedAt")} AS durationMs,
7353
+ timeToFirstChunkMs,
7328
7354
  if(isNotNull(error), 'error', 'success') AS status,
7329
7355
  error,
7330
7356
  entityType,
@@ -7443,9 +7469,11 @@ function traceOrder(plan) {
7443
7469
  */
7444
7470
  function keysetCursorCondition(plan, order, parameters) {
7445
7471
  if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7472
+ if (plan.cursor.sortValue === null) return `(isNull(sortValue) AND traceId > ${parameters.add(plan.cursor.traceId, "String")})`;
7473
+ const numeric = order.field === "timeToFirstChunkMs";
7446
7474
  const comparison = order.direction === "ASC" ? ">" : "<";
7447
- const sortValue = parameters.add(plan.cursor.sortValue, "DateTime64(3, 'UTC')");
7448
- return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")}))`;
7475
+ const sortValue = parameters.add(plan.cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
7476
+ return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")})${numeric ? " OR isNull(sortValue)" : ""})`;
7449
7477
  }
7450
7478
  /**
7451
7479
  * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
@@ -7457,21 +7485,22 @@ function compileClickHouseTraceQueryKeys(plan) {
7457
7485
  if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7458
7486
  const parameters = new ParameterBuilder();
7459
7487
  const order = traceOrder(plan);
7460
- const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt", parameters, QUERY_TRACES_SCOPE);
7488
+ const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt, r.timeToFirstChunkMs AS timeToFirstChunkMs", parameters, QUERY_TRACES_SCOPE);
7461
7489
  const cursor = keysetCursorCondition(plan, order, parameters);
7462
7490
  const pick = order.direction === "DESC" ? "max" : "min";
7491
+ const rootOrder = order.field === "timeToFirstChunkMs" ? `ifNull(${order.field}, ${pick === "max" ? "-inf" : "inf"})` : order.field;
7463
7492
  const limit = plan.paginationMode === "page" ? `LIMIT ${parameters.add(plan.perPage, "UInt64")} OFFSET ${parameters.add(plan.page * plan.perPage, "UInt64")}` : `LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`;
7464
7493
  return {
7465
7494
  query: `WITH ${ctes.join(",\n")},
7466
7495
  per_trace AS (
7467
7496
  SELECT traceId, ${pick}(${order.field}) AS sortValue,
7468
- ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${order.field}) AS root
7497
+ ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${rootOrder}) AS root
7469
7498
  FROM candidates
7470
7499
  GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7471
7500
  )
7472
7501
  SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7473
7502
  FROM per_trace
7474
- ORDER BY sortValue ${order.direction}, traceId ASC
7503
+ ORDER BY sortValue ${order.direction} NULLS LAST, traceId ASC
7475
7504
  ${limit}`,
7476
7505
  query_params: parameters.params,
7477
7506
  sharedSnapshot: true
@@ -7489,16 +7518,16 @@ function compileClickHouseTraceRowsByKey(plan, keys) {
7489
7518
  const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7490
7519
  const byEnd = order.field === "endedAt";
7491
7520
  const tuples = keys.map((key) => {
7492
- const started = ts(byEnd ? key.rootStartedAt : key.sortValue);
7521
+ const started = ts(key.rootStartedAt);
7493
7522
  const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7494
- return byEnd ? `(${started}, ${id}, ${ts(key.sortValue)})` : `(${started}, ${id})`;
7523
+ return byEnd ? `(${started}, ${id}, ${ts(String(key.sortValue))})` : `(${started}, ${id})`;
7495
7524
  });
7496
7525
  const predicate = (plan.where ? (plan.where.type === "boolean" && plan.where.operator === "and" ? plan.where.args : [plan.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : []).map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("");
7497
7526
  return {
7498
7527
  query: `SELECT ${TRACE_SELECT}
7499
7528
  FROM ${TABLE_TRACE_ROOTS} r
7500
7529
  WHERE (r.startedAt, r.traceId, r.dedupeKey${byEnd ? ", r.endedAt" : ""}) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7501
- ORDER BY ${order.field} ${order.direction}, traceId ASC
7530
+ ORDER BY ${order.field} ${order.direction} NULLS LAST, traceId ASC
7502
7531
  LIMIT 1 BY traceId`,
7503
7532
  query_params: parameters.params
7504
7533
  };
@@ -7737,6 +7766,12 @@ async function runWithClickHouseTraceQueryTimeout(client, limits, compiled, quer
7737
7766
  ...CH_SETTINGS,
7738
7767
  max_execution_time: resolvedTimeoutMs / 1e3,
7739
7768
  ...limits.memoryLimitBytes === void 0 ? {} : { max_memory_usage: String(limits.memoryLimitBytes) },
7769
+ ...limits.maxThreads === void 0 ? {} : { max_threads: limits.maxThreads },
7770
+ ...limits.maxBlockSize === void 0 ? {} : { max_block_size: String(limits.maxBlockSize) },
7771
+ ...limits.spillThresholdBytes === void 0 ? {} : {
7772
+ max_bytes_before_external_sort: String(limits.spillThresholdBytes),
7773
+ max_bytes_before_external_group_by: String(limits.spillThresholdBytes)
7774
+ },
7740
7775
  ...compiled.sharedSnapshot ? { enable_shared_storage_snapshot_in_query: 1 } : {}
7741
7776
  }
7742
7777
  })).json();
@@ -7859,7 +7894,8 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7859
7894
  entityName: row.entityName == null ? null : String(row.entityName),
7860
7895
  entityType: row.entityType == null ? null : String(row.entityType),
7861
7896
  environment: row.environment == null ? null : String(row.environment),
7862
- status: row.status
7897
+ status: row.status,
7898
+ ...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) }
7863
7899
  });
7864
7900
  if (plan.paginationMode === "delta") {
7865
7901
  const rows = await run(compileClickHouseTraceQuery(plan, deltaHead));
@@ -7882,6 +7918,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7882
7918
  });
7883
7919
  }
7884
7920
  const keys = await run(compileClickHouseTraceQueryKeys(plan));
7921
+ const sortValue = (value) => plan.orderBy.field !== "timeToFirstChunkMs" ? asIsoTimestamp(value) : value == null ? null : Number(value);
7885
7922
  let total = Number(keys[0]?.__total ?? 0);
7886
7923
  if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
7887
7924
  const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
@@ -7891,7 +7928,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7891
7928
  const rows = visible.length > 0 ? await run(compileClickHouseTraceRowsByKey(plan, visible.map((key) => ({
7892
7929
  traceId: String(key.traceId),
7893
7930
  dedupeKey: String(key.dedupeKey),
7894
- sortValue: asIsoTimestamp(key.sortValue),
7931
+ sortValue: sortValue(key.sortValue),
7895
7932
  rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7896
7933
  })))) : [];
7897
7934
  if (plan.paginationMode === "page") return coreStorage.traceQueryResponseSchema.parse({
@@ -7909,7 +7946,7 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7909
7946
  traces: await toTraceRecords(rows),
7910
7947
  page: { next: keys.length > plan.limit && lastKey ? coreStorage.encodeTraceQueryCursor(plan, {
7911
7948
  result: "traces",
7912
- sortValue: asIsoTimestamp(lastKey.sortValue),
7949
+ sortValue: sortValue(lastKey.sortValue),
7913
7950
  traceId: String(lastKey.traceId)
7914
7951
  }) : null }
7915
7952
  });
@@ -7934,6 +7971,16 @@ function compileSpanQueryPredicate(predicate) {
7934
7971
  params: parameters.params
7935
7972
  };
7936
7973
  }
7974
+ /** Thread filters see full-history representative values, never individual member traces. */
7975
+ function compileThreadQueryPredicate(predicate, parameters = new ParameterBuilder()) {
7976
+ return {
7977
+ sql: compileScalarPredicate(predicate, Object.fromEntries(Object.entries(coreStorage.THREAD_QUERY_FIELD_REGISTRY).map(([field, rule]) => [field, {
7978
+ sql: `s.${field}`,
7979
+ parameterType: rule.valueKind === "timestamp" ? "DateTime64(3, 'UTC')" : rule.valueKind === "number" ? "Float64" : "String"
7980
+ }])), parameters),
7981
+ params: parameters.params
7982
+ };
7983
+ }
7937
7984
  //#endregion
7938
7985
  //#region src/storage/domains/observability/v-next/span-query.ts
7939
7986
  const identityFields = [
@@ -7974,14 +8021,15 @@ const displayFields = [
7974
8021
  "model",
7975
8022
  "provider",
7976
8023
  "durationMs",
8024
+ "timeToFirstChunkMs",
7977
8025
  "status"
7978
8026
  ];
7979
8027
  var Parameters = class {
7980
8028
  values = {};
7981
- add(value, timestamp = false) {
8029
+ add(value, type = typeof value === "number" ? "UInt64" : "String") {
7982
8030
  const name = `span_${Object.keys(this.values).length}`;
7983
- this.values[name] = timestamp ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
7984
- return `{${name}:${timestamp ? "DateTime64(3, 'UTC')" : typeof value === "number" ? "UInt64" : "String"}}`;
8031
+ this.values[name] = type === "DateTime64(3, 'UTC')" ? new Date(value).toISOString().replace("T", " ").replace(/Z$/, "") : value;
8032
+ return `{${name}:${type}}`;
7985
8033
  }
7986
8034
  };
7987
8035
  function identityValues(row) {
@@ -7994,13 +8042,20 @@ function identityValues(row) {
7994
8042
  row.spanId
7995
8043
  ];
7996
8044
  }
7997
- function afterCursor(plan, parameters) {
8045
+ function afterCursor$1(plan, parameters) {
7998
8046
  if (!plan.cursor) return "1";
7999
8047
  const cursor = plan.cursor;
8000
- const timestamp = parameters.add(cursor.sortValue, true);
8001
8048
  const field = `s.${plan.orderBy.field}`;
8002
- const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${timestamp}`];
8003
- const equal = [`${field} = ${timestamp}`];
8049
+ const terms = [];
8050
+ const equal = [];
8051
+ if (cursor.sortValue === null) equal.push(`isNull(${field})`);
8052
+ else {
8053
+ const numeric = plan.orderBy.field === "timeToFirstChunkMs";
8054
+ const value = parameters.add(cursor.sortValue, numeric ? "Float64" : "DateTime64(3, 'UTC')");
8055
+ terms.push(`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${value}`);
8056
+ if (numeric) terms.push(`isNull(${field})`);
8057
+ equal.push(`${field} = ${value}`);
8058
+ }
8004
8059
  for (const key of identityFields) {
8005
8060
  const value = cursor[key];
8006
8061
  if (value === null) equal.push(`isNull(s.${key})`);
@@ -8012,10 +8067,10 @@ function afterCursor(plan, parameters) {
8012
8067
  }
8013
8068
  return `(${terms.join(" OR ")})`;
8014
8069
  }
8015
- function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
8070
+ function predicateFields$1(predicate, fields = /* @__PURE__ */ new Set()) {
8016
8071
  if (!predicate) return fields;
8017
- if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
8018
- else if (predicate.type === "not") predicateFields(predicate.arg, fields);
8072
+ if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields$1(arg, fields);
8073
+ else if (predicate.type === "not") predicateFields$1(predicate.arg, fields);
8019
8074
  else fields.add(predicate.field);
8020
8075
  return fields;
8021
8076
  }
@@ -8028,8 +8083,8 @@ function fieldExpression(field) {
8028
8083
  }
8029
8084
  function compileClickHouseSpanQuery(plan) {
8030
8085
  const parameters = new Parameters();
8031
- const from = parameters.add(plan.timeRange.from, true);
8032
- const to = parameters.add(plan.timeRange.to, true);
8086
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8087
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8033
8088
  const scope = [];
8034
8089
  if (plan.scope) {
8035
8090
  scope.push(`organizationId = ${parameters.add(plan.scope.organizationId)}`);
@@ -8042,10 +8097,11 @@ function compileClickHouseSpanQuery(plan) {
8042
8097
  const selectionFields = [.../* @__PURE__ */ new Set([
8043
8098
  "startedAt",
8044
8099
  "endedAt",
8045
- ...predicateFields(plan.where)
8100
+ plan.orderBy.field,
8101
+ ...predicateFields$1(plan.where)
8046
8102
  ])].filter((field) => !new Set(identityFields).has(field));
8047
8103
  const expressions = selectionFields.map(fieldExpression);
8048
- const after = afterCursor(plan, parameters);
8104
+ const after = afterCursor$1(plan, parameters);
8049
8105
  return {
8050
8106
  query: `WITH candidates AS (
8051
8107
  SELECT ${identities}, ${selectionFields.map((field, index) => `${expressions[index]} AS ${field}`).join(", ")}
@@ -8055,13 +8111,14 @@ function compileClickHouseSpanQuery(plan) {
8055
8111
  ORDER BY ${identities}, e.endedAt DESC
8056
8112
  LIMIT 1 BY ${identities}
8057
8113
  )
8058
- SELECT ${[
8114
+ SELECT ${[.../* @__PURE__ */ new Set([
8059
8115
  ...identityFields,
8060
8116
  "startedAt",
8061
- "endedAt"
8062
- ].map((field) => `s.${field}`).join(", ")}
8117
+ "endedAt",
8118
+ plan.orderBy.field
8119
+ ])].map((field) => `s.${field}`).join(", ")}
8063
8120
  FROM candidates s WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate.sql}) AND ${after}
8064
- ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8121
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, ${identityFields.map((field) => `s.${field} ASC NULLS LAST`).join(", ")}
8065
8122
  LIMIT ${parameters.add(plan.limit + 1)}`,
8066
8123
  query_params: {
8067
8124
  ...parameters.values,
@@ -8070,7 +8127,7 @@ function compileClickHouseSpanQuery(plan) {
8070
8127
  };
8071
8128
  }
8072
8129
  const LEGACY_PAYLOAD_BATCH_ROWS = 32;
8073
- function iso(value) {
8130
+ function iso$1(value) {
8074
8131
  return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
8075
8132
  }
8076
8133
  async function querySpans(client, plan, timeoutMs) {
@@ -8085,8 +8142,8 @@ async function querySpans(client, plan, timeoutMs) {
8085
8142
  };
8086
8143
  const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
8087
8144
  ...row,
8088
- startedAt: iso(row.startedAt),
8089
- endedAt: iso(row.endedAt)
8145
+ startedAt: iso$1(row.startedAt),
8146
+ endedAt: iso$1(row.endedAt)
8090
8147
  }));
8091
8148
  const page = selected.slice(0, plan.limit);
8092
8149
  if (!page.length) return {
@@ -8124,11 +8181,12 @@ async function querySpans(client, plan, timeoutMs) {
8124
8181
  const hydrated = page.flatMap((identity) => {
8125
8182
  const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
8126
8183
  if (!row) return [];
8127
- const { inputPreview: _input, outputPreview: _output, errorPreview: _error, ...metadata } = row;
8184
+ const { inputPreview: _input, outputPreview: _output, errorPreview: _error, timeToFirstChunkMs, ...metadata } = row;
8128
8185
  return [{
8129
8186
  ...metadata,
8130
8187
  ...identity,
8131
- durationMs: Number(row.durationMs)
8188
+ durationMs: Number(row.durationMs),
8189
+ ...timeToFirstChunkSupported && { timeToFirstChunkMs: timeToFirstChunkMs == null ? null : Number(timeToFirstChunkMs) }
8132
8190
  }];
8133
8191
  });
8134
8192
  let metrics = [];
@@ -8152,16 +8210,313 @@ async function querySpans(client, plan, timeoutMs) {
8152
8210
  resourceId: last.resourceId,
8153
8211
  traceId: last.traceId,
8154
8212
  spanId: last.spanId,
8155
- sortValue: last[plan.orderBy.field]
8213
+ sortValue: last[plan.orderBy.field] ?? null
8156
8214
  }) : null;
8157
8215
  return result;
8158
8216
  }
8159
8217
  //#endregion
8218
+ //#region src/storage/domains/observability/v-next/thread-query.ts
8219
+ const contextFields = [
8220
+ "resourceId",
8221
+ "userId",
8222
+ "entityType",
8223
+ "entityId",
8224
+ "entityName",
8225
+ "entityVersionId",
8226
+ "runId",
8227
+ "sessionId",
8228
+ "requestId",
8229
+ "serviceName",
8230
+ "experimentId",
8231
+ "environment",
8232
+ "executionSource"
8233
+ ];
8234
+ function predicateFields(predicate, fields = /* @__PURE__ */ new Set()) {
8235
+ if (!predicate) return fields;
8236
+ if (predicate.type === "boolean") for (const arg of predicate.args) predicateFields(arg, fields);
8237
+ else if (predicate.type === "not") predicateFields(predicate.arg, fields);
8238
+ else fields.add(predicate.field);
8239
+ return fields;
8240
+ }
8241
+ function afterCursor(plan, parameters) {
8242
+ if (!plan.cursor) return "1";
8243
+ const { cursor } = plan;
8244
+ const stamp = parameters.add(cursor.sortValue, "DateTime64(3, 'UTC')");
8245
+ const field = `s.${plan.orderBy.field}`;
8246
+ const terms = [`${field} ${plan.orderBy.direction === "asc" ? ">" : "<"} ${stamp}`];
8247
+ const equal = [`${field} = ${stamp}`];
8248
+ for (const key of ["organizationId", "threadId"]) if (cursor[key] === null) equal.push(`isNull(s.${key})`);
8249
+ else {
8250
+ const value = parameters.add(cursor[key], "String");
8251
+ terms.push(`(${equal.join(" AND ")} AND (s.${key} > ${value} OR isNull(s.${key})))`);
8252
+ equal.push(`s.${key} = ${value}`);
8253
+ }
8254
+ return `(${terms.join(" OR ")})`;
8255
+ }
8256
+ const THREAD_QUERY_EXECUTION_LIMITS = {
8257
+ memoryLimitBytes: 512 * 1024 * 1024,
8258
+ maxThreads: 2,
8259
+ maxBlockSize: 4096,
8260
+ spillThresholdBytes: 32 * 1024 * 1024
8261
+ };
8262
+ const MAX_CANDIDATE_THREADS = 16384;
8263
+ const MAX_CANDIDATE_TRACES = 1e5;
8264
+ const MAX_CANDIDATE_ID_BYTES = 4 * 1024 * 1024;
8265
+ const MAX_FULL_HISTORY_ROOTS = 25e4;
8266
+ function canUseThreadCandidates(probe) {
8267
+ return Number(probe.threadCount) <= MAX_CANDIDATE_THREADS && Number(probe.traceCount) <= MAX_CANDIDATE_TRACES && Number(probe.threadBytes) + Number(probe.traceBytes) <= MAX_CANDIDATE_ID_BYTES;
8268
+ }
8269
+ const identity = (field) => `tuple(isNull(organizationId), ifNull(organizationId, ''), ${field})`;
8270
+ const candidateThreadsFilter = `${identity("threadId")} IN (SELECT ${identity("threadId")} FROM candidate_threads)`;
8271
+ const candidateTracesFilter = `${identity("traceId")} IN (SELECT ${identity("traceId")} FROM candidate_traces)`;
8272
+ function candidateCtes(scope, from, to, plan, probe = false) {
8273
+ const timestamp = plan.timeRange.mode === "threads" ? plan.timeRange.field : "startedAt";
8274
+ const scoped = scope.length ? `${scope.join(" AND ")} AND ` : "";
8275
+ return `candidate_threads AS (
8276
+ SELECT DISTINCT organizationId, threadId FROM ${TABLE_TRACE_ROOTS}
8277
+ WHERE ${scoped}threadId IS NOT NULL AND ${timestamp} >= ${from} AND ${timestamp} < ${to}
8278
+ ${probe ? `LIMIT 16385` : ""}
8279
+ ), candidate_traces AS (
8280
+ SELECT DISTINCT organizationId, traceId FROM ${TABLE_TRACE_ROOTS}
8281
+ WHERE ${scoped}${candidateThreadsFilter}
8282
+ ${probe ? `LIMIT 100001` : ""}
8283
+ )`;
8284
+ }
8285
+ function compileClickHouseThreadRowsQuery(plan, strategy = "full") {
8286
+ const parameters = new ParameterBuilder();
8287
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8288
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8289
+ const scope = [];
8290
+ if (plan.scope) {
8291
+ scope.push(`organizationId = ${parameters.add(plan.scope.organizationId, "String")}`);
8292
+ if (plan.scope.resourceId !== void 0) scope.push(`resourceId = ${parameters.add(plan.scope.resourceId, "String")}`);
8293
+ }
8294
+ if (strategy === "historyBudget") return {
8295
+ query: `SELECT count() AS rootCount FROM (
8296
+ SELECT 1 FROM ${TABLE_TRACE_ROOTS} ${scope.length ? `WHERE ${scope.join(" AND ")}` : ""}
8297
+ LIMIT 250001
8298
+ )`,
8299
+ query_params: parameters.params
8300
+ };
8301
+ if (strategy === "probe") return {
8302
+ query: `WITH ${candidateCtes(scope, from, to, plan, true)}
8303
+ SELECT (SELECT count() FROM candidate_threads) AS threadCount,
8304
+ (SELECT sum(length(ifNull(organizationId, '')) + length(threadId)) FROM candidate_threads) AS threadBytes,
8305
+ count() AS traceCount, sum(length(ifNull(organizationId, '')) + length(traceId)) AS traceBytes
8306
+ FROM candidate_traces`,
8307
+ query_params: parameters.params
8308
+ };
8309
+ const predicate = plan.where ? compileThreadQueryPredicate(plan.where, parameters).sql : "1";
8310
+ const after = afterCursor(plan, parameters);
8311
+ const qualify = plan.timeRange.mode === "traces" ? "s.inWindow > 0" : `s.${plan.timeRange.field} >= ${from} AND s.${plan.timeRange.field} < ${to}`;
8312
+ const filtered = predicateFields(plan.where);
8313
+ const firstContext = [...contextFields, "timeToFirstChunkMs"].filter((field) => field === "resourceId" || filtered.has(field));
8314
+ const firstFields = [
8315
+ "startedAt",
8316
+ "traceId",
8317
+ "spanId",
8318
+ "endedAt",
8319
+ ...firstContext
8320
+ ];
8321
+ const lastFields = [
8322
+ "startedAt",
8323
+ "traceId",
8324
+ "spanId",
8325
+ "endedAt",
8326
+ "resourceId",
8327
+ ...filtered.has("status") ? ["status"] : []
8328
+ ];
8329
+ const rootDataFields = [
8330
+ "threadId",
8331
+ "spanId",
8332
+ "startedAt",
8333
+ "endedAt",
8334
+ ...firstContext,
8335
+ ...filtered.has("status") ? ["status"] : []
8336
+ ];
8337
+ const rootExpression = (field) => field === "timeToFirstChunkMs" ? "if(spanType = 'agent_run', timeToFirstChunkMs, NULL)" : field === "status" ? "if(isNull(error), 'success', 'error')" : field;
8338
+ const sortedRoots = `roots AS (
8339
+ SELECT organizationId, traceId, ${rootDataFields.map((field) => `${rootExpression(field)} AS ${field}`).join(", ")}
8340
+ FROM ${TABLE_TRACE_ROOTS} ${scope.length || strategy === "candidates" ? `WHERE ${[...scope, ...strategy === "candidates" ? [candidateTracesFilter] : []].join(" AND ")}` : ""}
8341
+ ORDER BY organizationId, traceId, startedAt ASC, spanId, endedAt DESC, isNotNull(threadId) DESC, dedupeKey
8342
+ LIMIT 1 BY organizationId, traceId
8343
+ )`;
8344
+ return {
8345
+ query: `WITH ${strategy === "candidates" ? `${candidateCtes(scope, from, to, plan)}, ` : ""}${sortedRoots}, grouped AS (
8346
+ SELECT organizationId, threadId, min(startedAt) AS firstActivity, max(endedAt) AS lastActivity,
8347
+ count() AS traceCount, countIf(startedAt >= ${from} AND startedAt < ${to}) AS inWindow,
8348
+ min(tuple(${firstFields.join(", ")})) AS first,
8349
+ max(tuple(${lastFields.join(", ")})) AS last
8350
+ FROM roots WHERE threadId IS NOT NULL ${strategy === "candidates" ? `AND ${candidateThreadsFilter}` : ""} GROUP BY organizationId, threadId
8351
+ ), rows AS (
8352
+ SELECT organizationId, threadId, firstActivity AS startedAt, lastActivity AS endedAt, traceCount, inWindow,
8353
+ ${firstFields.map((field, i) => `first.${i + 1} AS ${i < 4 ? `first${field[0].toUpperCase()}${field.slice(1)}` : field}`).join(", ")},
8354
+ ${lastFields.map((field, i) => `last.${i + 1} AS ${field === "status" ? "status" : `last${field[0].toUpperCase()}${field.slice(1)}`}`).join(", ")}
8355
+ FROM grouped
8356
+ ) SELECT * FROM rows s WHERE ${qualify} AND (${predicate}) AND ${after}
8357
+ ORDER BY s.${plan.orderBy.field} ${plan.orderBy.direction === "asc" ? "ASC" : "DESC"}, s.organizationId ASC NULLS LAST, s.threadId ASC
8358
+ LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
8359
+ query_params: parameters.params
8360
+ };
8361
+ }
8362
+ const rootKey = (row) => JSON.stringify([
8363
+ row.organizationId,
8364
+ row.traceId,
8365
+ row.spanId
8366
+ ]);
8367
+ const iso = (value) => new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
8368
+ /** Bound HTTP form parameter size as well as payload rows. Long IDs must not break large pages. */
8369
+ function* batches(rows, maxRows = 256) {
8370
+ let batch = [];
8371
+ let bytes = 0;
8372
+ for (const row of rows) {
8373
+ const size = Buffer.byteLength(JSON.stringify(row), "utf8") * 2;
8374
+ if (batch.length && (batch.length >= maxRows || bytes + size > 48 * 1024)) {
8375
+ yield batch;
8376
+ batch = [];
8377
+ bytes = 0;
8378
+ }
8379
+ batch.push(row);
8380
+ bytes += size;
8381
+ }
8382
+ if (batch.length) yield batch;
8383
+ }
8384
+ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8385
+ const deadline = performance.now() + coreStorage.resolveTraceQueryTimeoutMs(timeoutMs);
8386
+ const run = async (compiled) => {
8387
+ const remaining = Math.floor(deadline - performance.now());
8388
+ if (remaining <= 0) throw new coreStorage.TraceQueryExecutionError();
8389
+ return await runWithClickHouseTraceQueryTimeout(client, {
8390
+ timeoutMs: remaining,
8391
+ ...THREAD_QUERY_EXECUTION_LIMITS
8392
+ }, compiled);
8393
+ };
8394
+ if (strategy === "auto") {
8395
+ const [probe] = await run(compileClickHouseThreadRowsQuery(plan, "probe"));
8396
+ if (Number(probe.threadCount) === 0) return {
8397
+ threads: [],
8398
+ page: { next: null }
8399
+ };
8400
+ strategy = canUseThreadCandidates(probe) ? "candidates" : "full";
8401
+ }
8402
+ if (strategy === "full") {
8403
+ const [budget] = await run(compileClickHouseThreadRowsQuery(plan, "historyBudget"));
8404
+ if (Number(budget.rootCount) > MAX_FULL_HISTORY_ROOTS) throw new coreStorage.TraceQueryResourceLimitError();
8405
+ }
8406
+ const selected = await run(compileClickHouseThreadRowsQuery(plan, strategy));
8407
+ const page = selected.slice(0, plan.limit);
8408
+ if (!page.length) return {
8409
+ threads: [],
8410
+ page: { next: null }
8411
+ };
8412
+ const roots = /* @__PURE__ */ new Map();
8413
+ const representative = (row, which) => ({
8414
+ organizationId: row.organizationId,
8415
+ resourceId: which === "first" ? row.resourceId : row.lastResourceId,
8416
+ traceId: row[`${which}TraceId`],
8417
+ spanId: row[`${which}SpanId`],
8418
+ startedAt: row[`${which}StartedAt`],
8419
+ endedAt: row[`${which}EndedAt`]
8420
+ });
8421
+ for (const row of page) for (const which of ["first", "last"]) {
8422
+ const root = representative(row, which);
8423
+ roots.set(rootKey(root), root);
8424
+ }
8425
+ const keys = (batch) => batch.map((root) => new TupleParam([
8426
+ root.organizationId === null ? 1 : 0,
8427
+ root.organizationId ?? "",
8428
+ root.resourceId === null ? 1 : 0,
8429
+ root.resourceId ?? "",
8430
+ root.traceId,
8431
+ root.spanId,
8432
+ iso(root.startedAt).replace("T", " ").replace(/Z$/, ""),
8433
+ iso(root.endedAt).replace("T", " ").replace(/Z$/, "")
8434
+ ]));
8435
+ const rootWhere = `tuple(isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''),
8436
+ traceId, spanId, startedAt, endedAt) IN {roots:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC'), DateTime64(3, 'UTC')))}`;
8437
+ const stored = [];
8438
+ for (const batch of batches([...roots.values()])) stored.push(...await run({
8439
+ query: `SELECT organizationId, traceId, spanId, startedAt, endedAt, spanType,
8440
+ ${contextFields.join(", ")}, if(spanType = 'agent_run', timeToFirstChunkMs, NULL) AS timeToFirstChunkMs,
8441
+ if(isNull(error), 'success', 'error') AS status, ${PREVIEW_COLUMNS.join(", ")}
8442
+ FROM ${TABLE_TRACE_ROOTS} WHERE ${rootWhere}
8443
+ ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8444
+ query_params: { roots: keys(batch) }
8445
+ }));
8446
+ const byRoot = new Map(stored.map((row) => [rootKey(row), row]));
8447
+ const missing = stored.filter(needsPreviewPayloads);
8448
+ for (const batch of batches(missing, 32)) {
8449
+ const payloads = await run({
8450
+ query: `SELECT organizationId, traceId, spanId, input, output, error FROM ${TABLE_TRACE_ROOTS}
8451
+ WHERE ${rootWhere} ORDER BY isNotNull(threadId) DESC LIMIT 1 BY organizationId, traceId, spanId`,
8452
+ query_params: { roots: keys(batch) }
8453
+ });
8454
+ for (const payload of payloads) {
8455
+ const row = byRoot.get(rootKey(payload));
8456
+ if (row) Object.assign(row, resolveStoredPreviews(row, payload));
8457
+ }
8458
+ }
8459
+ const firstRoots = page.map((row) => representative(row, "first")).filter((root) => byRoot.get(rootKey(root))?.spanType === "agent_run");
8460
+ const models = [];
8461
+ for (const batch of batches(firstRoots)) models.push(...await run({
8462
+ query: `SELECT organizationId, traceId,
8463
+ if(JSONType(attributes, 'model') = 'String', JSONExtractString(attributes, 'model'), NULL) AS model,
8464
+ if(JSONType(attributes, 'provider') = 'String', JSONExtractString(attributes, 'provider'), NULL) AS provider
8465
+ FROM ${TABLE_SPAN_EVENTS} WHERE spanType = 'model_generation' AND
8466
+ tuple(isNull(organizationId), ifNull(organizationId, ''), isNull(resourceId), ifNull(resourceId, ''), traceId, parentSpanId)
8467
+ IN {agents:Array(Tuple(UInt8, String, UInt8, String, String, String))}
8468
+ ORDER BY organizationId, traceId, startedAt, spanId LIMIT 1 BY organizationId, traceId`,
8469
+ query_params: { agents: batch.map((root) => new TupleParam([
8470
+ root.organizationId === null ? 1 : 0,
8471
+ root.organizationId ?? "",
8472
+ root.resourceId === null ? 1 : 0,
8473
+ root.resourceId ?? "",
8474
+ root.traceId,
8475
+ root.spanId
8476
+ ])) }
8477
+ }));
8478
+ const byTrace = new Map(models.map((row) => [JSON.stringify([row.organizationId, row.traceId]), row]));
8479
+ const threads = page.flatMap((row) => {
8480
+ const first = byRoot.get(rootKey(representative(row, "first")));
8481
+ const last = byRoot.get(rootKey(representative(row, "last")));
8482
+ if (!first || !last) return [];
8483
+ const model = byTrace.get(JSON.stringify([row.organizationId, row.firstTraceId]));
8484
+ return [coreStorage.threadQueryRowSchema.parse({
8485
+ organizationId: row.organizationId,
8486
+ threadId: row.threadId,
8487
+ firstTraceId: row.firstTraceId,
8488
+ lastTraceId: row.lastTraceId,
8489
+ ...Object.fromEntries(contextFields.map((field) => [field, first[field]])),
8490
+ status: last.status,
8491
+ startedAt: iso(row.startedAt),
8492
+ endedAt: iso(row.endedAt),
8493
+ traceCount: Number(row.traceCount),
8494
+ timeToFirstChunkMs: first.timeToFirstChunkMs,
8495
+ model: model?.model ?? null,
8496
+ provider: model?.provider ?? null,
8497
+ inputPreview: resolveStoredPreviews(first).inputPreview,
8498
+ outputPreview: resolveStoredPreviews(last).outputPreview,
8499
+ errorPreview: resolveStoredPreviews(last).errorPreview
8500
+ })];
8501
+ });
8502
+ const last = page.at(-1);
8503
+ return {
8504
+ threads,
8505
+ page: { next: selected.length > plan.limit ? coreStorage.encodeThreadQueryCursor(plan, {
8506
+ organizationId: last.organizationId,
8507
+ threadId: last.threadId,
8508
+ sortValue: iso(last[plan.orderBy.field])
8509
+ }) : null }
8510
+ };
8511
+ }
8512
+ //#endregion
8160
8513
  //#region src/storage/domains/observability/v-next/trace-aggregate.ts
8161
8514
  /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
8162
8515
  const DIMENSION_SQL = {
8163
8516
  entityType: "r.entityType",
8164
8517
  entityName: "r.entityName",
8518
+ name: "r.name",
8519
+ spanType: "r.spanType",
8165
8520
  environment: "r.environment",
8166
8521
  status: TRACE_STATUS_SQL,
8167
8522
  serviceName: "r.serviceName",
@@ -9305,6 +9660,7 @@ function buildBranchCursor(row) {
9305
9660
  //#endregion
9306
9661
  //#region src/storage/domains/observability/v-next/index.ts
9307
9662
  const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query", "span-query-context-fields"] : [];
9663
+ const threadQueryFeatures = typeof coreStorage.planThreadRowsQuery === "function" ? ["thread-query-rows"] : [];
9308
9664
  function buildSignalMigrationRequiredMessage(args) {
9309
9665
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
9310
9666
  return `
@@ -9742,8 +10098,11 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
9742
10098
  "trace-query-tenant-scope",
9743
10099
  "feedback",
9744
10100
  "trace-query-context-ids",
10101
+ ...timeToFirstChunkFeatures,
10102
+ "trace-query-root-span-fields",
9745
10103
  "trace-aggregate",
9746
- ...spanQueryFeatures
10104
+ ...spanQueryFeatures,
10105
+ ...threadQueryFeatures
9747
10106
  ];
9748
10107
  return [
9749
10108
  "metrics",
@@ -9762,8 +10121,11 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
9762
10121
  "trace-query-tenant-scope",
9763
10122
  "feedback",
9764
10123
  "trace-query-context-ids",
10124
+ ...timeToFirstChunkFeatures,
10125
+ "trace-query-root-span-fields",
9765
10126
  "trace-aggregate",
9766
- ...spanQueryFeatures
10127
+ ...spanQueryFeatures,
10128
+ ...threadQueryFeatures
9767
10129
  ];
9768
10130
  }
9769
10131
  async createSpan(args) {
@@ -9940,6 +10302,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
9940
10302
  }
9941
10303
  async queryThreads(plan) {
9942
10304
  try {
10305
+ if (plan.result === "threadRows") return await queryThreadRows(this.#client, plan, this.#traceQueryTimeoutMs);
9943
10306
  return await queryThreads(this.#client, plan, this.#traceQueryTimeoutMs);
9944
10307
  } catch (error) {
9945
10308
  if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;