@hatchway/cli 0.50.64 → 0.50.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4424,8 +4424,22 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4424
4424
  let messageCount = 0;
4425
4425
  let toolCallCount = 0;
4426
4426
  let textBlockCount = 0;
4427
- // Wrap the entire agent execution in a gen_ai.invoke_agent span for Sentry AI monitoring
4428
- const agentSpan = Sentry.startInactiveSpan({
4427
+ // Create the gen_ai.invoke_agent span using startSpanManual.
4428
+ //
4429
+ // Why startSpanManual and not startSpan?
4430
+ // startSpan() takes a callback and ends the span when the callback returns.
4431
+ // But this is an async generator — we can't yield from inside a callback.
4432
+ // startSpanManual() makes the span active on the current scope AND gives us
4433
+ // a handle to end it ourselves in the finally block.
4434
+ //
4435
+ // Why this works now (it didn't before):
4436
+ // engine.ts captures the parent build.runner span before creating the
4437
+ // ReadableStream, then restores it via Sentry.withActiveSpan() inside the
4438
+ // stream's start() callback. So when this generator runs, the build.runner
4439
+ // span is the active parent, and our gen_ai.invoke_agent becomes its child.
4440
+ // Tool spans created with startSpan() inside the loop become children of
4441
+ // gen_ai.invoke_agent because it's the active span at that point.
4442
+ const agentSpan = Sentry.startSpanManual({
4429
4443
  op: 'gen_ai.invoke_agent',
4430
4444
  name: `Claude Agent (${modelId})`,
4431
4445
  attributes: {
@@ -4435,7 +4449,8 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4435
4449
  'gen_ai.system_prompt.length': appendedSystemPrompt.length,
4436
4450
  'gen_ai.agent.available_tools': JSON.stringify(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'WebFetch']),
4437
4451
  },
4438
- });
4452
+ }, (span) => span // Return the span so we control its lifecycle
4453
+ );
4439
4454
  try {
4440
4455
  // Stream messages directly from the SDK
4441
4456
  for await (const sdkMessage of query({ prompt: finalPrompt, options })) {
@@ -4449,8 +4464,12 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4449
4464
  if (block.type === 'tool_use') {
4450
4465
  toolCallCount++;
4451
4466
  debugLog$4(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
4452
- // Emit a gen_ai.execute_tool span for each tool invocation
4453
- const toolSpan = Sentry.startInactiveSpan({
4467
+ // Emit a gen_ai.execute_tool span as a child of gen_ai.invoke_agent.
4468
+ // Using startSpan (active) with an empty callback — the span is created,
4469
+ // becomes briefly active, records the tool invocation, and ends when
4470
+ // the callback returns. This gives Sentry the tool call event with
4471
+ // proper parent-child nesting.
4472
+ Sentry.startSpan({
4454
4473
  op: 'gen_ai.execute_tool',
4455
4474
  name: `Tool: ${block.name}`,
4456
4475
  attributes: {
@@ -4458,10 +4477,9 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4458
4477
  'gen_ai.tool.call_id': block.id,
4459
4478
  'gen_ai.tool.input': JSON.stringify(block.input).substring(0, 1000),
4460
4479
  },
4480
+ }, () => {
4481
+ // Span created and ended — marks the tool invocation point
4461
4482
  });
4462
- // Tool spans are completed immediately since we get input and output
4463
- // as separate messages — the output is captured in the tool_result handler below
4464
- toolSpan?.end();
4465
4483
  }
4466
4484
  else if (block.type === 'text') {
4467
4485
  textBlockCount++;
@@ -7177,38 +7195,53 @@ async function createBuildStream(options) {
7177
7195
  debugLog$1();
7178
7196
  const generator = query(fullPrompt, actualWorkingDir, systemPrompt, agent, options.codexThreadId, messageParts);
7179
7197
  debugLog$1();
7198
+ // Capture the active Sentry span BEFORE creating the ReadableStream.
7199
+ // The ReadableStream.start() callback runs in a new async context where the
7200
+ // parent build.runner span is no longer active. We restore it with withActiveSpan()
7201
+ // so that gen_ai.invoke_agent spans created inside the query generator are
7202
+ // properly nested as children of the build.runner span.
7203
+ const parentSpan = Sentry.getActiveSpan();
7180
7204
  // Create a ReadableStream from the AsyncGenerator
7181
7205
  const stream = new ReadableStream({
7182
7206
  async start(controller) {
7183
7207
  debugLog$1();
7184
- let chunkCount = 0;
7185
- try {
7186
- for await (const chunk of generator) {
7187
- chunkCount++;
7188
- if (chunkCount % 5 === 0) {
7189
- debugLog$1(`[runner] [build-engine] Processed ${chunkCount} chunks from generator\n`);
7190
- }
7191
- // Convert chunk to appropriate format
7192
- if (typeof chunk === 'string') {
7193
- controller.enqueue(new TextEncoder().encode(chunk));
7194
- }
7195
- else if (chunk instanceof Uint8Array) {
7196
- controller.enqueue(chunk);
7197
- }
7198
- else if (typeof chunk === 'object') {
7199
- controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk)));
7208
+ const consume = async () => {
7209
+ let chunkCount = 0;
7210
+ try {
7211
+ for await (const chunk of generator) {
7212
+ chunkCount++;
7213
+ if (chunkCount % 5 === 0) {
7214
+ debugLog$1(`[runner] [build-engine] Processed ${chunkCount} chunks from generator\n`);
7215
+ }
7216
+ // Convert chunk to appropriate format
7217
+ if (typeof chunk === 'string') {
7218
+ controller.enqueue(new TextEncoder().encode(chunk));
7219
+ }
7220
+ else if (chunk instanceof Uint8Array) {
7221
+ controller.enqueue(chunk);
7222
+ }
7223
+ else if (typeof chunk === 'object') {
7224
+ controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk)));
7225
+ }
7200
7226
  }
7227
+ debugLog$1(`[runner] [build-engine] ✅ Generator exhausted after ${chunkCount} chunks, closing stream\n`);
7228
+ controller.close();
7201
7229
  }
7202
- debugLog$1(`[runner] [build-engine] ✅ Generator exhausted after ${chunkCount} chunks, closing stream\n`);
7203
- controller.close();
7204
- }
7205
- catch (error) {
7206
- debugLog$1();
7207
- controller.error(error);
7230
+ catch (error) {
7231
+ debugLog$1();
7232
+ controller.error(error);
7233
+ }
7234
+ finally {
7235
+ // Restore the original working directory
7236
+ process.chdir(originalCwd);
7237
+ }
7238
+ };
7239
+ // Restore the parent span context so child spans nest correctly
7240
+ if (parentSpan) {
7241
+ await Sentry.withActiveSpan(parentSpan, consume);
7208
7242
  }
7209
- finally {
7210
- // Restore the original working directory
7211
- process.chdir(originalCwd);
7243
+ else {
7244
+ await consume();
7212
7245
  }
7213
7246
  },
7214
7247
  });