@volcengine/tls-observer-cursor 0.0.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/LICENCE +13 -0
- package/README.md +80 -0
- package/dist/config.d.ts +26 -0
- package/dist/exporter.d.ts +10 -0
- package/dist/hooks/compact.d.ts +3 -0
- package/dist/hooks/dispatch.d.ts +3 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/stop.d.ts +4 -0
- package/dist/hooks/subagent-stop.d.ts +4 -0
- package/dist/hooks/tool-use.d.ts +4 -0
- package/dist/hooks/trace-flow.d.ts +34 -0
- package/dist/hooks/user-prompt-submit.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2097 -0
- package/dist/state.d.ts +6 -0
- package/dist/stdin.d.ts +1 -0
- package/dist/tls-producer.d.ts +12 -0
- package/dist/trace.d.ts +42 -0
- package/dist/transcript.d.ts +17 -0
- package/dist/types.d.ts +228 -0
- package/hooks/hooks.json +24 -0
- package/package.json +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
import { homedir, hostname, networkInterfaces, userInfo } from "node:os";
|
|
2
|
+
import { tlsOpenapi } from "@volcengine/openapi";
|
|
3
|
+
import { createHash as external_node_crypto_createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import * as __rspack_external_node_fs_promises_153e37e0 from "node:fs/promises";
|
|
5
|
+
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
6
|
+
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
7
|
+
function firstEnv(...names) {
|
|
8
|
+
for (const name of names){
|
|
9
|
+
const value = process.env[name];
|
|
10
|
+
if ('string' == typeof value && '' !== value.trim()) return value.trim();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function envFlag(name, defaultValue = false) {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
if (void 0 === value || '' === value) return defaultValue;
|
|
16
|
+
return /^(1|true|yes|y|on)$/i.test(value);
|
|
17
|
+
}
|
|
18
|
+
function envDisabled(name) {
|
|
19
|
+
return /^(0|false|no|n|off)$/i.test(process.env[name] ?? '');
|
|
20
|
+
}
|
|
21
|
+
function envPositiveInteger(name, fallback) {
|
|
22
|
+
const value = Number.parseInt(process.env[name] ?? '', 10);
|
|
23
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
24
|
+
}
|
|
25
|
+
function defaultDataRoot() {
|
|
26
|
+
return __rspack_external_node_path_c5b9b54f.join(homedir(), '.cursor', 'plugins', 'data', 'tls-observer-cursor');
|
|
27
|
+
}
|
|
28
|
+
function defaultEnvFile() {
|
|
29
|
+
return __rspack_external_node_path_c5b9b54f.join(homedir(), '.cursor', 'tls-observer-cursor.env');
|
|
30
|
+
}
|
|
31
|
+
function parseEnvValue(raw) {
|
|
32
|
+
const value = raw.trim();
|
|
33
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function loadEnvFile(filePath) {
|
|
37
|
+
const target = filePath || firstEnv('CURSOR_TLS_ENV_FILE') || defaultEnvFile();
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = __rspack_external_node_fs_5ea92f0c.readFileSync(target, 'utf8');
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (const rawLine of text.split(/\r?\n/)){
|
|
45
|
+
const line = rawLine.trim();
|
|
46
|
+
if (!line || line.startsWith('#')) continue;
|
|
47
|
+
const normalized = line.startsWith('export ') ? line.slice(7).trim() : line;
|
|
48
|
+
const match = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
49
|
+
if (!match) continue;
|
|
50
|
+
const [, key, rawValue] = match;
|
|
51
|
+
process.env[key] ??= parseEnvValue(rawValue);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function loadConfig() {
|
|
56
|
+
loadEnvFile();
|
|
57
|
+
const dataRoot = firstEnv('CURSOR_TLS_DATA_ROOT') || defaultDataRoot();
|
|
58
|
+
const serviceName = firstEnv('CURSOR_OTEL_SERVICE_NAME') || "cursor-transcript-hook";
|
|
59
|
+
const exportRetries = Math.min(envPositiveInteger('CURSOR_TLS_EXPORT_TIMEOUT_RETRIES', 1), 3);
|
|
60
|
+
return {
|
|
61
|
+
enabled: !envDisabled('CURSOR_TLS_EXPORT'),
|
|
62
|
+
dataRoot,
|
|
63
|
+
stateFilePath: firstEnv('CURSOR_TLS_STATE_FILE') || __rspack_external_node_path_c5b9b54f.join(dataRoot, 'state', 'cursor-state.json'),
|
|
64
|
+
sendLogsDir: __rspack_external_node_path_c5b9b54f.join(dataRoot, 'logs', 'send'),
|
|
65
|
+
artifactsDir: __rspack_external_node_path_c5b9b54f.join(dataRoot, 'artifacts'),
|
|
66
|
+
diagnosticLogMaxBytes: envPositiveInteger('CURSOR_TLS_DIAGNOSTIC_LOG_MAX_BYTES', 5242880),
|
|
67
|
+
captureContent: !envDisabled('CURSOR_TLS_CAPTURE_CONTENT'),
|
|
68
|
+
debugArtifacts: envFlag('CURSOR_TLS_DEBUG_ARTIFACTS', false),
|
|
69
|
+
serviceName,
|
|
70
|
+
serviceInstanceId: `${serviceName}@${hostname()}:${process.pid}`,
|
|
71
|
+
tlsEndpoint: firstEnv('CURSOR_TLS_ENDPOINT', 'TRACE_TLS_ENDPOINT', 'TLS_ENDPOINT'),
|
|
72
|
+
otelEndpoint: firstEnv('CURSOR_TLS_OTEL_ENDPOINT', 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'),
|
|
73
|
+
region: firstEnv('CURSOR_TLS_REGION', 'TLS_REGION'),
|
|
74
|
+
traceTopicId: firstEnv('CURSOR_TLS_TRACE_TOPIC_ID', 'CURSOR_TLS_TOPIC_ID', 'TLS_TRACE_TOPIC_ID'),
|
|
75
|
+
apiKey: firstEnv('CURSOR_TLS_API_KEY', 'TLS_API_KEY'),
|
|
76
|
+
ak: firstEnv('CURSOR_TLS_AK', 'TLS_AK'),
|
|
77
|
+
sk: firstEnv('CURSOR_TLS_SK', 'TLS_SK'),
|
|
78
|
+
authMode: 'resource' === firstEnv('CURSOR_TLS_AUTH_MODE', 'TLS_AUTH_MODE') ? 'resource' : 'header',
|
|
79
|
+
exportTimeoutMs: envPositiveInteger('CURSOR_TLS_EXPORT_TIMEOUT_MS', 20000),
|
|
80
|
+
exportTimeoutRetries: exportRetries,
|
|
81
|
+
transcriptLookbehindBytes: envPositiveInteger('CURSOR_TLS_TRANSCRIPT_LOOKBEHIND_BYTES', 4194304)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function localIdentityMetadata() {
|
|
85
|
+
return {
|
|
86
|
+
'user.name': userInfo().username
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function json_safeJsonStringify(value, fallback = '{}') {
|
|
90
|
+
const seen = new WeakSet();
|
|
91
|
+
try {
|
|
92
|
+
return JSON.stringify(value, (_key, item)=>{
|
|
93
|
+
if (item && 'object' == typeof item) {
|
|
94
|
+
if (seen.has(item)) return '[Circular]';
|
|
95
|
+
seen.add(item);
|
|
96
|
+
}
|
|
97
|
+
return item;
|
|
98
|
+
}) ?? fallback;
|
|
99
|
+
} catch {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function compactObject(value) {
|
|
104
|
+
return Object.fromEntries(Object.entries(value).filter(([, item])=>null != item && '' !== item));
|
|
105
|
+
}
|
|
106
|
+
const TLS_ANONYMOUS_IDENTITY_HEADER = 'x-tls-anonymous-identity';
|
|
107
|
+
const NO_RETRY_STATUS_CODES = [
|
|
108
|
+
400,
|
|
109
|
+
401,
|
|
110
|
+
403,
|
|
111
|
+
404
|
|
112
|
+
];
|
|
113
|
+
const producerEntries = new Map();
|
|
114
|
+
function firstExternalIpv4() {
|
|
115
|
+
for (const items of Object.values(networkInterfaces()))for (const item of items ?? [])if ('IPv4' === item.family && !item.internal && item.address) return item.address;
|
|
116
|
+
return '127.0.0.1';
|
|
117
|
+
}
|
|
118
|
+
function normalizeProducerEndpoint(endpoint) {
|
|
119
|
+
const raw = endpoint.trim();
|
|
120
|
+
if (!raw) return raw;
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(/^https?:\/\//.test(raw) ? raw : `https://${raw}`);
|
|
123
|
+
return url.hostname || raw;
|
|
124
|
+
} catch {
|
|
125
|
+
return raw.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/:\d+$/, '');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function producerKey(config) {
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
endpoint: normalizeProducerEndpoint(config.endpoint),
|
|
131
|
+
region: config.region,
|
|
132
|
+
apiKey: config.apiKey || '',
|
|
133
|
+
ak: config.ak || '',
|
|
134
|
+
sk: config.sk || ''
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function timeoutError(label, timeoutMs) {
|
|
138
|
+
return new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
139
|
+
}
|
|
140
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
141
|
+
const effectiveTimeoutMs = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? Math.trunc(Number(timeoutMs)) : 20000;
|
|
142
|
+
return new Promise((resolve, reject)=>{
|
|
143
|
+
const timer = setTimeout(()=>reject(timeoutError(label, effectiveTimeoutMs)), effectiveTimeoutMs);
|
|
144
|
+
promise.then((value)=>{
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
resolve(value);
|
|
147
|
+
}, (error)=>{
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
reject(error);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function getProducer(config) {
|
|
154
|
+
const key = producerKey(config);
|
|
155
|
+
const existing = producerEntries.get(key);
|
|
156
|
+
if (existing) return existing;
|
|
157
|
+
const producerConfig = tlsOpenapi.Producer.getDefaultProducerConfig();
|
|
158
|
+
producerConfig.Endpoint = normalizeProducerEndpoint(config.endpoint);
|
|
159
|
+
producerConfig.Region = config.region;
|
|
160
|
+
producerConfig.NoRetryStatusCodeList = NO_RETRY_STATUS_CODES;
|
|
161
|
+
if (config.apiKey) {
|
|
162
|
+
producerConfig.AccessKeyID = '';
|
|
163
|
+
producerConfig.AccessKeySecret = '';
|
|
164
|
+
producerConfig.Headers = {
|
|
165
|
+
[TLS_ANONYMOUS_IDENTITY_HEADER]: config.apiKey
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
producerConfig.AccessKeyID = config.ak || '';
|
|
169
|
+
producerConfig.AccessKeySecret = config.sk || '';
|
|
170
|
+
}
|
|
171
|
+
const producer = new tlsOpenapi.Producer(producerConfig);
|
|
172
|
+
producer.Start();
|
|
173
|
+
const entry = {
|
|
174
|
+
producer,
|
|
175
|
+
source: firstExternalIpv4(),
|
|
176
|
+
timeoutMs: config.timeoutMs ?? 20000
|
|
177
|
+
};
|
|
178
|
+
producerEntries.set(key, entry);
|
|
179
|
+
return entry;
|
|
180
|
+
}
|
|
181
|
+
async function sendTlsLogRecords(config, records, fileName) {
|
|
182
|
+
if (0 === records.length) return;
|
|
183
|
+
const entry = getProducer(config);
|
|
184
|
+
const logGroup = {
|
|
185
|
+
Logs: records.map((record)=>({
|
|
186
|
+
Time: Date.now(),
|
|
187
|
+
Contents: Object.entries(record).map(([Key, Value])=>({
|
|
188
|
+
Key,
|
|
189
|
+
Value
|
|
190
|
+
}))
|
|
191
|
+
})),
|
|
192
|
+
Source: entry.source,
|
|
193
|
+
LogTags: [],
|
|
194
|
+
FileName: fileName
|
|
195
|
+
};
|
|
196
|
+
await withTimeout(new Promise((resolve, reject)=>{
|
|
197
|
+
let settled = false;
|
|
198
|
+
const settle = (fn, value)=>{
|
|
199
|
+
if (settled) return;
|
|
200
|
+
settled = true;
|
|
201
|
+
fn(value);
|
|
202
|
+
};
|
|
203
|
+
const resolveUnknown = resolve;
|
|
204
|
+
const sendPromise = entry.producer.SendLogs('', config.traceTopicId, entry.source, fileName, logGroup, {
|
|
205
|
+
Success: ()=>settle(resolveUnknown),
|
|
206
|
+
Fail: (result)=>{
|
|
207
|
+
const attempts = result?.Attempts ?? [];
|
|
208
|
+
const lastAttempt = attempts[attempts.length - 1] ?? {};
|
|
209
|
+
settle(reject, new Error(`TLS SendLogs failed topicId=${config.traceTopicId} requestId=${String(lastAttempt.RequestId ?? '')} errorCode=${String(lastAttempt.ErrorCode ?? '')} error=${JSON.stringify(lastAttempt)}`));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
Promise.resolve(sendPromise).catch((error)=>settle(reject, error));
|
|
213
|
+
}), config.timeoutMs, 'TLS SendLogs');
|
|
214
|
+
}
|
|
215
|
+
async function closeTraceLogProducers() {
|
|
216
|
+
const entries = [
|
|
217
|
+
...producerEntries.values()
|
|
218
|
+
];
|
|
219
|
+
producerEntries.clear();
|
|
220
|
+
await Promise.allSettled(entries.map((entry)=>{
|
|
221
|
+
const closeTimeoutMs = Math.min(Math.max(entry.timeoutMs || 20000, 1000), 5000);
|
|
222
|
+
return withTimeout(Promise.resolve(entry.producer.Close()), closeTimeoutMs, 'TLS Producer Close');
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
var package_namespaceObject = {
|
|
226
|
+
rE: "0.0.4"
|
|
227
|
+
};
|
|
228
|
+
function toOtlpAnyValue(value) {
|
|
229
|
+
if ('string' == typeof value) return {
|
|
230
|
+
stringValue: value
|
|
231
|
+
};
|
|
232
|
+
if ('boolean' == typeof value) return {
|
|
233
|
+
boolValue: value
|
|
234
|
+
};
|
|
235
|
+
if ('number' == typeof value) return Number.isInteger(value) ? {
|
|
236
|
+
intValue: String(value)
|
|
237
|
+
} : {
|
|
238
|
+
doubleValue: value
|
|
239
|
+
};
|
|
240
|
+
if (Array.isArray(value)) return {
|
|
241
|
+
arrayValue: {
|
|
242
|
+
values: value.map((item)=>toOtlpAnyValue(item))
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
if (value && 'object' == typeof value) return {
|
|
246
|
+
kvlistValue: {
|
|
247
|
+
values: Object.entries(value).filter(([, item])=>void 0 !== item).map(([key, item])=>({
|
|
248
|
+
key,
|
|
249
|
+
value: toOtlpAnyValue(item)
|
|
250
|
+
}))
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
stringValue: String(value ?? '')
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function toOtlpAttributes(attributes) {
|
|
258
|
+
return Object.entries(attributes).filter(([, value])=>null != value).map(([key, value])=>({
|
|
259
|
+
key,
|
|
260
|
+
value: toOtlpAnyValue(value)
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
function otlpValueToJs(value) {
|
|
264
|
+
if (void 0 !== value.stringValue) return value.stringValue;
|
|
265
|
+
if (void 0 !== value.intValue) return Number(value.intValue);
|
|
266
|
+
if (void 0 !== value.doubleValue) return value.doubleValue;
|
|
267
|
+
if (void 0 !== value.boolValue) return value.boolValue;
|
|
268
|
+
if (value.arrayValue && 'object' == typeof value.arrayValue) {
|
|
269
|
+
const arr = value.arrayValue;
|
|
270
|
+
return (arr.values || []).map(otlpValueToJs);
|
|
271
|
+
}
|
|
272
|
+
if (value.kvlistValue && 'object' == typeof value.kvlistValue) {
|
|
273
|
+
const kv = value.kvlistValue;
|
|
274
|
+
const result = {};
|
|
275
|
+
for (const item of kv.values || [])result[item.key] = otlpValueToJs(item.value);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
function otlpAttributesToMap(attributes) {
|
|
281
|
+
return attributes.reduce((acc, attr)=>{
|
|
282
|
+
acc[attr.key] = otlpValueToJs(attr.value);
|
|
283
|
+
return acc;
|
|
284
|
+
}, {});
|
|
285
|
+
}
|
|
286
|
+
function toUnixNanoString(ms) {
|
|
287
|
+
return String(BigInt(Math.trunc(Number.isFinite(ms) ? ms : Date.now())) * BigInt(1000000));
|
|
288
|
+
}
|
|
289
|
+
function spanKindToName(kind) {
|
|
290
|
+
const kindMap = {
|
|
291
|
+
0: 'unspecified',
|
|
292
|
+
1: 'internal',
|
|
293
|
+
2: 'server',
|
|
294
|
+
3: 'client',
|
|
295
|
+
4: 'producer',
|
|
296
|
+
5: 'consumer'
|
|
297
|
+
};
|
|
298
|
+
if ('number' == typeof kind) return kindMap[kind] || 'internal';
|
|
299
|
+
const upper = String(kind).toUpperCase();
|
|
300
|
+
if ('SERVER' === upper) return 'server';
|
|
301
|
+
if ('CLIENT' === upper) return 'client';
|
|
302
|
+
return 'internal';
|
|
303
|
+
}
|
|
304
|
+
function statusCodeToName(code) {
|
|
305
|
+
const statusMap = {
|
|
306
|
+
0: 'UNSET',
|
|
307
|
+
1: 'OK',
|
|
308
|
+
2: 'ERROR'
|
|
309
|
+
};
|
|
310
|
+
if (null == code) return 'UNSET';
|
|
311
|
+
const num = Number(code);
|
|
312
|
+
return statusMap[num] ?? 'UNSET';
|
|
313
|
+
}
|
|
314
|
+
function inferSpanTypeFromName(name, explicitType) {
|
|
315
|
+
if ('string' == typeof explicitType && explicitType) return explicitType;
|
|
316
|
+
if ('agent.turn' === name || 'codex_turn' === name) return 'message';
|
|
317
|
+
if ('llm.request' === name) return 'model';
|
|
318
|
+
if ('tool.call' === name) return 'tool';
|
|
319
|
+
}
|
|
320
|
+
function otlpSpanToTlsEnvelope(otlpSpan, resource, scope, options) {
|
|
321
|
+
const status = otlpSpan.status;
|
|
322
|
+
const statusCodeNum = Number(status?.code);
|
|
323
|
+
const isError = 2 === statusCodeNum;
|
|
324
|
+
const attributes = otlpAttributesToMap(otlpSpan.attributes || []);
|
|
325
|
+
const spanType = inferSpanTypeFromName(otlpSpan.name, otlpSpan.type);
|
|
326
|
+
const isTool = 'tool' === spanType;
|
|
327
|
+
if (isTool) attributes['tool.error'] = isError;
|
|
328
|
+
else delete attributes['tool.error'];
|
|
329
|
+
if (isError) {
|
|
330
|
+
attributes['error.type'] = attributes['error.type'] ?? 'error';
|
|
331
|
+
attributes['error.message'] = String(status?.message || '');
|
|
332
|
+
} else {
|
|
333
|
+
attributes['error.type'] = '';
|
|
334
|
+
attributes['error.message'] = '';
|
|
335
|
+
}
|
|
336
|
+
if (options?.filterAttributes) {
|
|
337
|
+
for (const key of Object.keys(attributes))if (!options.filterAttributes(key)) delete attributes[key];
|
|
338
|
+
}
|
|
339
|
+
const resourceAttributes = otlpAttributesToMap(resource.attributes || []);
|
|
340
|
+
const events = (otlpSpan.events || []).map((event)=>({
|
|
341
|
+
timeUnixNano: event.timeUnixNano,
|
|
342
|
+
name: event.name,
|
|
343
|
+
attributes: otlpAttributesToMap(event.attributes || [])
|
|
344
|
+
}));
|
|
345
|
+
const links = (otlpSpan.links || []).map((link)=>({
|
|
346
|
+
traceId: link.traceId,
|
|
347
|
+
spanId: link.spanId,
|
|
348
|
+
traceState: link.traceState,
|
|
349
|
+
attributes: otlpAttributesToMap(link.attributes || [])
|
|
350
|
+
}));
|
|
351
|
+
const startNano = BigInt(String(otlpSpan.startTimeUnixNano));
|
|
352
|
+
const endNano = BigInt(String(otlpSpan.endTimeUnixNano));
|
|
353
|
+
const us = BigInt(1000);
|
|
354
|
+
const durationUs = (endNano - startNano) / us;
|
|
355
|
+
return {
|
|
356
|
+
TraceID: String(otlpSpan.traceId),
|
|
357
|
+
SpanID: String(otlpSpan.spanId),
|
|
358
|
+
ParentSpanID: String(otlpSpan.parentSpanId || ''),
|
|
359
|
+
Name: String(otlpSpan.name),
|
|
360
|
+
Kind: spanKindToName(otlpSpan.kind),
|
|
361
|
+
Start: String(startNano / us),
|
|
362
|
+
End: String(endNano / us),
|
|
363
|
+
Duration: String(durationUs),
|
|
364
|
+
StatusCode: statusCodeToName(statusCodeNum),
|
|
365
|
+
StatusDescription: String(status?.message || ''),
|
|
366
|
+
Attributes: json_safeJsonStringify(attributes),
|
|
367
|
+
Resource: json_safeJsonStringify(resourceAttributes),
|
|
368
|
+
ServiceName: String(resourceAttributes['service.name'] || ''),
|
|
369
|
+
Host: String(resourceAttributes['host.name'] || ''),
|
|
370
|
+
OTLPName: String(scope.name),
|
|
371
|
+
OTLPVersion: String(scope.version),
|
|
372
|
+
Events: json_safeJsonStringify(events),
|
|
373
|
+
Links: json_safeJsonStringify(links),
|
|
374
|
+
TraceState: String(otlpSpan.traceState || '')
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const TLS_AUTH_RESOURCE_KEYS = {
|
|
378
|
+
traceTopicId: 'tls.otel.tracetopic',
|
|
379
|
+
region: 'tls.otel.region'
|
|
380
|
+
};
|
|
381
|
+
const SCHEMA_FIELD_ORDER = [
|
|
382
|
+
'span.name',
|
|
383
|
+
'span.kind',
|
|
384
|
+
'tls.app.type',
|
|
385
|
+
'session.id',
|
|
386
|
+
'span.status.code',
|
|
387
|
+
'start_time_unix_us',
|
|
388
|
+
'end_time_unix_us',
|
|
389
|
+
'span.duration_us',
|
|
390
|
+
'service.name',
|
|
391
|
+
'service.instance.id',
|
|
392
|
+
'host.name',
|
|
393
|
+
'observed_time_unix_ms',
|
|
394
|
+
'telemetry.sdk.name',
|
|
395
|
+
'telemetry.sdk.language',
|
|
396
|
+
'telemetry.sdk.version',
|
|
397
|
+
'otel.scope.name',
|
|
398
|
+
'otel.scope.version',
|
|
399
|
+
'event.name',
|
|
400
|
+
'user.role',
|
|
401
|
+
'gen_ai.operation.name',
|
|
402
|
+
'gen_ai.provider.name',
|
|
403
|
+
'gen_ai.request.model',
|
|
404
|
+
'gen_ai.response.model',
|
|
405
|
+
'gen_ai.request.duration_ms',
|
|
406
|
+
'gen_ai.input.messages',
|
|
407
|
+
'gen_ai.output.messages',
|
|
408
|
+
'gen_ai.usage.input_tokens',
|
|
409
|
+
'gen_ai.usage.output_tokens',
|
|
410
|
+
'gen_ai.usage.reasoning.output_tokens',
|
|
411
|
+
'gen_ai.usage.cache_read.input_tokens',
|
|
412
|
+
'gen_ai.usage.cache_creation.input_tokens',
|
|
413
|
+
'gen_ai.usage.total_tokens',
|
|
414
|
+
'gen_ai.tool.name',
|
|
415
|
+
'gen_ai.tool.call.id',
|
|
416
|
+
'gen_ai.tool.call.arguments',
|
|
417
|
+
'gen_ai.tool.call.result',
|
|
418
|
+
'error.type',
|
|
419
|
+
'error.message'
|
|
420
|
+
];
|
|
421
|
+
function serializeForAttr(value) {
|
|
422
|
+
const serialized = 'string' == typeof value ? value : json_safeJsonStringify(value);
|
|
423
|
+
return String(serialized ?? '').slice(0, 65536);
|
|
424
|
+
}
|
|
425
|
+
function spanKind(span) {
|
|
426
|
+
if ('SERVER' === span.span_kind) return 2;
|
|
427
|
+
if ('CLIENT' === span.span_kind) return 3;
|
|
428
|
+
return 1;
|
|
429
|
+
}
|
|
430
|
+
function spanKindName(span) {
|
|
431
|
+
if ('SERVER' === span.span_kind) return 'server';
|
|
432
|
+
if ('CLIENT' === span.span_kind) return 'client';
|
|
433
|
+
return 'internal';
|
|
434
|
+
}
|
|
435
|
+
function unixMsToUs(ms) {
|
|
436
|
+
return String(1000 * Math.trunc(Number.isFinite(ms) ? ms : Date.now()));
|
|
437
|
+
}
|
|
438
|
+
function hasError(span) {
|
|
439
|
+
if (span.status?.code === 'ERROR') return true;
|
|
440
|
+
return Boolean(span.attributes['error.type']);
|
|
441
|
+
}
|
|
442
|
+
function buildResourceAttributes(trace, config) {
|
|
443
|
+
const attributes = {
|
|
444
|
+
...trace.resource
|
|
445
|
+
};
|
|
446
|
+
if ('resource' === config.authMode) {
|
|
447
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.traceTopicId] = config.traceTopicId;
|
|
448
|
+
attributes[TLS_AUTH_RESOURCE_KEYS.region] = config.region;
|
|
449
|
+
}
|
|
450
|
+
return attributes;
|
|
451
|
+
}
|
|
452
|
+
function cleanSpanAttributesForOtlp(span) {
|
|
453
|
+
const attributes = {
|
|
454
|
+
...span.attributes
|
|
455
|
+
};
|
|
456
|
+
for (const key of [
|
|
457
|
+
'span.name',
|
|
458
|
+
'span.kind',
|
|
459
|
+
'span.status.code',
|
|
460
|
+
'service.name',
|
|
461
|
+
'service.instance.id',
|
|
462
|
+
'host.name',
|
|
463
|
+
'telemetry.sdk.name',
|
|
464
|
+
'telemetry.sdk.language',
|
|
465
|
+
'telemetry.sdk.version'
|
|
466
|
+
])delete attributes[key];
|
|
467
|
+
return attributes;
|
|
468
|
+
}
|
|
469
|
+
function buildOtlpPayload(trace, config) {
|
|
470
|
+
return {
|
|
471
|
+
resourceSpans: [
|
|
472
|
+
{
|
|
473
|
+
schemaUrl: trace.schema_url,
|
|
474
|
+
resource: {
|
|
475
|
+
attributes: toOtlpAttributes(buildResourceAttributes(trace, config))
|
|
476
|
+
},
|
|
477
|
+
scopeSpans: [
|
|
478
|
+
{
|
|
479
|
+
schemaUrl: trace.schema_url,
|
|
480
|
+
scope: {
|
|
481
|
+
name: 'tls-observer-cursor',
|
|
482
|
+
version: package_namespaceObject.rE
|
|
483
|
+
},
|
|
484
|
+
spans: trace.spans.map((span)=>({
|
|
485
|
+
traceId: span.trace_id,
|
|
486
|
+
spanId: span.span_id,
|
|
487
|
+
...span.parent_span_id ? {
|
|
488
|
+
parentSpanId: span.parent_span_id
|
|
489
|
+
} : {},
|
|
490
|
+
name: span.name,
|
|
491
|
+
kind: spanKind(span),
|
|
492
|
+
startTimeUnixNano: toUnixNanoString(span.start_time_unix_ms),
|
|
493
|
+
endTimeUnixNano: toUnixNanoString(span.end_time_unix_ms),
|
|
494
|
+
attributes: toOtlpAttributes(cleanSpanAttributesForOtlp(span)),
|
|
495
|
+
status: hasError(span) ? {
|
|
496
|
+
code: 2,
|
|
497
|
+
message: span.status?.message || 'error'
|
|
498
|
+
} : {
|
|
499
|
+
code: span.status?.code === 'OK' ? 1 : 0
|
|
500
|
+
}
|
|
501
|
+
}))
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function requireExportConfig(config) {
|
|
509
|
+
if (!config.enabled) return {
|
|
510
|
+
ok: false,
|
|
511
|
+
reason: 'disabled'
|
|
512
|
+
};
|
|
513
|
+
const missing = [];
|
|
514
|
+
const endpoint = config.tlsEndpoint || config.otelEndpoint;
|
|
515
|
+
if (!endpoint) missing.push('CURSOR_TLS_ENDPOINT or CURSOR_TLS_OTEL_ENDPOINT');
|
|
516
|
+
if (!config.region) missing.push('CURSOR_TLS_REGION');
|
|
517
|
+
if (!config.traceTopicId) missing.push('CURSOR_TLS_TRACE_TOPIC_ID');
|
|
518
|
+
if (!config.apiKey && (!config.ak || !config.sk)) missing.push('CURSOR_TLS_API_KEY or CURSOR_TLS_AK/CURSOR_TLS_SK');
|
|
519
|
+
if (config.apiKey && (config.ak || config.sk)) return {
|
|
520
|
+
ok: false,
|
|
521
|
+
reason: 'invalid_auth',
|
|
522
|
+
missing: [
|
|
523
|
+
'use apiKey or ak/sk, not both'
|
|
524
|
+
]
|
|
525
|
+
};
|
|
526
|
+
if (missing.length > 0) return {
|
|
527
|
+
ok: false,
|
|
528
|
+
reason: 'missing_config',
|
|
529
|
+
missing
|
|
530
|
+
};
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
url: endpoint
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function exporter_producerConfig(config) {
|
|
537
|
+
return {
|
|
538
|
+
endpoint: config.tlsEndpoint || config.otelEndpoint || '',
|
|
539
|
+
region: config.region || '',
|
|
540
|
+
traceTopicId: config.traceTopicId || '',
|
|
541
|
+
apiKey: config.apiKey,
|
|
542
|
+
ak: config.ak,
|
|
543
|
+
sk: config.sk,
|
|
544
|
+
timeoutMs: config.exportTimeoutMs
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
async function exportTraceToTls(trace, config) {
|
|
548
|
+
const required = requireExportConfig(config);
|
|
549
|
+
if (!required.ok) return {
|
|
550
|
+
status: 'skipped',
|
|
551
|
+
reason: required.reason,
|
|
552
|
+
message: required.missing?.join(', '),
|
|
553
|
+
trace_id: trace.trace_id,
|
|
554
|
+
span_count: trace.spans.length
|
|
555
|
+
};
|
|
556
|
+
const otlpPayload = buildOtlpPayload(trace, config);
|
|
557
|
+
const resourceSpan = otlpPayload.resourceSpans[0];
|
|
558
|
+
const resource = resourceSpan.resource;
|
|
559
|
+
const scopeSpan = resourceSpan.scopeSpans[0];
|
|
560
|
+
const scope = scopeSpan.scope;
|
|
561
|
+
const otlpSpans = scopeSpan.spans;
|
|
562
|
+
const records = otlpSpans.map((otlpSpan)=>otlpSpanToTlsEnvelope(otlpSpan, resource, scope));
|
|
563
|
+
const totalPayloadBytes = records.reduce((sum, record)=>sum + Buffer.byteLength(JSON.stringify(record), 'utf8'), 0);
|
|
564
|
+
const startedAt = Date.now();
|
|
565
|
+
const diagnostics = {
|
|
566
|
+
endpoint_host: normalizeProducerEndpoint(required.url),
|
|
567
|
+
endpoint_path: 'SendLogs'
|
|
568
|
+
};
|
|
569
|
+
try {
|
|
570
|
+
await sendTlsLogRecords(exporter_producerConfig(config), records, 'cursor-trace');
|
|
571
|
+
} catch (error) {
|
|
572
|
+
return {
|
|
573
|
+
status: 'failed',
|
|
574
|
+
reason: 'producer_error',
|
|
575
|
+
message: error instanceof Error ? error.message : String(error),
|
|
576
|
+
trace_id: trace.trace_id,
|
|
577
|
+
span_count: trace.spans.length,
|
|
578
|
+
payload_bytes: totalPayloadBytes,
|
|
579
|
+
attempts: 1,
|
|
580
|
+
duration_ms: Date.now() - startedAt,
|
|
581
|
+
...diagnostics
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
status: 'success',
|
|
586
|
+
trace_id: trace.trace_id,
|
|
587
|
+
span_count: trace.spans.length,
|
|
588
|
+
payload_bytes: totalPayloadBytes,
|
|
589
|
+
attempts: 1,
|
|
590
|
+
duration_ms: Date.now() - startedAt,
|
|
591
|
+
...diagnostics
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function stableFieldValue(value) {
|
|
595
|
+
if (null == value) return '';
|
|
596
|
+
if ('string' == typeof value || 'number' == typeof value || 'boolean' == typeof value) return value;
|
|
597
|
+
return serializeForAttr(value);
|
|
598
|
+
}
|
|
599
|
+
function inferUserRole(span) {
|
|
600
|
+
const input = span.input;
|
|
601
|
+
const role = input?.messages?.find((message)=>'string' == typeof message?.role)?.role;
|
|
602
|
+
return role || '';
|
|
603
|
+
}
|
|
604
|
+
function inferErrorMessage(span) {
|
|
605
|
+
if ('string' == typeof span.status?.message) return span.status.message;
|
|
606
|
+
const output = span.output;
|
|
607
|
+
if ('string' == typeof output?.error) return output.error;
|
|
608
|
+
return '';
|
|
609
|
+
}
|
|
610
|
+
function buildSchemaAlignedRecord(trace, span, observedTimeUnixMs = Date.now()) {
|
|
611
|
+
const attributes = span.attributes ?? {};
|
|
612
|
+
const resource = trace.resource ?? {};
|
|
613
|
+
const isModel = 'model' === span.type;
|
|
614
|
+
const isError = hasError(span);
|
|
615
|
+
const record = {
|
|
616
|
+
'span.name': span.name,
|
|
617
|
+
'span.kind': spanKindName(span),
|
|
618
|
+
'tls.app.type': attributes['tls.app.type'] ?? '',
|
|
619
|
+
'session.id': attributes['session.id'] ?? attributes['gen_ai.conversation.id'] ?? '',
|
|
620
|
+
'span.status.code': isError ? 'ERROR' : 'UNSET',
|
|
621
|
+
start_time_unix_us: unixMsToUs(span.start_time_unix_ms),
|
|
622
|
+
end_time_unix_us: unixMsToUs(span.end_time_unix_ms),
|
|
623
|
+
'span.duration_us': String(1000 * Math.max(0, Math.trunc(span.duration_ms))),
|
|
624
|
+
'service.name': resource['service.name'] ?? '',
|
|
625
|
+
'service.instance.id': resource['service.instance.id'] ?? '',
|
|
626
|
+
'host.name': resource['host.name'] ?? '',
|
|
627
|
+
observed_time_unix_ms: observedTimeUnixMs,
|
|
628
|
+
'telemetry.sdk.name': resource['telemetry.sdk.name'] ?? 'tls-observer-cursor',
|
|
629
|
+
'telemetry.sdk.language': resource['telemetry.sdk.language'] ?? 'nodejs',
|
|
630
|
+
'telemetry.sdk.version': resource['telemetry.sdk.version'] ?? '',
|
|
631
|
+
'otel.scope.name': 'tls-observer-cursor',
|
|
632
|
+
'otel.scope.version': package_namespaceObject.rE,
|
|
633
|
+
'event.name': span.name,
|
|
634
|
+
'user.role': inferUserRole(span),
|
|
635
|
+
'gen_ai.operation.name': attributes['gen_ai.operation.name'] ?? '',
|
|
636
|
+
'gen_ai.provider.name': attributes['gen_ai.provider.name'] ?? '',
|
|
637
|
+
'gen_ai.request.model': attributes['gen_ai.request.model'] ?? '',
|
|
638
|
+
'gen_ai.response.model': attributes['gen_ai.response.model'] ?? '',
|
|
639
|
+
'gen_ai.request.duration_ms': isModel ? span.duration_ms : '',
|
|
640
|
+
'gen_ai.input.messages': attributes['gen_ai.input.messages'] ?? '',
|
|
641
|
+
'gen_ai.output.messages': attributes['gen_ai.output.messages'] ?? '',
|
|
642
|
+
'gen_ai.usage.input_tokens': attributes['gen_ai.usage.input_tokens'] ?? '',
|
|
643
|
+
'gen_ai.usage.output_tokens': attributes['gen_ai.usage.output_tokens'] ?? '',
|
|
644
|
+
'gen_ai.usage.reasoning.output_tokens': attributes['gen_ai.usage.reasoning.output_tokens'] ?? '',
|
|
645
|
+
'gen_ai.usage.cache_read.input_tokens': attributes['gen_ai.usage.cache_read.input_tokens'] ?? '',
|
|
646
|
+
'gen_ai.usage.cache_creation.input_tokens': attributes['gen_ai.usage.cache_creation.input_tokens'] ?? '',
|
|
647
|
+
'gen_ai.usage.total_tokens': attributes['gen_ai.usage.total_tokens'] ?? '',
|
|
648
|
+
'gen_ai.tool.name': attributes['gen_ai.tool.name'] ?? '',
|
|
649
|
+
'gen_ai.tool.call.id': attributes['gen_ai.tool.call.id'] ?? '',
|
|
650
|
+
'gen_ai.tool.call.arguments': attributes['gen_ai.tool.call.arguments'] ?? '',
|
|
651
|
+
'gen_ai.tool.call.result': attributes['gen_ai.tool.call.result'] ?? '',
|
|
652
|
+
'error.type': isError ? attributes['error.type'] ?? 'error' : '',
|
|
653
|
+
'error.message': isError ? attributes['error.message'] || inferErrorMessage(span) || '' : ''
|
|
654
|
+
};
|
|
655
|
+
return Object.fromEntries(SCHEMA_FIELD_ORDER.map((key)=>[
|
|
656
|
+
key,
|
|
657
|
+
stableFieldValue(record[key])
|
|
658
|
+
]));
|
|
659
|
+
}
|
|
660
|
+
function buildSchemaAlignedRecords(trace, observedTimeUnixMs = Date.now()) {
|
|
661
|
+
return trace.spans.map((span)=>buildSchemaAlignedRecord(trace, span, observedTimeUnixMs));
|
|
662
|
+
}
|
|
663
|
+
function exportSummary(trace, result) {
|
|
664
|
+
return compactObject({
|
|
665
|
+
generated_at: new Date().toISOString(),
|
|
666
|
+
trace_id: trace.trace_id,
|
|
667
|
+
span_count: trace.spans.length,
|
|
668
|
+
export_status: result.status,
|
|
669
|
+
export_reason: result.reason,
|
|
670
|
+
export_message: result.message,
|
|
671
|
+
endpoint_host: result.endpoint_host,
|
|
672
|
+
endpoint_path: result.endpoint_path,
|
|
673
|
+
payload_bytes: result.payload_bytes,
|
|
674
|
+
duration_ms: result.duration_ms
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
678
|
+
const LOCK_RETRY_MS = 20;
|
|
679
|
+
const SESSION_MAX_AGE_MS = 86400000;
|
|
680
|
+
function sleep(ms) {
|
|
681
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
682
|
+
}
|
|
683
|
+
function state_lockPath(stateFilePath) {
|
|
684
|
+
return `${stateFilePath}.lock`;
|
|
685
|
+
}
|
|
686
|
+
async function acquireLock(stateFilePath) {
|
|
687
|
+
const lock = state_lockPath(stateFilePath);
|
|
688
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
689
|
+
__rspack_external_node_fs_5ea92f0c.mkdirSync(__rspack_external_node_path_c5b9b54f.dirname(stateFilePath), {
|
|
690
|
+
recursive: true
|
|
691
|
+
});
|
|
692
|
+
while(Date.now() < deadline)try {
|
|
693
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(lock, 'wx');
|
|
694
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
695
|
+
return;
|
|
696
|
+
} catch {
|
|
697
|
+
await sleep(LOCK_RETRY_MS);
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
__rspack_external_node_fs_5ea92f0c.unlinkSync(lock);
|
|
701
|
+
} catch {}
|
|
702
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(lock, 'wx');
|
|
703
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
704
|
+
}
|
|
705
|
+
function releaseLock(stateFilePath) {
|
|
706
|
+
try {
|
|
707
|
+
__rspack_external_node_fs_5ea92f0c.unlinkSync(state_lockPath(stateFilePath));
|
|
708
|
+
} catch {}
|
|
709
|
+
}
|
|
710
|
+
function state_loadState(stateFilePath) {
|
|
711
|
+
try {
|
|
712
|
+
return JSON.parse(__rspack_external_node_fs_5ea92f0c.readFileSync(stateFilePath, 'utf8'));
|
|
713
|
+
} catch {
|
|
714
|
+
return {};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function state_getSessionState(state, sessionId) {
|
|
718
|
+
return state[sessionId] ?? {
|
|
719
|
+
last_line: -1,
|
|
720
|
+
last_offset: 0,
|
|
721
|
+
turn_count: 0,
|
|
722
|
+
updated: '',
|
|
723
|
+
tool_start_times: {},
|
|
724
|
+
tool_observations: {},
|
|
725
|
+
tool_observation_order: [],
|
|
726
|
+
pending_subagent_traces: [],
|
|
727
|
+
subagent_turn_count: 0
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async function state_atomicUpdateState(stateFilePath, fn) {
|
|
731
|
+
await acquireLock(stateFilePath);
|
|
732
|
+
try {
|
|
733
|
+
const next = pruneOldSessions(fn(state_loadState(stateFilePath)));
|
|
734
|
+
__rspack_external_node_fs_5ea92f0c.mkdirSync(__rspack_external_node_path_c5b9b54f.dirname(stateFilePath), {
|
|
735
|
+
recursive: true
|
|
736
|
+
});
|
|
737
|
+
__rspack_external_node_fs_5ea92f0c.writeFileSync(stateFilePath, JSON.stringify(next, null, 2));
|
|
738
|
+
} finally{
|
|
739
|
+
releaseLock(stateFilePath);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function pruneOldSessions(state, now = Date.now()) {
|
|
743
|
+
const cutoff = now - SESSION_MAX_AGE_MS;
|
|
744
|
+
const pruned = {};
|
|
745
|
+
for (const [sessionId, session] of Object.entries(state)){
|
|
746
|
+
const updatedMs = session.updated ? new Date(session.updated).getTime() : 0;
|
|
747
|
+
if (updatedMs >= cutoff || session.current_turn_id) pruned[sessionId] = session;
|
|
748
|
+
}
|
|
749
|
+
return pruned;
|
|
750
|
+
}
|
|
751
|
+
const SCHEMA_URL = 'https://opentelemetry.io/schemas/1.41.0';
|
|
752
|
+
const MAX_ATTRIBUTE_TEXT = 65536;
|
|
753
|
+
const TLS_APP_TYPE = 'cursor';
|
|
754
|
+
const ROOT_SPAN_NAME = 'agent.turn';
|
|
755
|
+
const LLM_SPAN_NAME = 'llm.request';
|
|
756
|
+
const TOOL_SPAN_NAME = 'tool.call';
|
|
757
|
+
function hashHex(value, length) {
|
|
758
|
+
const hex = external_node_crypto_createHash('sha256').update(value).digest('hex').slice(0, length);
|
|
759
|
+
if (!/^0+$/.test(hex)) return hex;
|
|
760
|
+
return '1'.padStart(length, '0');
|
|
761
|
+
}
|
|
762
|
+
function toTraceId(value) {
|
|
763
|
+
const compact = value.replace(/-/g, '').toLowerCase();
|
|
764
|
+
if (/^[0-9a-f]{32}$/.test(compact) && compact !== '0'.repeat(32)) return compact;
|
|
765
|
+
return hashHex(value, 32);
|
|
766
|
+
}
|
|
767
|
+
function toSpanId(...parts) {
|
|
768
|
+
return hashHex(parts.map((part)=>String(part ?? '')).join(':'), 16);
|
|
769
|
+
}
|
|
770
|
+
function parseMs(value, fallback) {
|
|
771
|
+
if ('number' == typeof value && Number.isFinite(value)) return Math.trunc(value);
|
|
772
|
+
const parsed = Date.parse(String(value ?? ''));
|
|
773
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
774
|
+
}
|
|
775
|
+
function orderedTiming(startTime, endTime) {
|
|
776
|
+
const fallback = Date.now();
|
|
777
|
+
const startMs = parseMs(startTime, fallback);
|
|
778
|
+
const rawEndMs = parseMs(endTime, startMs);
|
|
779
|
+
if (rawEndMs < startMs) return {
|
|
780
|
+
startMs,
|
|
781
|
+
endMs: startMs
|
|
782
|
+
};
|
|
783
|
+
return {
|
|
784
|
+
startMs,
|
|
785
|
+
endMs: rawEndMs
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function clipString(value) {
|
|
789
|
+
return value.length > MAX_ATTRIBUTE_TEXT ? `${value.slice(0, MAX_ATTRIBUTE_TEXT)}...[truncated]` : value;
|
|
790
|
+
}
|
|
791
|
+
function normalizeValue(value, captureContent) {
|
|
792
|
+
if (!captureContent) return;
|
|
793
|
+
if ('string' == typeof value) return clipString(value);
|
|
794
|
+
if (null == value) return value;
|
|
795
|
+
const text = json_safeJsonStringify(value);
|
|
796
|
+
return text.length > MAX_ATTRIBUTE_TEXT ? `${text.slice(0, MAX_ATTRIBUTE_TEXT)}...[truncated]` : value;
|
|
797
|
+
}
|
|
798
|
+
function stringifyStructuredAttributes(attributes) {
|
|
799
|
+
return Object.fromEntries(Object.entries(attributes).map(([key, value])=>[
|
|
800
|
+
key,
|
|
801
|
+
null !== value && 'object' == typeof value ? clipString(json_safeJsonStringify(value)) : value
|
|
802
|
+
]));
|
|
803
|
+
}
|
|
804
|
+
function traceSpan(options) {
|
|
805
|
+
const { startMs, endMs } = orderedTiming(options.startTime, options.endTime);
|
|
806
|
+
return {
|
|
807
|
+
trace_id: options.traceId,
|
|
808
|
+
span_id: options.spanId,
|
|
809
|
+
parent_span_id: options.parentSpanId ?? null,
|
|
810
|
+
name: options.name,
|
|
811
|
+
type: options.type,
|
|
812
|
+
span_kind: options.spanKind ?? ('entry' === options.type ? 'SERVER' : 'model' === options.type ? 'CLIENT' : 'INTERNAL'),
|
|
813
|
+
start_time: new Date(startMs).toISOString(),
|
|
814
|
+
end_time: new Date(endMs).toISOString(),
|
|
815
|
+
start_time_unix_ms: startMs,
|
|
816
|
+
end_time_unix_ms: endMs,
|
|
817
|
+
duration_ms: endMs - startMs,
|
|
818
|
+
attributes: compactObject(stringifyStructuredAttributes({
|
|
819
|
+
...options.attributes ?? {},
|
|
820
|
+
'tls.app.type': TLS_APP_TYPE
|
|
821
|
+
})),
|
|
822
|
+
input: options.input,
|
|
823
|
+
output: options.output,
|
|
824
|
+
status: options.status
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function modelId(model) {
|
|
828
|
+
if ('string' == typeof model) return model;
|
|
829
|
+
return model?.id ?? model?.display_name;
|
|
830
|
+
}
|
|
831
|
+
function inputModelId(input) {
|
|
832
|
+
return modelId(input.model) ?? input.model_id;
|
|
833
|
+
}
|
|
834
|
+
function contentBlockToPart(block, captureContent) {
|
|
835
|
+
if ('text' === block.type) return {
|
|
836
|
+
type: 'text',
|
|
837
|
+
content: normalizeValue(block.text, captureContent) ?? ''
|
|
838
|
+
};
|
|
839
|
+
if ('thinking' === block.type || 'reasoning' === block.type) return {
|
|
840
|
+
type: 'reasoning',
|
|
841
|
+
content: normalizeValue(block.thinking ?? block.text, captureContent) ?? ''
|
|
842
|
+
};
|
|
843
|
+
if ('tool_use' === block.type) return {
|
|
844
|
+
type: 'tool_call',
|
|
845
|
+
id: block.id,
|
|
846
|
+
name: block.name,
|
|
847
|
+
arguments: normalizeValue(block.input, captureContent)
|
|
848
|
+
};
|
|
849
|
+
return {
|
|
850
|
+
type: 'unknown',
|
|
851
|
+
content: ''
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function messageContentToParts(content, captureContent) {
|
|
855
|
+
if ('string' == typeof content) return [
|
|
856
|
+
{
|
|
857
|
+
type: 'text',
|
|
858
|
+
content: normalizeValue(content, captureContent) ?? ''
|
|
859
|
+
}
|
|
860
|
+
];
|
|
861
|
+
return content.map((item)=>normalizeValue(item, captureContent));
|
|
862
|
+
}
|
|
863
|
+
function assistantMessage(call, captureContent) {
|
|
864
|
+
return {
|
|
865
|
+
role: 'assistant',
|
|
866
|
+
parts: call.content.map((block)=>contentBlockToPart(block, captureContent))
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function finishReasonForCall(call) {
|
|
870
|
+
if (call.stopReason) return call.stopReason;
|
|
871
|
+
return call.toolCalls.length > 0 ? 'tool_calls' : void 0;
|
|
872
|
+
}
|
|
873
|
+
function assistantOutputMessages(call, captureContent) {
|
|
874
|
+
const message = assistantMessage(call, captureContent);
|
|
875
|
+
const finishReason = finishReasonForCall(call);
|
|
876
|
+
return [
|
|
877
|
+
finishReason ? {
|
|
878
|
+
...message,
|
|
879
|
+
finish_reason: finishReason
|
|
880
|
+
} : message
|
|
881
|
+
];
|
|
882
|
+
}
|
|
883
|
+
function messagesAttribute(messages, captureContent) {
|
|
884
|
+
if (!captureContent || !messages) return;
|
|
885
|
+
return clipString(json_safeJsonStringify(messages));
|
|
886
|
+
}
|
|
887
|
+
function toolResultMessage(toolCallId, result, captureContent) {
|
|
888
|
+
const normalizedResult = normalizeValue(result, captureContent);
|
|
889
|
+
return {
|
|
890
|
+
role: 'tool',
|
|
891
|
+
parts: [
|
|
892
|
+
{
|
|
893
|
+
type: 'tool_call_response',
|
|
894
|
+
id: toolCallId,
|
|
895
|
+
response: normalizedResult,
|
|
896
|
+
result: normalizedResult
|
|
897
|
+
}
|
|
898
|
+
]
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function usageAttributes(usage) {
|
|
902
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
903
|
+
const cacheCreation = usage.cache_creation_input_tokens ?? 0;
|
|
904
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
905
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
906
|
+
return compactObject({
|
|
907
|
+
'gen_ai.usage.input_tokens': inputTokens || void 0,
|
|
908
|
+
'gen_ai.usage.output_tokens': outputTokens || void 0,
|
|
909
|
+
'gen_ai.usage.total_tokens': inputTokens || outputTokens ? inputTokens + outputTokens : void 0,
|
|
910
|
+
'gen_ai.usage.reasoning.output_tokens': usage.reasoning_output_tokens || void 0,
|
|
911
|
+
'gen_ai.usage.cache_read.input_tokens': cacheRead || void 0,
|
|
912
|
+
'gen_ai.usage.cache_creation.input_tokens': cacheCreation || void 0
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function sumNumericAttribute(records, key) {
|
|
916
|
+
return records.reduce((sum, record)=>{
|
|
917
|
+
const value = record[key];
|
|
918
|
+
return 'number' == typeof value && Number.isFinite(value) ? sum + value : sum;
|
|
919
|
+
}, 0);
|
|
920
|
+
}
|
|
921
|
+
function rootUsageAttributes(calls) {
|
|
922
|
+
const usageRecords = calls.map((call)=>usageAttributes(call.usage));
|
|
923
|
+
const totalTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.total_tokens');
|
|
924
|
+
const inputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.input_tokens');
|
|
925
|
+
const outputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.output_tokens');
|
|
926
|
+
const reasoningOutputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.reasoning.output_tokens');
|
|
927
|
+
const cacheReadInputTokens = sumNumericAttribute(usageRecords, 'gen_ai.usage.cache_read.input_tokens');
|
|
928
|
+
return compactObject({
|
|
929
|
+
'gen_ai.usage.total_tokens': totalTokens || void 0,
|
|
930
|
+
'gen_ai.usage.input_tokens': inputTokens || void 0,
|
|
931
|
+
'gen_ai.usage.output_tokens': outputTokens || void 0,
|
|
932
|
+
'gen_ai.usage.reasoning.output_tokens': reasoningOutputTokens || void 0,
|
|
933
|
+
'gen_ai.usage.cache_read.input_tokens': cacheReadInputTokens || void 0
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function commonSessionAttributes(input) {
|
|
937
|
+
const conversationId = input.conversation_id ?? input.session_id;
|
|
938
|
+
return compactObject({
|
|
939
|
+
'session.id': input.session_id ?? input.conversation_id,
|
|
940
|
+
'gen_ai.conversation.id': conversationId,
|
|
941
|
+
'cursor.session.id': input.session_id,
|
|
942
|
+
'cursor.conversation.id': input.conversation_id,
|
|
943
|
+
'cursor.generation.id': input.generation_id,
|
|
944
|
+
'cursor.session.name': input.session_name,
|
|
945
|
+
'cursor.version': input.cursor_version ?? input.version,
|
|
946
|
+
'cursor.user.email': input.user_email,
|
|
947
|
+
'cursor.cwd': input.cwd ?? input.workspace?.current_dir,
|
|
948
|
+
'cursor.workspace.roots': input.workspace_roots,
|
|
949
|
+
'cursor.workspace.project_dir': input.workspace?.project_dir,
|
|
950
|
+
"cursor.transcript_path": input.transcript_path,
|
|
951
|
+
'cursor.model.id': inputModelId(input),
|
|
952
|
+
'cursor.model.params': input.model_params
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
function buildResource(config) {
|
|
956
|
+
return compactObject({
|
|
957
|
+
'service.name': config.serviceName,
|
|
958
|
+
'service.instance.id': config.serviceInstanceId,
|
|
959
|
+
'host.name': hostname(),
|
|
960
|
+
'telemetry.sdk.language': 'nodejs',
|
|
961
|
+
'telemetry.sdk.name': 'tls-observer-cursor',
|
|
962
|
+
'telemetry.sdk.version': '0.1.0',
|
|
963
|
+
...localIdentityMetadata()
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
function buildTraceFromTurn(options) {
|
|
967
|
+
const { input, turn, turnIndex, sessionState, config, extraAttributes } = options;
|
|
968
|
+
const traceSeed = [
|
|
969
|
+
input.session_id ?? 'cursor',
|
|
970
|
+
turn.promptId ?? sessionState.current_turn_id ?? turnIndex,
|
|
971
|
+
turn.userTimestamp,
|
|
972
|
+
options.traceSeedSuffix
|
|
973
|
+
].filter((part)=>void 0 !== part && '' !== part).join(':');
|
|
974
|
+
const traceId = toTraceId(traceSeed);
|
|
975
|
+
const spans = [];
|
|
976
|
+
const fallbackEndMs = parseMs(options.turnEndTimeMs, Date.now());
|
|
977
|
+
const userStartMs = parseMs(turn.userTimestamp, parseMs(sessionState.current_turn_start_ms, fallbackEndMs));
|
|
978
|
+
const turnStartMs = Math.min(userStartMs, parseMs(sessionState.current_turn_start_ms, userStartMs), ...turn.llmCalls.map((call)=>parseMs(call.startTime, userStartMs)));
|
|
979
|
+
const activityEnds = [
|
|
980
|
+
...turn.llmCalls.map((call)=>parseMs(call.endTime, turnStartMs)),
|
|
981
|
+
...turn.llmCalls.flatMap((call)=>call.toolCalls.map((tool)=>parseMs(tool.result?.timestamp, turnStartMs)))
|
|
982
|
+
].filter(Number.isFinite);
|
|
983
|
+
const turnEndMs = Math.max(turnStartMs, ...activityEnds, fallbackEndMs);
|
|
984
|
+
const turnSpanId = toSpanId(traceId, 'turn', turnIndex);
|
|
985
|
+
const sessionAttrs = compactObject({
|
|
986
|
+
...commonSessionAttributes(input),
|
|
987
|
+
...extraAttributes ?? {}
|
|
988
|
+
});
|
|
989
|
+
const userMessages = [
|
|
990
|
+
{
|
|
991
|
+
role: 'user',
|
|
992
|
+
parts: messageContentToParts(turn.userContent, config.captureContent)
|
|
993
|
+
}
|
|
994
|
+
];
|
|
995
|
+
const lastAssistant = turn.llmCalls.at(-1);
|
|
996
|
+
const requestModel = modelId(input.model) ?? lastAssistant?.model;
|
|
997
|
+
const responseModel = lastAssistant?.model ?? requestModel;
|
|
998
|
+
const outputMessages = lastAssistant ? assistantOutputMessages(lastAssistant, config.captureContent) : void 0;
|
|
999
|
+
spans.push(traceSpan({
|
|
1000
|
+
traceId,
|
|
1001
|
+
spanId: turnSpanId,
|
|
1002
|
+
name: ROOT_SPAN_NAME,
|
|
1003
|
+
type: 'entry',
|
|
1004
|
+
startTime: turnStartMs,
|
|
1005
|
+
endTime: turnEndMs,
|
|
1006
|
+
attributes: {
|
|
1007
|
+
...sessionAttrs,
|
|
1008
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
1009
|
+
'gen_ai.provider.name': 'cursor',
|
|
1010
|
+
'gen_ai.request.model': requestModel,
|
|
1011
|
+
'gen_ai.response.model': responseModel,
|
|
1012
|
+
'gen_ai.input.messages': messagesAttribute(userMessages, config.captureContent),
|
|
1013
|
+
'gen_ai.output.messages': messagesAttribute(outputMessages, config.captureContent),
|
|
1014
|
+
...rootUsageAttributes(turn.llmCalls)
|
|
1015
|
+
},
|
|
1016
|
+
input: config.captureContent ? {
|
|
1017
|
+
messages: userMessages
|
|
1018
|
+
} : void 0,
|
|
1019
|
+
output: config.captureContent && outputMessages ? {
|
|
1020
|
+
messages: outputMessages
|
|
1021
|
+
} : void 0,
|
|
1022
|
+
status: turn.isComplete ? {
|
|
1023
|
+
code: 'OK'
|
|
1024
|
+
} : {
|
|
1025
|
+
code: 'ERROR',
|
|
1026
|
+
message: 'interrupted'
|
|
1027
|
+
}
|
|
1028
|
+
}));
|
|
1029
|
+
const accumulatedMessages = [
|
|
1030
|
+
...userMessages
|
|
1031
|
+
];
|
|
1032
|
+
for (const [llmIndex, call] of turn.llmCalls.entries()){
|
|
1033
|
+
const llmPointMs = parseMs(call.endTime, parseMs(call.startTime, turnStartMs));
|
|
1034
|
+
const assistant = assistantMessage(call, config.captureContent);
|
|
1035
|
+
const output = assistantOutputMessages(call, config.captureContent);
|
|
1036
|
+
const inputMessages = 0 === llmIndex ? [
|
|
1037
|
+
...accumulatedMessages
|
|
1038
|
+
] : void 0;
|
|
1039
|
+
const llmSpanId = toSpanId(traceId, 'llm', llmIndex, call.startTime, call.endTime);
|
|
1040
|
+
const finishReason = finishReasonForCall(call);
|
|
1041
|
+
spans.push(traceSpan({
|
|
1042
|
+
traceId,
|
|
1043
|
+
spanId: llmSpanId,
|
|
1044
|
+
parentSpanId: turnSpanId,
|
|
1045
|
+
name: LLM_SPAN_NAME,
|
|
1046
|
+
type: 'model',
|
|
1047
|
+
spanKind: 'CLIENT',
|
|
1048
|
+
startTime: llmPointMs,
|
|
1049
|
+
endTime: llmPointMs,
|
|
1050
|
+
attributes: {
|
|
1051
|
+
...sessionAttrs,
|
|
1052
|
+
'gen_ai.operation.name': 'chat',
|
|
1053
|
+
'gen_ai.provider.name': 'cursor',
|
|
1054
|
+
'gen_ai.request.model': modelId(input.model) ?? call.model,
|
|
1055
|
+
'gen_ai.response.model': call.model,
|
|
1056
|
+
'gen_ai.response.finish_reasons': finishReason ? [
|
|
1057
|
+
finishReason
|
|
1058
|
+
] : void 0,
|
|
1059
|
+
'gen_ai.input.messages': messagesAttribute(inputMessages, config.captureContent),
|
|
1060
|
+
'gen_ai.output.messages': messagesAttribute(output, config.captureContent),
|
|
1061
|
+
...usageAttributes(call.usage)
|
|
1062
|
+
},
|
|
1063
|
+
input: config.captureContent && inputMessages ? {
|
|
1064
|
+
messages: inputMessages
|
|
1065
|
+
} : void 0,
|
|
1066
|
+
output: config.captureContent ? {
|
|
1067
|
+
messages: output
|
|
1068
|
+
} : void 0
|
|
1069
|
+
}));
|
|
1070
|
+
accumulatedMessages.push(assistant);
|
|
1071
|
+
for (const toolCall of call.toolCalls){
|
|
1072
|
+
const toolInput = toolCall.tool_use.input;
|
|
1073
|
+
const toolOutput = toolCall.result?.content;
|
|
1074
|
+
const isError = toolCall.result?.is_error;
|
|
1075
|
+
const toolSpanId = toSpanId(traceId, 'tool', toolCall.tool_use.id);
|
|
1076
|
+
const errorType = 'tool_error';
|
|
1077
|
+
spans.push(traceSpan({
|
|
1078
|
+
traceId,
|
|
1079
|
+
spanId: toolSpanId,
|
|
1080
|
+
parentSpanId: llmSpanId,
|
|
1081
|
+
name: TOOL_SPAN_NAME,
|
|
1082
|
+
type: 'tool',
|
|
1083
|
+
spanKind: 'INTERNAL',
|
|
1084
|
+
startTime: llmPointMs,
|
|
1085
|
+
endTime: llmPointMs,
|
|
1086
|
+
attributes: {
|
|
1087
|
+
...sessionAttrs,
|
|
1088
|
+
'gen_ai.operation.name': 'execute_tool',
|
|
1089
|
+
'gen_ai.tool.name': toolCall.tool_use.name,
|
|
1090
|
+
'gen_ai.tool.call.id': toolCall.tool_use.id,
|
|
1091
|
+
'gen_ai.tool.call.arguments': normalizeValue(toolInput, config.captureContent),
|
|
1092
|
+
'gen_ai.tool.call.result': normalizeValue(toolOutput, config.captureContent),
|
|
1093
|
+
'error.type': isError ? errorType : void 0,
|
|
1094
|
+
'error.message': isError ? normalizeValue(toolOutput, config.captureContent) : void 0
|
|
1095
|
+
},
|
|
1096
|
+
input: normalizeValue(toolInput, config.captureContent),
|
|
1097
|
+
output: normalizeValue(toolOutput, config.captureContent),
|
|
1098
|
+
status: isError ? {
|
|
1099
|
+
code: 'ERROR',
|
|
1100
|
+
message: errorType
|
|
1101
|
+
} : {
|
|
1102
|
+
code: 'OK'
|
|
1103
|
+
}
|
|
1104
|
+
}));
|
|
1105
|
+
if (void 0 !== toolOutput) accumulatedMessages.push(toolResultMessage(toolCall.tool_use.id, toolOutput, config.captureContent));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
schema_url: SCHEMA_URL,
|
|
1110
|
+
source: options.source ?? "cursor_stop_hook_transcript",
|
|
1111
|
+
trace_id: traceId,
|
|
1112
|
+
resource: buildResource(config),
|
|
1113
|
+
spans: spans.sort((left, right)=>{
|
|
1114
|
+
const leftRoot = null === left.parent_span_id ? -1 : 0;
|
|
1115
|
+
const rightRoot = null === right.parent_span_id ? -1 : 0;
|
|
1116
|
+
return leftRoot - rightRoot || left.start_time_unix_ms - right.start_time_unix_ms || left.end_time_unix_ms - right.end_time_unix_ms;
|
|
1117
|
+
})
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
function hookTextMessages(role, text, captureContent) {
|
|
1121
|
+
if (!text) return;
|
|
1122
|
+
return [
|
|
1123
|
+
{
|
|
1124
|
+
role,
|
|
1125
|
+
parts: [
|
|
1126
|
+
{
|
|
1127
|
+
type: 'text',
|
|
1128
|
+
content: normalizeValue(text, captureContent) ?? ''
|
|
1129
|
+
}
|
|
1130
|
+
]
|
|
1131
|
+
}
|
|
1132
|
+
];
|
|
1133
|
+
}
|
|
1134
|
+
function buildTraceFromHookState(options) {
|
|
1135
|
+
const { input, sessionState, config, turnIndex, extraAttributes } = options;
|
|
1136
|
+
const prompt = sessionState.current_prompt;
|
|
1137
|
+
const responseText = input.last_assistant_message;
|
|
1138
|
+
const stopMs = options.stopTimeMs ?? Date.now();
|
|
1139
|
+
const activityStarts = [
|
|
1140
|
+
sessionState.current_turn_start_ms
|
|
1141
|
+
].filter((value)=>'number' == typeof value && Number.isFinite(value));
|
|
1142
|
+
if (!prompt && !responseText) return;
|
|
1143
|
+
const turnStartMs = Math.min(...activityStarts, stopMs);
|
|
1144
|
+
const turnEndMs = Math.max(turnStartMs, stopMs);
|
|
1145
|
+
const traceSeed = [
|
|
1146
|
+
input.session_id ?? input.conversation_id ?? 'cursor',
|
|
1147
|
+
input.generation_id,
|
|
1148
|
+
sessionState.current_turn_id ?? turnIndex,
|
|
1149
|
+
sessionState.current_turn_start_ms ?? turnStartMs,
|
|
1150
|
+
options.traceSeedSuffix
|
|
1151
|
+
].filter((part)=>void 0 !== part && '' !== part).join(':');
|
|
1152
|
+
const traceId = toTraceId(traceSeed);
|
|
1153
|
+
const turnSpanId = toSpanId(traceId, 'turn', turnIndex);
|
|
1154
|
+
const llmSpanId = toSpanId(traceId, 'llm', 'hook', turnIndex);
|
|
1155
|
+
const sessionAttrs = compactObject({
|
|
1156
|
+
...commonSessionAttributes(input),
|
|
1157
|
+
...extraAttributes ?? {}
|
|
1158
|
+
});
|
|
1159
|
+
const userMessages = hookTextMessages('user', prompt, config.captureContent);
|
|
1160
|
+
const outputMessages = hookTextMessages('assistant', responseText, config.captureContent);
|
|
1161
|
+
const requestModel = inputModelId(input);
|
|
1162
|
+
const responseModel = requestModel;
|
|
1163
|
+
const spans = [];
|
|
1164
|
+
spans.push(traceSpan({
|
|
1165
|
+
traceId,
|
|
1166
|
+
spanId: turnSpanId,
|
|
1167
|
+
name: ROOT_SPAN_NAME,
|
|
1168
|
+
type: 'entry',
|
|
1169
|
+
startTime: turnStartMs,
|
|
1170
|
+
endTime: turnEndMs,
|
|
1171
|
+
attributes: {
|
|
1172
|
+
...sessionAttrs,
|
|
1173
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
1174
|
+
'gen_ai.provider.name': 'cursor',
|
|
1175
|
+
'gen_ai.request.model': requestModel,
|
|
1176
|
+
'gen_ai.response.model': responseModel,
|
|
1177
|
+
'gen_ai.input.messages': messagesAttribute(userMessages, config.captureContent),
|
|
1178
|
+
'gen_ai.output.messages': messagesAttribute(outputMessages, config.captureContent)
|
|
1179
|
+
},
|
|
1180
|
+
input: config.captureContent && userMessages ? {
|
|
1181
|
+
messages: userMessages
|
|
1182
|
+
} : void 0,
|
|
1183
|
+
output: config.captureContent && outputMessages ? {
|
|
1184
|
+
messages: outputMessages
|
|
1185
|
+
} : void 0,
|
|
1186
|
+
status: 'error' === input.reason ? {
|
|
1187
|
+
code: 'ERROR',
|
|
1188
|
+
message: 'error'
|
|
1189
|
+
} : {
|
|
1190
|
+
code: 'OK'
|
|
1191
|
+
}
|
|
1192
|
+
}));
|
|
1193
|
+
spans.push(traceSpan({
|
|
1194
|
+
traceId,
|
|
1195
|
+
spanId: llmSpanId,
|
|
1196
|
+
parentSpanId: turnSpanId,
|
|
1197
|
+
name: LLM_SPAN_NAME,
|
|
1198
|
+
type: 'model',
|
|
1199
|
+
spanKind: 'CLIENT',
|
|
1200
|
+
startTime: turnEndMs,
|
|
1201
|
+
endTime: turnEndMs,
|
|
1202
|
+
attributes: {
|
|
1203
|
+
...sessionAttrs,
|
|
1204
|
+
'gen_ai.operation.name': 'chat',
|
|
1205
|
+
'gen_ai.provider.name': 'cursor',
|
|
1206
|
+
'gen_ai.request.model': requestModel,
|
|
1207
|
+
'gen_ai.response.model': responseModel,
|
|
1208
|
+
'gen_ai.input.messages': messagesAttribute(userMessages, config.captureContent),
|
|
1209
|
+
'gen_ai.output.messages': messagesAttribute(outputMessages, config.captureContent)
|
|
1210
|
+
},
|
|
1211
|
+
input: config.captureContent && userMessages ? {
|
|
1212
|
+
messages: userMessages
|
|
1213
|
+
} : void 0,
|
|
1214
|
+
output: config.captureContent && outputMessages ? {
|
|
1215
|
+
messages: outputMessages
|
|
1216
|
+
} : void 0,
|
|
1217
|
+
status: {
|
|
1218
|
+
code: 'OK'
|
|
1219
|
+
}
|
|
1220
|
+
}));
|
|
1221
|
+
return {
|
|
1222
|
+
schema_url: SCHEMA_URL,
|
|
1223
|
+
source: options.source ?? 'cursor_stop_hook_events',
|
|
1224
|
+
trace_id: traceId,
|
|
1225
|
+
resource: buildResource(config),
|
|
1226
|
+
spans: spans.sort((left, right)=>{
|
|
1227
|
+
const leftRoot = null === left.parent_span_id ? -1 : 0;
|
|
1228
|
+
const rightRoot = null === right.parent_span_id ? -1 : 0;
|
|
1229
|
+
return leftRoot - rightRoot || left.start_time_unix_ms - right.start_time_unix_ms || left.end_time_unix_ms - right.end_time_unix_ms;
|
|
1230
|
+
})
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
async function writeTextFileAtomic(filePath, text, options = {}) {
|
|
1234
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(__rspack_external_node_path_c5b9b54f.dirname(filePath), {
|
|
1235
|
+
recursive: true
|
|
1236
|
+
});
|
|
1237
|
+
const tempPath = __rspack_external_node_path_c5b9b54f.join(__rspack_external_node_path_c5b9b54f.dirname(filePath), `.${__rspack_external_node_path_c5b9b54f.basename(filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
|
|
1238
|
+
let renamed = false;
|
|
1239
|
+
try {
|
|
1240
|
+
await __rspack_external_node_fs_promises_153e37e0.writeFile(tempPath, text, {
|
|
1241
|
+
encoding: 'utf8',
|
|
1242
|
+
...void 0 === options.mode ? {} : {
|
|
1243
|
+
mode: options.mode
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
await __rspack_external_node_fs_promises_153e37e0.rename(tempPath, filePath);
|
|
1247
|
+
renamed = true;
|
|
1248
|
+
if (void 0 !== options.mode) await __rspack_external_node_fs_promises_153e37e0.chmod(filePath, options.mode);
|
|
1249
|
+
} finally{
|
|
1250
|
+
if (!renamed) await __rspack_external_node_fs_promises_153e37e0.rm(tempPath, {
|
|
1251
|
+
force: true
|
|
1252
|
+
}).catch(()=>void 0);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function writeJsonFileAtomic(filePath, value, options = {}) {
|
|
1256
|
+
await writeTextFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
|
|
1257
|
+
}
|
|
1258
|
+
const DEFAULT_MAX_FILES = 3;
|
|
1259
|
+
const LOCK_STALE_MS = 30000;
|
|
1260
|
+
const jsonl_LOCK_TIMEOUT_MS = 5000;
|
|
1261
|
+
const jsonl_LOCK_RETRY_MS = 25;
|
|
1262
|
+
async function appendJsonlFile(filePath, values) {
|
|
1263
|
+
const entries = Array.isArray(values) ? values : [
|
|
1264
|
+
values
|
|
1265
|
+
];
|
|
1266
|
+
if (0 === entries.length) return;
|
|
1267
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(__rspack_external_node_path_c5b9b54f.dirname(filePath), {
|
|
1268
|
+
recursive: true
|
|
1269
|
+
});
|
|
1270
|
+
await __rspack_external_node_fs_promises_153e37e0.appendFile(filePath, `${entries.map((entry)=>JSON.stringify(entry)).join('\n')}\n`, 'utf8');
|
|
1271
|
+
}
|
|
1272
|
+
async function delay(ms) {
|
|
1273
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
1274
|
+
}
|
|
1275
|
+
async function withJsonlFileLock(filePath, action) {
|
|
1276
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(__rspack_external_node_path_c5b9b54f.dirname(filePath), {
|
|
1277
|
+
recursive: true
|
|
1278
|
+
});
|
|
1279
|
+
const lockPath = `${filePath}.lock`;
|
|
1280
|
+
const startedAt = Date.now();
|
|
1281
|
+
while(true)try {
|
|
1282
|
+
const handle = await __rspack_external_node_fs_promises_153e37e0.open(lockPath, 'wx');
|
|
1283
|
+
try {
|
|
1284
|
+
await handle.writeFile(`${process.pid}:${new Date().toISOString()}\n`, 'utf8');
|
|
1285
|
+
return await action();
|
|
1286
|
+
} finally{
|
|
1287
|
+
await handle.close().catch(()=>void 0);
|
|
1288
|
+
await __rspack_external_node_fs_promises_153e37e0.rm(lockPath, {
|
|
1289
|
+
force: true
|
|
1290
|
+
}).catch(()=>void 0);
|
|
1291
|
+
}
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if ('EEXIST' !== error.code) throw error;
|
|
1294
|
+
try {
|
|
1295
|
+
const stat = await __rspack_external_node_fs_promises_153e37e0.stat(lockPath);
|
|
1296
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
1297
|
+
await __rspack_external_node_fs_promises_153e37e0.rm(lockPath, {
|
|
1298
|
+
force: true
|
|
1299
|
+
}).catch(()=>void 0);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
} catch (statError) {
|
|
1303
|
+
if ('ENOENT' === statError.code) continue;
|
|
1304
|
+
throw statError;
|
|
1305
|
+
}
|
|
1306
|
+
if (Date.now() - startedAt > jsonl_LOCK_TIMEOUT_MS) throw new Error(`Timed out waiting for JSONL file lock: ${lockPath}`);
|
|
1307
|
+
await delay(jsonl_LOCK_RETRY_MS);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function rotatedFileName(filePath, now = new Date()) {
|
|
1311
|
+
const dir = __rspack_external_node_path_c5b9b54f.dirname(filePath);
|
|
1312
|
+
const ext = __rspack_external_node_path_c5b9b54f.extname(filePath);
|
|
1313
|
+
const base = __rspack_external_node_path_c5b9b54f.basename(filePath, ext);
|
|
1314
|
+
const stamp = now.toISOString().replace(/[-:.TZ]/g, '').slice(0, 17);
|
|
1315
|
+
return __rspack_external_node_path_c5b9b54f.join(dir, `${base}.${stamp}.${process.pid}.${randomUUID()}${ext || '.jsonl'}`);
|
|
1316
|
+
}
|
|
1317
|
+
function rotatedFilePattern(filePath) {
|
|
1318
|
+
const ext = __rspack_external_node_path_c5b9b54f.extname(filePath);
|
|
1319
|
+
const base = __rspack_external_node_path_c5b9b54f.basename(filePath, ext).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1320
|
+
const escapedExt = (ext || '.jsonl').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1321
|
+
return new RegExp(`^${base}\\.\\d{17}\\.\\d+\\.[0-9a-f-]+${escapedExt}$`);
|
|
1322
|
+
}
|
|
1323
|
+
async function pruneRotatedJsonlFiles(filePath, maxFiles) {
|
|
1324
|
+
const maxRotatedFiles = Math.max(0, maxFiles - 1);
|
|
1325
|
+
const dir = __rspack_external_node_path_c5b9b54f.dirname(filePath);
|
|
1326
|
+
const pattern = rotatedFilePattern(filePath);
|
|
1327
|
+
let entries;
|
|
1328
|
+
try {
|
|
1329
|
+
entries = await __rspack_external_node_fs_promises_153e37e0.readdir(dir);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
if ('ENOENT' === error.code) return;
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
const rotated = entries.filter((entry)=>pattern.test(entry)).sort((left, right)=>right.localeCompare(left)).map((entry)=>__rspack_external_node_path_c5b9b54f.join(dir, entry));
|
|
1335
|
+
await Promise.all(rotated.slice(maxRotatedFiles).map((target)=>__rspack_external_node_fs_promises_153e37e0.rm(target, {
|
|
1336
|
+
force: true
|
|
1337
|
+
})));
|
|
1338
|
+
}
|
|
1339
|
+
async function rotateActiveJsonlFile(filePath, maxFiles) {
|
|
1340
|
+
await __rspack_external_node_fs_promises_153e37e0.rename(filePath, rotatedFileName(filePath)).catch(async (error)=>{
|
|
1341
|
+
if ('ENOENT' !== error.code) throw error;
|
|
1342
|
+
});
|
|
1343
|
+
await pruneRotatedJsonlFiles(filePath, maxFiles);
|
|
1344
|
+
}
|
|
1345
|
+
async function trimJsonlFileByBytes(filePath, maxBytes) {
|
|
1346
|
+
if (maxBytes <= 0) return void await __rspack_external_node_fs_promises_153e37e0.rm(filePath, {
|
|
1347
|
+
force: true
|
|
1348
|
+
}).catch(()=>void 0);
|
|
1349
|
+
let stat;
|
|
1350
|
+
try {
|
|
1351
|
+
stat = await __rspack_external_node_fs_promises_153e37e0.stat(filePath);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if ('ENOENT' === error.code) return;
|
|
1354
|
+
throw error;
|
|
1355
|
+
}
|
|
1356
|
+
if (stat.size <= maxBytes) return;
|
|
1357
|
+
const lines = (await __rspack_external_node_fs_promises_153e37e0.readFile(filePath, 'utf8')).split(/\r?\n/).filter(Boolean);
|
|
1358
|
+
const kept = [];
|
|
1359
|
+
let keptBytes = 0;
|
|
1360
|
+
for(let index = lines.length - 1; index >= 0; index -= 1){
|
|
1361
|
+
const line = lines[index];
|
|
1362
|
+
const lineBytes = Buffer.byteLength(`${line}\n`, 'utf8');
|
|
1363
|
+
if (kept.length > 0 && keptBytes + lineBytes > maxBytes) break;
|
|
1364
|
+
kept.push(line);
|
|
1365
|
+
keptBytes += lineBytes;
|
|
1366
|
+
if (keptBytes >= maxBytes) break;
|
|
1367
|
+
}
|
|
1368
|
+
await writeTextFileAtomic(filePath, kept.length ? `${kept.reverse().join('\n')}\n` : '');
|
|
1369
|
+
}
|
|
1370
|
+
async function appendBoundedJsonlFile(filePath, values, maxBytes, options = {}) {
|
|
1371
|
+
const entries = Array.isArray(values) ? values : [
|
|
1372
|
+
values
|
|
1373
|
+
];
|
|
1374
|
+
if (0 === entries.length) return;
|
|
1375
|
+
if ('rotate' !== options.strategy) return void await withJsonlFileLock(filePath, async ()=>{
|
|
1376
|
+
await appendJsonlFile(filePath, entries);
|
|
1377
|
+
await trimJsonlFileByBytes(filePath, maxBytes);
|
|
1378
|
+
});
|
|
1379
|
+
await withJsonlFileLock(filePath, async ()=>{
|
|
1380
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
1381
|
+
if (maxBytes <= 0 || maxFiles <= 0) {
|
|
1382
|
+
await __rspack_external_node_fs_promises_153e37e0.rm(filePath, {
|
|
1383
|
+
force: true
|
|
1384
|
+
}).catch(()=>void 0);
|
|
1385
|
+
await pruneRotatedJsonlFiles(filePath, 0);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
let currentBytes = 0;
|
|
1389
|
+
try {
|
|
1390
|
+
currentBytes = (await __rspack_external_node_fs_promises_153e37e0.stat(filePath)).size;
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
if ('ENOENT' !== error.code) throw error;
|
|
1393
|
+
}
|
|
1394
|
+
for (const entry of entries){
|
|
1395
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
1396
|
+
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
1397
|
+
if (currentBytes > 0 && currentBytes + lineBytes > maxBytes) {
|
|
1398
|
+
await rotateActiveJsonlFile(filePath, maxFiles);
|
|
1399
|
+
currentBytes = 0;
|
|
1400
|
+
}
|
|
1401
|
+
await __rspack_external_node_fs_promises_153e37e0.appendFile(filePath, line, 'utf8');
|
|
1402
|
+
currentBytes += lineBytes;
|
|
1403
|
+
}
|
|
1404
|
+
await pruneRotatedJsonlFiles(filePath, maxFiles);
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function readTranscript(filePath, afterLine = -1, afterOffset = 0, options = {}) {
|
|
1408
|
+
let size;
|
|
1409
|
+
try {
|
|
1410
|
+
size = __rspack_external_node_fs_5ea92f0c.statSync(filePath).size;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return {
|
|
1413
|
+
messages: [],
|
|
1414
|
+
lastLine: afterLine,
|
|
1415
|
+
lastOffset: Math.max(0, afterOffset)
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
const offsetBeyondFile = Number.isFinite(afterOffset) && afterOffset > size;
|
|
1419
|
+
const effectiveAfterLine = offsetBeyondFile ? -1 : afterLine;
|
|
1420
|
+
const requestedOffset = Number.isFinite(afterOffset) && afterOffset > 0 && afterOffset <= size ? Math.trunc(afterOffset) : 0;
|
|
1421
|
+
const lookBehindBytes = Math.max(0, Math.trunc(options.lookBehindBytes ?? 0));
|
|
1422
|
+
const startOffset = requestedOffset > 0 ? Math.max(0, requestedOffset - lookBehindBytes) : 0;
|
|
1423
|
+
const usingOffset = requestedOffset > 0;
|
|
1424
|
+
const fd = __rspack_external_node_fs_5ea92f0c.openSync(filePath, 'r');
|
|
1425
|
+
try {
|
|
1426
|
+
let discardFirstLine = false;
|
|
1427
|
+
if (startOffset > 0) {
|
|
1428
|
+
const previousByte = Buffer.alloc(1);
|
|
1429
|
+
__rspack_external_node_fs_5ea92f0c.readSync(fd, previousByte, 0, 1, startOffset - 1);
|
|
1430
|
+
discardFirstLine = 0x0a !== previousByte[0];
|
|
1431
|
+
}
|
|
1432
|
+
const chunkSize = 2097152;
|
|
1433
|
+
const buf = Buffer.alloc(chunkSize);
|
|
1434
|
+
const messages = [];
|
|
1435
|
+
let lastLine = effectiveAfterLine;
|
|
1436
|
+
let lastOffset = startOffset;
|
|
1437
|
+
let lineIndex = usingOffset ? effectiveAfterLine : -1;
|
|
1438
|
+
let partial = '';
|
|
1439
|
+
let readPos = startOffset;
|
|
1440
|
+
let cursorOffset = startOffset;
|
|
1441
|
+
let bytesRead = 0;
|
|
1442
|
+
while((bytesRead = __rspack_external_node_fs_5ea92f0c.readSync(fd, buf, 0, chunkSize, readPos)) > 0){
|
|
1443
|
+
readPos += bytesRead;
|
|
1444
|
+
const chunk = partial + buf.toString('utf8', 0, bytesRead);
|
|
1445
|
+
const lines = chunk.split('\n');
|
|
1446
|
+
partial = lines.pop() ?? '';
|
|
1447
|
+
for (const line of lines){
|
|
1448
|
+
const rawWithNewline = `${line}\n`;
|
|
1449
|
+
const lineStartOffset = cursorOffset;
|
|
1450
|
+
cursorOffset += Buffer.byteLength(rawWithNewline, 'utf8');
|
|
1451
|
+
if (discardFirstLine) {
|
|
1452
|
+
discardFirstLine = false;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
const trimmed = line.trim();
|
|
1456
|
+
if (trimmed) {
|
|
1457
|
+
lineIndex += 1;
|
|
1458
|
+
lastLine = lineIndex;
|
|
1459
|
+
lastOffset = cursorOffset;
|
|
1460
|
+
if (usingOffset || !(lineIndex <= effectiveAfterLine)) try {
|
|
1461
|
+
messages.push({
|
|
1462
|
+
...JSON.parse(trimmed),
|
|
1463
|
+
__line: lineIndex,
|
|
1464
|
+
__startOffset: lineStartOffset,
|
|
1465
|
+
__endOffset: cursorOffset
|
|
1466
|
+
});
|
|
1467
|
+
} catch {}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (partial.trim()) {
|
|
1472
|
+
const lineStartOffset = cursorOffset;
|
|
1473
|
+
cursorOffset += Buffer.byteLength(partial, 'utf8');
|
|
1474
|
+
if (discardFirstLine) {
|
|
1475
|
+
discardFirstLine = false;
|
|
1476
|
+
return {
|
|
1477
|
+
messages,
|
|
1478
|
+
lastLine,
|
|
1479
|
+
lastOffset: cursorOffset
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
lineIndex += 1;
|
|
1483
|
+
lastLine = lineIndex;
|
|
1484
|
+
lastOffset = cursorOffset;
|
|
1485
|
+
if (usingOffset || lineIndex > effectiveAfterLine) try {
|
|
1486
|
+
messages.push({
|
|
1487
|
+
...JSON.parse(partial.trim()),
|
|
1488
|
+
__line: lineIndex,
|
|
1489
|
+
__startOffset: lineStartOffset,
|
|
1490
|
+
__endOffset: cursorOffset
|
|
1491
|
+
});
|
|
1492
|
+
} catch {}
|
|
1493
|
+
}
|
|
1494
|
+
return {
|
|
1495
|
+
messages,
|
|
1496
|
+
lastLine,
|
|
1497
|
+
lastOffset
|
|
1498
|
+
};
|
|
1499
|
+
} finally{
|
|
1500
|
+
__rspack_external_node_fs_5ea92f0c.closeSync(fd);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
function getTranscriptEndPosition(filePath) {
|
|
1504
|
+
try {
|
|
1505
|
+
return {
|
|
1506
|
+
lastLine: -1,
|
|
1507
|
+
lastOffset: __rspack_external_node_fs_5ea92f0c.statSync(filePath).size
|
|
1508
|
+
};
|
|
1509
|
+
} catch {
|
|
1510
|
+
return {
|
|
1511
|
+
lastLine: -1,
|
|
1512
|
+
lastOffset: 0
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function hashId(value) {
|
|
1517
|
+
return external_node_crypto_createHash('sha256').update(JSON.stringify(value ?? '')).digest('hex').slice(0, 12);
|
|
1518
|
+
}
|
|
1519
|
+
function syntheticTime(baseMs, entry, blockIndex = 0) {
|
|
1520
|
+
return new Date(baseMs + 10 * Math.max(0, entry.__line ?? 0) + blockIndex).toISOString();
|
|
1521
|
+
}
|
|
1522
|
+
function asUsage(value) {
|
|
1523
|
+
return value && 'object' == typeof value ? value : {};
|
|
1524
|
+
}
|
|
1525
|
+
function textFromUnknown(value) {
|
|
1526
|
+
if ('string' == typeof value) return value;
|
|
1527
|
+
if (Array.isArray(value)) return value.map((item)=>{
|
|
1528
|
+
if ('string' == typeof item) return item;
|
|
1529
|
+
if (item && 'object' == typeof item && 'string' == typeof item.text) return item.text;
|
|
1530
|
+
return '';
|
|
1531
|
+
}).join('');
|
|
1532
|
+
if (null == value) return '';
|
|
1533
|
+
return JSON.stringify(value);
|
|
1534
|
+
}
|
|
1535
|
+
function contentArray(entry) {
|
|
1536
|
+
const content = entry.message?.content;
|
|
1537
|
+
if (Array.isArray(content)) return content;
|
|
1538
|
+
if ('string' == typeof content) return [
|
|
1539
|
+
{
|
|
1540
|
+
type: 'text',
|
|
1541
|
+
text: content
|
|
1542
|
+
}
|
|
1543
|
+
];
|
|
1544
|
+
return [];
|
|
1545
|
+
}
|
|
1546
|
+
function normalizeToolUse(block, entry, blockIndex) {
|
|
1547
|
+
const name = String(block.name ?? block.tool_name ?? block.tool_type ?? 'tool');
|
|
1548
|
+
const input = block.input && 'object' == typeof block.input ? block.input : block.tool_input && 'object' == typeof block.tool_input ? block.tool_input : {};
|
|
1549
|
+
const id = String(block.id ?? block.tool_use_id ?? block.tool_call_id ?? `cursor-tool-${entry.__line ?? 'unknown'}-${blockIndex}-${hashId({
|
|
1550
|
+
name,
|
|
1551
|
+
input
|
|
1552
|
+
})}`);
|
|
1553
|
+
return {
|
|
1554
|
+
type: 'tool_use',
|
|
1555
|
+
id,
|
|
1556
|
+
name,
|
|
1557
|
+
input
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function normalizeContent(entry) {
|
|
1561
|
+
const blocks = [];
|
|
1562
|
+
const toolResults = [];
|
|
1563
|
+
for (const [index, raw] of contentArray(entry).entries()){
|
|
1564
|
+
if ('string' == typeof raw) {
|
|
1565
|
+
blocks.push({
|
|
1566
|
+
type: 'text',
|
|
1567
|
+
text: raw
|
|
1568
|
+
});
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (!raw || 'object' != typeof raw) continue;
|
|
1572
|
+
const block = raw;
|
|
1573
|
+
const type = String(block.type ?? '');
|
|
1574
|
+
if ('text' === type && 'string' == typeof block.text) {
|
|
1575
|
+
const previous = blocks.at(-1);
|
|
1576
|
+
if (previous?.type === 'text') previous.text += block.text;
|
|
1577
|
+
else blocks.push({
|
|
1578
|
+
type: 'text',
|
|
1579
|
+
text: block.text
|
|
1580
|
+
});
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if ('thinking' === type || 'reasoning' === type) {
|
|
1584
|
+
blocks.push({
|
|
1585
|
+
type,
|
|
1586
|
+
thinking: 'string' == typeof block.thinking ? block.thinking : textFromUnknown(block.text)
|
|
1587
|
+
});
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if ('tool_use' === type) {
|
|
1591
|
+
blocks.push(normalizeToolUse(block, entry, index));
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if ('tool_result' === type) toolResults.push({
|
|
1595
|
+
type: 'tool_result',
|
|
1596
|
+
tool_use_id: 'string' == typeof block.tool_use_id ? block.tool_use_id : void 0,
|
|
1597
|
+
content: block.content,
|
|
1598
|
+
is_error: Boolean(block.is_error)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
blocks,
|
|
1603
|
+
toolResults
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function isUserEntry(entry) {
|
|
1607
|
+
return 'user' === entry.role || entry.message?.role === 'user';
|
|
1608
|
+
}
|
|
1609
|
+
function isAssistantEntry(entry) {
|
|
1610
|
+
return 'assistant' === entry.role || entry.message?.role === 'assistant';
|
|
1611
|
+
}
|
|
1612
|
+
function isTurnEnd(entry) {
|
|
1613
|
+
return 'turn_ended' === entry.type;
|
|
1614
|
+
}
|
|
1615
|
+
function userContent(entry) {
|
|
1616
|
+
const normalized = normalizeContent(entry);
|
|
1617
|
+
if (!normalized.blocks.length) return '';
|
|
1618
|
+
if (normalized.blocks.every((block)=>'text' === block.type)) return normalized.blocks.map((block)=>block.text).join('');
|
|
1619
|
+
return normalized.blocks;
|
|
1620
|
+
}
|
|
1621
|
+
function findToolResult(toolUseId, toolResults) {
|
|
1622
|
+
const matched = toolResults.find((item)=>item.result.tool_use_id === toolUseId);
|
|
1623
|
+
if (!matched) return;
|
|
1624
|
+
return {
|
|
1625
|
+
content: textFromUnknown(matched.result.content),
|
|
1626
|
+
timestamp: matched.timestamp,
|
|
1627
|
+
is_error: matched.result.is_error
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function groupIntoTurns(messages, options = {}) {
|
|
1631
|
+
const turns = [];
|
|
1632
|
+
const baseMs = options.fallbackStartMs ?? Date.now();
|
|
1633
|
+
let currentUser = null;
|
|
1634
|
+
let assistantEntries = [];
|
|
1635
|
+
let toolResults = [];
|
|
1636
|
+
let currentTurnEndOffset;
|
|
1637
|
+
let complete = false;
|
|
1638
|
+
function finalize(forceIncomplete = false) {
|
|
1639
|
+
if (!currentUser) return;
|
|
1640
|
+
const llmCalls = assistantEntries.map((entry)=>{
|
|
1641
|
+
const normalized = normalizeContent(entry);
|
|
1642
|
+
const content = normalized.blocks;
|
|
1643
|
+
for (const result of normalized.toolResults)toolResults.push({
|
|
1644
|
+
result,
|
|
1645
|
+
timestamp: entry.timestamp ?? syntheticTime(baseMs, entry)
|
|
1646
|
+
});
|
|
1647
|
+
if (!content.length) return;
|
|
1648
|
+
const startTime = entry.timestamp ?? syntheticTime(baseMs, entry);
|
|
1649
|
+
const endTime = entry.timestamp ?? syntheticTime(baseMs, entry, content.length);
|
|
1650
|
+
const toolCalls = content.filter((block)=>'tool_use' === block.type).map((toolUse)=>{
|
|
1651
|
+
const result = findToolResult(toolUse.id, toolResults);
|
|
1652
|
+
return result ? {
|
|
1653
|
+
tool_use: toolUse,
|
|
1654
|
+
result
|
|
1655
|
+
} : {
|
|
1656
|
+
tool_use: toolUse
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1659
|
+
return {
|
|
1660
|
+
content,
|
|
1661
|
+
model: entry.message?.model ?? options.fallbackModel ?? 'cursor-agent',
|
|
1662
|
+
usage: asUsage(entry.message?.usage),
|
|
1663
|
+
startTime,
|
|
1664
|
+
endTime,
|
|
1665
|
+
toolCalls,
|
|
1666
|
+
stopReason: entry.message?.stop_reason
|
|
1667
|
+
};
|
|
1668
|
+
}).filter((call)=>Boolean(call));
|
|
1669
|
+
turns.push({
|
|
1670
|
+
userContent: userContent(currentUser),
|
|
1671
|
+
userTimestamp: currentUser.timestamp ?? syntheticTime(baseMs, currentUser),
|
|
1672
|
+
promptId: currentUser.promptId,
|
|
1673
|
+
llmCalls,
|
|
1674
|
+
isComplete: complete && !forceIncomplete,
|
|
1675
|
+
startOffset: currentUser.__startOffset,
|
|
1676
|
+
endOffset: currentTurnEndOffset ?? assistantEntries.at(-1)?.__endOffset ?? currentUser.__endOffset
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
for (const entry of messages){
|
|
1680
|
+
if (isUserEntry(entry)) {
|
|
1681
|
+
finalize();
|
|
1682
|
+
currentUser = entry;
|
|
1683
|
+
assistantEntries = [];
|
|
1684
|
+
toolResults = [];
|
|
1685
|
+
currentTurnEndOffset = void 0;
|
|
1686
|
+
complete = false;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (currentUser) {
|
|
1690
|
+
if (isAssistantEntry(entry)) {
|
|
1691
|
+
assistantEntries.push(entry);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
if (isTurnEnd(entry)) {
|
|
1695
|
+
complete = 'error' !== entry.status;
|
|
1696
|
+
currentTurnEndOffset = entry.__endOffset;
|
|
1697
|
+
finalize();
|
|
1698
|
+
currentUser = null;
|
|
1699
|
+
assistantEntries = [];
|
|
1700
|
+
toolResults = [];
|
|
1701
|
+
currentTurnEndOffset = void 0;
|
|
1702
|
+
complete = false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
finalize(true);
|
|
1707
|
+
return turns;
|
|
1708
|
+
}
|
|
1709
|
+
function trace_flow_expandHome(filePath) {
|
|
1710
|
+
if (!filePath) return;
|
|
1711
|
+
if ('~' === filePath) return homedir();
|
|
1712
|
+
if (filePath.startsWith('~/')) return __rspack_external_node_path_c5b9b54f.join(homedir(), filePath.slice(2));
|
|
1713
|
+
return filePath;
|
|
1714
|
+
}
|
|
1715
|
+
function trace_flow_sessionIdForInput(input, transcriptPath) {
|
|
1716
|
+
if (input.session_id) return input.session_id;
|
|
1717
|
+
if (input.conversation_id) return input.conversation_id;
|
|
1718
|
+
const resolvedTranscript = transcriptPath ?? trace_flow_expandHome(input.transcript_path ?? void 0);
|
|
1719
|
+
if (!resolvedTranscript) return;
|
|
1720
|
+
const parts = resolvedTranscript.split(__rspack_external_node_path_c5b9b54f.sep).filter(Boolean);
|
|
1721
|
+
const transcriptFile = parts.at(-1)?.replace(/\.jsonl$/i, '');
|
|
1722
|
+
const parent = parts.at(-2);
|
|
1723
|
+
return transcriptFile || parent;
|
|
1724
|
+
}
|
|
1725
|
+
function sanitizeFileName(value) {
|
|
1726
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 160) || 'trace';
|
|
1727
|
+
}
|
|
1728
|
+
async function writeSendLog(config, record) {
|
|
1729
|
+
await writeJsonFileAtomic(__rspack_external_node_path_c5b9b54f.join(config.sendLogsDir, 'latest.send-log.json'), record);
|
|
1730
|
+
await appendBoundedJsonlFile(__rspack_external_node_path_c5b9b54f.join(config.dataRoot, 'logs', 'diagnostics', 'send-log.jsonl'), record, config.diagnosticLogMaxBytes);
|
|
1731
|
+
}
|
|
1732
|
+
async function writeDebugArtifacts(config, trace) {
|
|
1733
|
+
if (!config.debugArtifacts) return {};
|
|
1734
|
+
const baseName = sanitizeFileName(`${trace.trace_id}.${Date.now()}`);
|
|
1735
|
+
const tracePath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'otel-traces', `${baseName}.trace.json`);
|
|
1736
|
+
const payloadPath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'otlp-payloads', `${baseName}.otlp.json`);
|
|
1737
|
+
const schemaPath = __rspack_external_node_path_c5b9b54f.join(config.artifactsDir, 'schema-aligned', `${baseName}.schema-aligned.json`);
|
|
1738
|
+
await writeJsonFileAtomic(tracePath, {
|
|
1739
|
+
generated_at: new Date().toISOString(),
|
|
1740
|
+
trace
|
|
1741
|
+
});
|
|
1742
|
+
await writeJsonFileAtomic(payloadPath, buildOtlpPayload(trace, config));
|
|
1743
|
+
await writeJsonFileAtomic(schemaPath, {
|
|
1744
|
+
generated_at: new Date().toISOString(),
|
|
1745
|
+
records: buildSchemaAlignedRecords(trace)
|
|
1746
|
+
});
|
|
1747
|
+
return {
|
|
1748
|
+
trace_path: tracePath,
|
|
1749
|
+
otlp_payload_path: payloadPath,
|
|
1750
|
+
schema_aligned_path: schemaPath
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
async function trace_flow_exportTraceAndLog(options) {
|
|
1754
|
+
const { config, trace, hookEventName } = options;
|
|
1755
|
+
const artifacts = await writeDebugArtifacts(config, trace);
|
|
1756
|
+
const result = await exportTraceToTls(trace, config);
|
|
1757
|
+
await writeSendLog(config, {
|
|
1758
|
+
...exportSummary(trace, result),
|
|
1759
|
+
hook_event_name: hookEventName,
|
|
1760
|
+
capture_content: config.captureContent,
|
|
1761
|
+
debug_artifacts: config.debugArtifacts,
|
|
1762
|
+
...artifacts
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
function synthesizeMissingFinalAssistant(turns, input, turnEndTimeMs) {
|
|
1766
|
+
if (!turns.length || !input.last_assistant_message) return;
|
|
1767
|
+
const lastTurn = turns[turns.length - 1];
|
|
1768
|
+
const lastLlm = lastTurn.llmCalls.at(-1);
|
|
1769
|
+
if (!lastLlm || 0 === lastLlm.toolCalls.length) return;
|
|
1770
|
+
const lastToolEnd = Date.parse(lastLlm.endTime);
|
|
1771
|
+
lastTurn.llmCalls.push({
|
|
1772
|
+
content: [
|
|
1773
|
+
{
|
|
1774
|
+
type: 'text',
|
|
1775
|
+
text: input.last_assistant_message
|
|
1776
|
+
}
|
|
1777
|
+
],
|
|
1778
|
+
model: lastLlm.model,
|
|
1779
|
+
usage: {},
|
|
1780
|
+
startTime: new Date(lastToolEnd).toISOString(),
|
|
1781
|
+
endTime: new Date(turnEndTimeMs).toISOString(),
|
|
1782
|
+
toolCalls: [],
|
|
1783
|
+
synthetic: true
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
async function exportTranscriptTurns(options) {
|
|
1787
|
+
const turnEndTimeMs = options.turnEndTimeMs ?? Date.now();
|
|
1788
|
+
const { messages, lastLine, lastOffset } = readTranscript(options.transcriptPath, options.afterLine, options.afterOffset ?? 0, {
|
|
1789
|
+
lookBehindBytes: options.config.transcriptLookbehindBytes
|
|
1790
|
+
});
|
|
1791
|
+
const parsedTurns = groupIntoTurns(messages, {
|
|
1792
|
+
fallbackStartMs: options.session.current_turn_start_ms,
|
|
1793
|
+
fallbackModel: 'string' == typeof options.input.model ? options.input.model : options.input.model?.id ?? options.input.model?.display_name
|
|
1794
|
+
});
|
|
1795
|
+
const afterOffset = options.afterOffset ?? 0;
|
|
1796
|
+
const turns = afterOffset > 0 ? parsedTurns.filter((turn)=>void 0 === turn.endOffset || turn.endOffset > afterOffset) : parsedTurns;
|
|
1797
|
+
if (options.synthesizeFinalAssistant) synthesizeMissingFinalAssistant(turns, options.input, turnEndTimeMs);
|
|
1798
|
+
let tracedTurns = 0;
|
|
1799
|
+
for (const turn of turns){
|
|
1800
|
+
const trace = buildTraceFromTurn({
|
|
1801
|
+
input: options.input,
|
|
1802
|
+
turn,
|
|
1803
|
+
turnIndex: (options.turnIndexBase ?? options.session.turn_count) + tracedTurns + 1,
|
|
1804
|
+
sessionState: options.session,
|
|
1805
|
+
config: options.config,
|
|
1806
|
+
turnEndTimeMs,
|
|
1807
|
+
source: options.source,
|
|
1808
|
+
traceSeedSuffix: options.traceSeedSuffix,
|
|
1809
|
+
extraAttributes: options.extraAttributes
|
|
1810
|
+
});
|
|
1811
|
+
await trace_flow_exportTraceAndLog({
|
|
1812
|
+
config: options.config,
|
|
1813
|
+
trace,
|
|
1814
|
+
hookEventName: options.hookEventName
|
|
1815
|
+
});
|
|
1816
|
+
tracedTurns += 1;
|
|
1817
|
+
}
|
|
1818
|
+
return {
|
|
1819
|
+
lastLine,
|
|
1820
|
+
lastOffset,
|
|
1821
|
+
tracedTurns
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
const TRANSCRIPT_FLUSH_DELAY_MS = 200;
|
|
1825
|
+
function clearedActiveSession(session, updates = {}) {
|
|
1826
|
+
return {
|
|
1827
|
+
...session,
|
|
1828
|
+
...updates,
|
|
1829
|
+
current_turn_id: void 0,
|
|
1830
|
+
current_turn_start_ms: void 0,
|
|
1831
|
+
current_prompt: void 0,
|
|
1832
|
+
tool_start_times: {},
|
|
1833
|
+
tool_observations: {},
|
|
1834
|
+
tool_observation_order: [],
|
|
1835
|
+
pending_subagent_traces: [],
|
|
1836
|
+
updated: new Date().toISOString()
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async function waitForTranscriptFlush() {
|
|
1840
|
+
await new Promise((resolve)=>setTimeout(resolve, TRANSCRIPT_FLUSH_DELAY_MS));
|
|
1841
|
+
}
|
|
1842
|
+
async function handleStop(input, config) {
|
|
1843
|
+
if (input.stop_hook_active) return;
|
|
1844
|
+
const stopTimeMs = Date.now();
|
|
1845
|
+
const transcriptPath = trace_flow_expandHome(input.transcript_path ?? void 0);
|
|
1846
|
+
const sessionId = trace_flow_sessionIdForInput(input, transcriptPath);
|
|
1847
|
+
if (!sessionId) return;
|
|
1848
|
+
await waitForTranscriptFlush();
|
|
1849
|
+
const session = state_getSessionState(state_loadState(config.stateFilePath), sessionId);
|
|
1850
|
+
let tracedTurns = 0;
|
|
1851
|
+
let lastLine = session.last_line;
|
|
1852
|
+
let lastOffset = session.last_offset ?? 0;
|
|
1853
|
+
const stopInput = {
|
|
1854
|
+
...input,
|
|
1855
|
+
hook_event_name: 'stop',
|
|
1856
|
+
session_id: input.session_id ?? sessionId,
|
|
1857
|
+
conversation_id: input.conversation_id ?? sessionId,
|
|
1858
|
+
transcript_path: transcriptPath
|
|
1859
|
+
};
|
|
1860
|
+
if (transcriptPath) {
|
|
1861
|
+
const result = await exportTranscriptTurns({
|
|
1862
|
+
input: stopInput,
|
|
1863
|
+
config,
|
|
1864
|
+
transcriptPath,
|
|
1865
|
+
afterLine: session.last_line,
|
|
1866
|
+
afterOffset: session.last_offset ?? 0,
|
|
1867
|
+
session,
|
|
1868
|
+
hookEventName: input.hook_event_name,
|
|
1869
|
+
turnEndTimeMs: stopTimeMs,
|
|
1870
|
+
source: "cursor_stop_hook_transcript"
|
|
1871
|
+
});
|
|
1872
|
+
tracedTurns = result.tracedTurns;
|
|
1873
|
+
lastLine = result.lastLine;
|
|
1874
|
+
lastOffset = result.lastOffset;
|
|
1875
|
+
}
|
|
1876
|
+
if (0 === tracedTurns) {
|
|
1877
|
+
const hookTrace = buildTraceFromHookState({
|
|
1878
|
+
input: stopInput,
|
|
1879
|
+
sessionState: session,
|
|
1880
|
+
config,
|
|
1881
|
+
turnIndex: session.turn_count + 1,
|
|
1882
|
+
stopTimeMs,
|
|
1883
|
+
source: 'cursor_stop_hook_state'
|
|
1884
|
+
});
|
|
1885
|
+
if (hookTrace) {
|
|
1886
|
+
await trace_flow_exportTraceAndLog({
|
|
1887
|
+
config,
|
|
1888
|
+
trace: hookTrace,
|
|
1889
|
+
hookEventName: input.hook_event_name
|
|
1890
|
+
});
|
|
1891
|
+
tracedTurns = 1;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
await state_atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1895
|
+
const latestSession = state_getSessionState(state, sessionId);
|
|
1896
|
+
return {
|
|
1897
|
+
...state,
|
|
1898
|
+
[sessionId]: clearedActiveSession(latestSession, {
|
|
1899
|
+
last_line: lastLine,
|
|
1900
|
+
last_offset: lastOffset,
|
|
1901
|
+
turn_count: latestSession.turn_count + tracedTurns,
|
|
1902
|
+
subagent_turn_count: latestSession.subagent_turn_count ?? 0
|
|
1903
|
+
})
|
|
1904
|
+
};
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
async function handleSessionEnd(input, config) {
|
|
1908
|
+
const stopTimeMs = Date.now();
|
|
1909
|
+
const transcriptPath = trace_flow_expandHome(input.transcript_path ?? void 0);
|
|
1910
|
+
const sessionId = trace_flow_sessionIdForInput(input, transcriptPath);
|
|
1911
|
+
if (!sessionId) return;
|
|
1912
|
+
const session = state_getSessionState(state_loadState(config.stateFilePath), sessionId);
|
|
1913
|
+
if (!session.current_turn_id) return;
|
|
1914
|
+
await waitForTranscriptFlush();
|
|
1915
|
+
let tracedTurns = 0;
|
|
1916
|
+
let lastLine = session.last_line;
|
|
1917
|
+
let lastOffset = session.last_offset ?? 0;
|
|
1918
|
+
const stopInput = {
|
|
1919
|
+
...input,
|
|
1920
|
+
hook_event_name: 'stop',
|
|
1921
|
+
session_id: input.session_id ?? sessionId,
|
|
1922
|
+
conversation_id: input.conversation_id ?? sessionId,
|
|
1923
|
+
transcript_path: transcriptPath
|
|
1924
|
+
};
|
|
1925
|
+
if (session.current_turn_id && transcriptPath) {
|
|
1926
|
+
const result = await exportTranscriptTurns({
|
|
1927
|
+
input: stopInput,
|
|
1928
|
+
config,
|
|
1929
|
+
transcriptPath,
|
|
1930
|
+
afterLine: session.last_line,
|
|
1931
|
+
afterOffset: session.last_offset ?? 0,
|
|
1932
|
+
session,
|
|
1933
|
+
hookEventName: input.hook_event_name,
|
|
1934
|
+
turnEndTimeMs: stopTimeMs,
|
|
1935
|
+
source: "cursor_session_end_transcript",
|
|
1936
|
+
traceSeedSuffix: `session-end:${input.reason ?? 'unknown'}`
|
|
1937
|
+
});
|
|
1938
|
+
tracedTurns = result.tracedTurns;
|
|
1939
|
+
lastLine = result.lastLine;
|
|
1940
|
+
lastOffset = result.lastOffset;
|
|
1941
|
+
}
|
|
1942
|
+
if (session.current_turn_id && 0 === tracedTurns) {
|
|
1943
|
+
const hookTrace = buildTraceFromHookState({
|
|
1944
|
+
input: stopInput,
|
|
1945
|
+
sessionState: session,
|
|
1946
|
+
config,
|
|
1947
|
+
turnIndex: session.turn_count + 1,
|
|
1948
|
+
stopTimeMs,
|
|
1949
|
+
source: 'cursor_session_end_hook_state',
|
|
1950
|
+
traceSeedSuffix: `session-end:${input.reason ?? 'unknown'}`
|
|
1951
|
+
});
|
|
1952
|
+
if (hookTrace) {
|
|
1953
|
+
await trace_flow_exportTraceAndLog({
|
|
1954
|
+
config,
|
|
1955
|
+
trace: hookTrace,
|
|
1956
|
+
hookEventName: input.hook_event_name
|
|
1957
|
+
});
|
|
1958
|
+
tracedTurns = 1;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
await state_atomicUpdateState(config.stateFilePath, (state)=>{
|
|
1962
|
+
const latestSession = state_getSessionState(state, sessionId);
|
|
1963
|
+
return {
|
|
1964
|
+
...state,
|
|
1965
|
+
[sessionId]: clearedActiveSession(latestSession, {
|
|
1966
|
+
last_line: lastLine,
|
|
1967
|
+
last_offset: lastOffset,
|
|
1968
|
+
turn_count: latestSession.turn_count + tracedTurns,
|
|
1969
|
+
subagent_turn_count: latestSession.subagent_turn_count ?? 0
|
|
1970
|
+
})
|
|
1971
|
+
};
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
function promptText(input) {
|
|
1975
|
+
if ('string' == typeof input.prompt) return input.prompt;
|
|
1976
|
+
if ('string' == typeof input.user_prompt) return input.user_prompt;
|
|
1977
|
+
if ('string' == typeof input.query) return input.query;
|
|
1978
|
+
if ('string' == typeof input.text) return input.text;
|
|
1979
|
+
}
|
|
1980
|
+
async function handleUserPromptSubmit(input, config) {
|
|
1981
|
+
const transcriptPath = trace_flow_expandHome(input.transcript_path ?? void 0);
|
|
1982
|
+
const sessionId = trace_flow_sessionIdForInput(input, transcriptPath);
|
|
1983
|
+
if (!sessionId) return;
|
|
1984
|
+
const startMs = Date.now();
|
|
1985
|
+
let interruptedLastLine;
|
|
1986
|
+
let interruptedLastOffset;
|
|
1987
|
+
let interruptedTurns = 0;
|
|
1988
|
+
const previousSession = state_getSessionState(state_loadState(config.stateFilePath), sessionId);
|
|
1989
|
+
if (previousSession.current_turn_id) {
|
|
1990
|
+
const stopInput = {
|
|
1991
|
+
...input,
|
|
1992
|
+
hook_event_name: 'stop',
|
|
1993
|
+
session_id: input.session_id ?? sessionId,
|
|
1994
|
+
conversation_id: input.conversation_id ?? sessionId,
|
|
1995
|
+
transcript_path: transcriptPath
|
|
1996
|
+
};
|
|
1997
|
+
if (transcriptPath) {
|
|
1998
|
+
const result = await exportTranscriptTurns({
|
|
1999
|
+
input: stopInput,
|
|
2000
|
+
config,
|
|
2001
|
+
transcriptPath,
|
|
2002
|
+
afterLine: previousSession.last_line,
|
|
2003
|
+
afterOffset: previousSession.last_offset ?? 0,
|
|
2004
|
+
session: previousSession,
|
|
2005
|
+
hookEventName: input.hook_event_name,
|
|
2006
|
+
source: "cursor_interrupted_turn_transcript",
|
|
2007
|
+
traceSeedSuffix: 'interrupted-before-new-prompt'
|
|
2008
|
+
});
|
|
2009
|
+
interruptedTurns = result.tracedTurns;
|
|
2010
|
+
interruptedLastLine = result.lastLine;
|
|
2011
|
+
interruptedLastOffset = result.lastOffset;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
const endPosition = transcriptPath ? getTranscriptEndPosition(transcriptPath) : {
|
|
2015
|
+
lastLine: -1,
|
|
2016
|
+
lastOffset: 0
|
|
2017
|
+
};
|
|
2018
|
+
await state_atomicUpdateState(config.stateFilePath, (state)=>{
|
|
2019
|
+
const session = state_getSessionState(state, sessionId);
|
|
2020
|
+
return {
|
|
2021
|
+
...state,
|
|
2022
|
+
[sessionId]: {
|
|
2023
|
+
...session,
|
|
2024
|
+
last_line: interruptedLastLine ?? endPosition.lastLine,
|
|
2025
|
+
last_offset: interruptedLastOffset ?? endPosition.lastOffset,
|
|
2026
|
+
turn_count: session.turn_count + interruptedTurns,
|
|
2027
|
+
subagent_turn_count: session.subagent_turn_count ?? 0,
|
|
2028
|
+
current_turn_id: `${sessionId}:${startMs}`,
|
|
2029
|
+
current_turn_start_ms: startMs,
|
|
2030
|
+
current_prompt: promptText(input),
|
|
2031
|
+
tool_start_times: {},
|
|
2032
|
+
tool_observations: {},
|
|
2033
|
+
tool_observation_order: [],
|
|
2034
|
+
pending_subagent_traces: [],
|
|
2035
|
+
updated: new Date().toISOString()
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
function eventName(input) {
|
|
2041
|
+
return String(input.hook_event_name ?? input.event_name ?? '');
|
|
2042
|
+
}
|
|
2043
|
+
async function dispatchHook(input, config) {
|
|
2044
|
+
switch(eventName(input)){
|
|
2045
|
+
case 'beforeSubmitPrompt':
|
|
2046
|
+
case 'UserPromptSubmit':
|
|
2047
|
+
await handleUserPromptSubmit(input, config);
|
|
2048
|
+
return;
|
|
2049
|
+
case 'stop':
|
|
2050
|
+
case 'Stop':
|
|
2051
|
+
await handleStop(input, config);
|
|
2052
|
+
return;
|
|
2053
|
+
case 'sessionEnd':
|
|
2054
|
+
case 'SessionEnd':
|
|
2055
|
+
await handleSessionEnd(input, config);
|
|
2056
|
+
return;
|
|
2057
|
+
default:
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
async function readStdinJson() {
|
|
2062
|
+
const text = await new Promise((resolve, reject)=>{
|
|
2063
|
+
let input = '';
|
|
2064
|
+
process.stdin.setEncoding('utf8');
|
|
2065
|
+
process.stdin.on('data', (chunk)=>{
|
|
2066
|
+
input += chunk;
|
|
2067
|
+
});
|
|
2068
|
+
process.stdin.on('end', ()=>resolve(input));
|
|
2069
|
+
process.stdin.on('error', reject);
|
|
2070
|
+
});
|
|
2071
|
+
if (!text.trim()) return {};
|
|
2072
|
+
return JSON.parse(text);
|
|
2073
|
+
}
|
|
2074
|
+
async function main() {
|
|
2075
|
+
try {
|
|
2076
|
+
const input = await readStdinJson();
|
|
2077
|
+
const config = loadConfig();
|
|
2078
|
+
await __rspack_external_node_fs_promises_153e37e0.mkdir(config.dataRoot, {
|
|
2079
|
+
recursive: true
|
|
2080
|
+
});
|
|
2081
|
+
await dispatchHook(input, config);
|
|
2082
|
+
} finally{
|
|
2083
|
+
await closeTraceLogProducers();
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
main().catch(async (error)=>{
|
|
2087
|
+
try {
|
|
2088
|
+
const config = loadConfig();
|
|
2089
|
+
await writeSendLog(config, {
|
|
2090
|
+
generated_at: new Date().toISOString(),
|
|
2091
|
+
export_status: 'failed',
|
|
2092
|
+
reason: 'hook_error',
|
|
2093
|
+
message: error instanceof Error ? error.stack || error.message : String(error)
|
|
2094
|
+
});
|
|
2095
|
+
} catch {}
|
|
2096
|
+
process.exit(0);
|
|
2097
|
+
});
|