@respan/cli 0.6.9 → 0.7.1

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.
@@ -142,6 +142,36 @@ function detectModel(hookData) {
142
142
  return String(llmReq.model ?? '') || 'gemini-cli';
143
143
  }
144
144
  // ── Span construction ─────────────────────────────────────────────
145
+ function buildToolSpan(detail, idx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime) {
146
+ const toolName = detail?.name ?? '';
147
+ const toolArgs = detail?.args ?? detail?.input ?? {};
148
+ const toolOutput = detail?.output ?? '';
149
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${idx + 1}`;
150
+ const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
151
+ const toolMeta = {};
152
+ if (toolName)
153
+ toolMeta.tool_name = toolName;
154
+ if (detail?.error)
155
+ toolMeta.error = detail.error;
156
+ const toolStart = detail?.start_time ?? beginTime;
157
+ const toolEnd = detail?.end_time ?? endTime;
158
+ const toolLat = latencySeconds(toolStart, toolEnd);
159
+ return {
160
+ trace_unique_id: traceUniqueId,
161
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${idx + 1}`,
162
+ span_parent_id: rootSpanId,
163
+ span_name: `Tool: ${displayName}`,
164
+ span_workflow_name: workflowName,
165
+ span_path: toolName ? `tool_${toolName}` : 'tool_call',
166
+ provider_id: '',
167
+ metadata: toolMeta,
168
+ input: toolInputStr,
169
+ output: truncate(toolOutput, MAX_CHARS),
170
+ timestamp: toolEnd,
171
+ start_time: toolStart,
172
+ ...(toolLat !== undefined ? { latency: toolLat } : {}),
173
+ };
174
+ }
145
175
  function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
146
176
  const spans = [];
147
177
  const sessionId = String(hookData.session_id ?? '');
@@ -233,76 +263,18 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
233
263
  }
234
264
  // Tool spans that come after this round (before next round)
235
265
  if (r < rounds.length - 1) {
236
- // Emit all tools between this round and the next
237
266
  while (toolIdx < toolDetails.length) {
238
- const detail = toolDetails[toolIdx];
239
- const toolName = detail?.name ?? '';
240
- const toolArgs = detail?.args ?? detail?.input ?? {};
241
- const toolOutput = detail?.output ?? '';
242
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
243
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
244
- const toolMeta = {};
245
- if (toolName)
246
- toolMeta.tool_name = toolName;
247
- if (detail?.error)
248
- toolMeta.error = detail.error;
249
- const toolStart = detail?.start_time ?? beginTime;
250
- const toolEnd = detail?.end_time ?? endTime;
251
- const toolLat = latencySeconds(toolStart, toolEnd);
252
- spans.push({
253
- trace_unique_id: traceUniqueId,
254
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
255
- span_parent_id: rootSpanId,
256
- span_name: `Tool: ${displayName}`,
257
- span_workflow_name: workflowName,
258
- span_path: toolName ? `tool_${toolName}` : 'tool_call',
259
- provider_id: '',
260
- metadata: toolMeta,
261
- input: toolInputStr,
262
- output: truncate(toolOutput, MAX_CHARS),
263
- timestamp: toolEnd,
264
- start_time: toolStart,
265
- ...(toolLat !== undefined ? { latency: toolLat } : {}),
266
- });
267
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
267
268
  toolIdx++;
268
- // If next tool starts after next round's start time, break — it belongs to a later gap
269
269
  const nextDetail = toolDetails[toolIdx];
270
270
  if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1])
271
271
  break;
272
272
  }
273
273
  }
274
274
  }
275
- // Any remaining tools not yet emitted (e.g. only one round but tools exist)
275
+ // Any remaining tools not yet emitted
276
276
  while (toolIdx < toolDetails.length) {
277
- const detail = toolDetails[toolIdx];
278
- const toolName = detail?.name ?? '';
279
- const toolArgs = detail?.args ?? detail?.input ?? {};
280
- const toolOutput = detail?.output ?? '';
281
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
282
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
283
- const toolMeta = {};
284
- if (toolName)
285
- toolMeta.tool_name = toolName;
286
- if (detail?.error)
287
- toolMeta.error = detail.error;
288
- const toolStart = detail?.start_time ?? beginTime;
289
- const toolEnd = detail?.end_time ?? endTime;
290
- const toolLat = latencySeconds(toolStart, toolEnd);
291
- spans.push({
292
- trace_unique_id: traceUniqueId,
293
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
294
- span_parent_id: rootSpanId,
295
- span_name: `Tool: ${displayName}`,
296
- span_workflow_name: workflowName,
297
- span_path: toolName ? `tool_${toolName}` : 'tool_call',
298
- provider_id: '',
299
- metadata: toolMeta,
300
- input: toolInputStr,
301
- output: truncate(toolOutput, MAX_CHARS),
302
- timestamp: toolEnd,
303
- start_time: toolStart,
304
- ...(toolLat !== undefined ? { latency: toolLat } : {}),
305
- });
277
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
306
278
  toolIdx++;
307
279
  }
308
280
  // Reasoning span
@@ -448,7 +420,6 @@ function processBeforeTool(hookData) {
448
420
  // Increment send_version to cancel any pending delayed sends —
449
421
  // the turn isn't done yet, a tool is about to execute.
450
422
  state.send_version = (state.send_version ?? 0) + 1;
451
- state.tool_turns = (state.tool_turns ?? 0) + 1;
452
423
  saveStreamState(sessionId, state);
453
424
  }
454
425
  function processAfterTool(hookData) {
@@ -624,66 +595,87 @@ function processChunk(hookData) {
624
595
  launchDelayedSend(sessionId, state.send_version, spans, creds.apiKey, creds.baseUrl);
625
596
  }
626
597
  // ── Main ──────────────────────────────────────────────────────────
627
- function mainWorker(raw) {
598
+ function processChunkInWorker(dataFile) {
628
599
  try {
600
+ const raw = fs.readFileSync(dataFile, 'utf-8');
601
+ fs.unlinkSync(dataFile);
629
602
  if (!raw.trim())
630
603
  return;
631
604
  const hookData = JSON.parse(raw);
632
- const event = String(hookData.hook_event_name ?? '');
633
605
  const unlock = acquireLock(LOCK_PATH);
634
606
  try {
635
- if (event === 'BeforeTool') {
636
- processBeforeTool(hookData);
637
- }
638
- else if (event === 'AfterTool') {
639
- processAfterTool(hookData);
640
- }
641
- else {
642
- processChunk(hookData);
643
- }
607
+ processChunk(hookData);
644
608
  }
645
609
  finally {
646
610
  unlock?.();
647
611
  }
648
612
  }
649
613
  catch (e) {
650
- if (e instanceof SyntaxError) {
651
- log('ERROR', `Invalid JSON from stdin: ${e}`);
652
- }
653
- else {
654
- log('ERROR', `Hook error: ${e}`);
614
+ log('ERROR', `Worker error: ${e}`);
615
+ try {
616
+ fs.unlinkSync(dataFile);
655
617
  }
618
+ catch { }
656
619
  }
657
620
  }
658
621
  function main() {
659
- // Worker mode: re-invoked as detached subprocess
622
+ // Worker mode: process chunk from temp file
660
623
  if (process.env._RESPAN_GEM_WORKER === '1') {
661
- const raw = process.env._RESPAN_GEM_DATA ?? '';
662
- mainWorker(raw);
624
+ const dataFile = process.env._RESPAN_GEM_FILE ?? '';
625
+ if (dataFile)
626
+ processChunkInWorker(dataFile);
663
627
  return;
664
628
  }
665
- // Read stdin synchronously, respond immediately, fork worker, exit
666
629
  let raw = '';
667
630
  try {
668
631
  raw = fs.readFileSync(0, 'utf-8');
669
632
  }
670
633
  catch { }
634
+ // Respond immediately so Gemini CLI doesn't block
671
635
  process.stdout.write('{}\n');
672
636
  if (!raw.trim()) {
673
637
  process.exit(0);
674
638
  }
675
639
  try {
676
- const scriptPath = __filename || process.argv[1];
677
- const child = execFile('node', [scriptPath], {
678
- env: { ...process.env, _RESPAN_GEM_WORKER: '1', _RESPAN_GEM_DATA: raw },
679
- stdio: 'ignore',
680
- detached: true,
681
- });
682
- child.unref();
640
+ const hookData = JSON.parse(raw);
641
+ const event = String(hookData.hook_event_name ?? '');
642
+ if (event === 'BeforeTool' || event === 'AfterTool') {
643
+ // Tool events are fast (just state updates) and must run in order.
644
+ // Process inline, don't fork.
645
+ const unlock = acquireLock(LOCK_PATH);
646
+ try {
647
+ if (event === 'BeforeTool')
648
+ processBeforeTool(hookData);
649
+ else
650
+ processAfterTool(hookData);
651
+ }
652
+ finally {
653
+ unlock?.();
654
+ }
655
+ }
656
+ else {
657
+ // AfterModel chunks: fork to background so Gemini CLI doesn't block.
658
+ // Write data to temp file (avoids env var size limits).
659
+ const dataFile = path.join(STATE_DIR, `respan_chunk_${process.pid}.json`);
660
+ fs.mkdirSync(STATE_DIR, { recursive: true });
661
+ fs.writeFileSync(dataFile, raw);
662
+ try {
663
+ const scriptPath = __filename || process.argv[1];
664
+ const child = execFile('node', [scriptPath], {
665
+ env: { ...process.env, _RESPAN_GEM_WORKER: '1', _RESPAN_GEM_FILE: dataFile },
666
+ stdio: 'ignore',
667
+ detached: true,
668
+ });
669
+ child.unref();
670
+ }
671
+ catch (e) {
672
+ // Fallback: run inline
673
+ processChunkInWorker(dataFile);
674
+ }
675
+ }
683
676
  }
684
677
  catch (e) {
685
- // Fallback: run inline
686
- mainWorker(raw);
678
+ log('ERROR', `Hook error: ${e}`);
687
679
  }
688
680
  process.exit(0);
689
681
  }
@@ -395,7 +395,7 @@ export function toOtlpPayload(spans) {
395
395
  }),
396
396
  },
397
397
  scopeSpans: [{
398
- scope: { name: 'respan-cli-hooks', version: '0.5.3' },
398
+ scope: { name: 'respan-cli-hooks', version: '0.7.0' },
399
399
  spans: otlpSpans,
400
400
  }],
401
401
  }],