Skip to content

feat: lazy stack trace capture with deferred symbolization#19

Merged
ankurs merged 5 commits intomainfrom
feat/lazy-stack-capture
Mar 31, 2026
Merged

feat: lazy stack trace capture with deferred symbolization#19
ankurs merged 5 commits intomainfrom
feat/lazy-stack-capture

Conversation

@ankurs
Copy link
Copy Markdown
Member

@ankurs ankurs commented Mar 30, 2026

Summary

Replaces eager per-frame stack capture with batch PC capture and deferred symbolization, reducing error creation cost by 6.5x on the hot path.

What changed

  • Batch PC capture: single runtime.Callers() call replaces a loop of runtime.Caller(i) + runtime.FuncForPC(pc) per frame
  • Lazy symbolization: StackFrame() resolves frames on first access via sync.Once, not at creation time
  • Default depth: reduced from 64 to 16 frames (configurable via SetMaxStackDepth)
  • Thread-safe config: SetMaxStackDepth uses atomic.Int32, safe for concurrent use
  • Consistent pointer receivers: all customError methods use pointer receivers (required by sync.Once)

Benchmark results (Apple M1 Pro)

Benchmark Before After Improvement
errors.New 3500 ns / 1600 B / 19 allocs 540 ns / 392 B / 6 allocs 6.5x faster, 75% less memory
New + StackFrame() 3500 ns / 1600 B / 19 allocs 1250 ns / 824 B / 9 allocs 2.8x faster, 49% less memory
Wrap (reuse stack) 120 ns / 240 B / 4 allocs 110 ns / 256 B / 4 allocs ~same
NewDeepStack (32 frames) 40500 ns / 17 KB / 125 allocs 1060 ns / 392 B / 6 allocs 38x faster, 98% less memory

Compatibility

  • Callers() []uintptr — unchanged, returns raw PCs as before
  • StackFrame() []StackFrame — same output, now lazy. Sentry/Rollbar/Airbrake notifiers all work unchanged
  • SetMaxStackDepth(n) — now thread-safe, default lowered to 16

Test plan

  • make test passes with -race (both errors and notifier packages)
  • make build compiles cleanly
  • Benchmarks added (bench_test.go)
  • Deploy to staging and verify stack traces in error notifications

Summary by CodeRabbit

  • New Features

    • Added documented SupportPackageIsVersion1 constant.
  • Bug Fixes

    • Stack-depth and base-path configuration made concurrency-safe; SetMaxStackDepth now ignores out-of-range inputs.
  • Changes

    • Default max stack depth reduced to 16.
    • Stack-frame resolution is now lazy and cached to improve performance and reduce allocations.
  • Documentation

    • README/API docs updated and index entry added.
  • Tests

    • New benchmarks and tests for stack depth, frame consistency, and related behavior.

Replace eager per-frame runtime.Caller + runtime.FuncForPC with batch
runtime.Callers for PC capture, deferring symbolization to first
StackFrame() access via sync.Once.

Performance (errors.New): 3500ns→540ns (6.5x), 1600B→392B (75% less)
Deep stacks (32 frames): 40500ns→1060ns (38x), 17KB→392B (98% less)

Changes:
- Default stack depth reduced from 64 to 16 (configurable via SetMaxStackDepth)
- captureStack: single runtime.Callers call instead of loop of runtime.Caller
- StackFrame: lazy resolution via sync.Once + runtime.CallersFrames
- All receivers changed to pointer (required by sync.Once, consistent)
- SetMaxStackDepth: atomic.Int32 for concurrent safety
- resolveFrames: pre-allocated slice, strings.TrimPrefix for base path
- splitFuncName: takes string instead of uintptr (no runtime.FuncForPC)
Copilot AI review requested due to automatic review settings March 30, 2026 07:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57e9f6fc-8498-4a2d-86e7-8333d6b27ab0

📥 Commits

Reviewing files that changed from the base of the PR and between 901a0f6 and 9870abb.

📒 Files selected for processing (1)
  • errors.go

📝 Walkthrough

Walkthrough

Switches to two-phase lazy stack symbolization: capture program counters and basePath at error creation, resolve frames on first StackFrame() call with sync.Once, make stack depth and base path atomic and concurrency-safe, add benchmarks and tests, and update README anchors and docs.

Changes

Cohort / File(s) Summary
Documentation
README.md, notifier/README.md
Added SupportPackageIsVersion1 doc entry and TOC insertion; updated anchor/line targets for multiple exported APIs; revised SetMaxStackDepth description to state default=16, bounds [1,256], and concurrency-safety.
Core stack trace logic
errors.go
Replaced eager frame generation with captureStack storing PCs and basePath; added lazy resolution via sync.Once caching []StackFrame; changed wrapper behavior to preserve callers and basePath snapshot; replaced globals with atomic-backed atomicStackDepth/atomicBasePath and defaultStackDepth=16; switched some customError methods to pointer receivers; removed old symbolization helpers and introduced updated func-name/file trimming logic.
Benchmarks
bench_test.go
Added four benchmarks: BenchmarkNew, BenchmarkNewAndStackFrame, BenchmarkWrap, BenchmarkNewDeepStack (each uses b.ReportAllocs()).
Tests
errors_test.go
Updated depth tests to assert against defaultStackDepth and Callers(); added TestSetMaxStackDepth and TestStackFrameConsistency to validate atomic depth behavior, bounds handling, and cached lazy resolution consistency.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as User
    participant Creator as Error Creator
    participant Capture as Stack Capture
    participant Resolver as Frame Resolver
    participant Runtime as runtime

    Caller->>Creator: call New / Wrap
    Creator->>Capture: captureStack(skip) -> runtime.Callers(...) -> store PCs + basePath
    Capture-->>Creator: return error (frames unresolved)

    Caller->>Creator: call StackFrame() [first time]
    Creator->>Resolver: resolveFrames() via sync.Once
    Resolver->>Runtime: CallersFrames(pcs)
    Runtime-->>Resolver: frames (Function, File, Line)
    Resolver-->>Creator: cache []StackFrame
    Creator-->>Caller: return resolved frames

    Caller->>Creator: call StackFrame() [subsequent]
    Creator-->>Caller: return cached frames
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I stash PCs like crunchy seeds,

Frames wake only when someone needs,
Atomics count my gentle hops,
Benchmarks drum and testing hops,
A lazy leap — no tangled threads.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing eager stack generation with lazy capture and deferred symbolization, which is the core architectural improvement across errors.go and supporting files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lazy-stack-capture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the core errors package to reduce error creation overhead by capturing stack PCs in one batch and deferring symbolization until stack frames are actually requested, with accompanying documentation and benchmarks.

Changes:

  • Replaced eager per-frame stack capture with runtime.Callers PC capture and lazy StackFrame() resolution via sync.Once.
  • Made max stack depth configuration thread-safe with atomic.Int32 and reduced default captured depth to 16.
  • Regenerated package docs/READMEs and added benchmarks covering New, Wrap, and deep-stack creation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
errors.go Implements lazy stack frame symbolization, batch PC capture, and atomic stack depth config.
bench_test.go Adds benchmarks to measure the new stack capture/symbolization behavior.
README.md Regenerated docs reflecting updated defaults and exported constants.
notifier/README.md Regenerated notifier docs/links to reflect updated line references.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errors.go
Comment thread errors.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

errors.go:244

  • When wrapping an existing ErrorExt, the code now reuses only Callers() and no longer reuses any already-resolved frames. If the wrapped error’s frames were previously materialized, calling StackFrame() on the new wrapper will re-symbolize the same PCs again. Consider carrying forward already-resolved frames (without forcing symbolization) to avoid repeated work in wrap chains where stack frames are accessed multiple times.
	//if we have stack information reuse that
	if e, ok := err.(ErrorExt); ok {
		c := &customError{
			Msg:          msg + e.Error(),
			cause:        e.Cause(),
			wrapped:      err, // preserve full chain for errors.Is/errors.As
			status:       status,
			shouldNotify: true,
		}

		c.stack = e.Callers()
		if n, ok := e.(NotifyExt); ok {
			c.shouldNotify = n.ShouldNotify()
		}
		return c

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errors.go
Comment thread errors.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
errors.go (1)

231-244: ⚠️ Potential issue | 🟠 Major

Preserve third-party ErrorExt frames when re-wrapping.

This is only lossless for *customError. A public ErrorExt can legitimately precompute or sanitize StackFrame(), and this wrapper now discards that data by keeping only Callers().

💡 Preserve existing frames for non-*customError implementations
 func (c *customError) StackFrame() []StackFrame {
 	c.frameOnce.Do(func() {
-		if len(c.stack) > 0 {
+		if len(c.frame) == 0 && len(c.stack) > 0 {
 			c.frame = resolveFrames(c.stack)
 		}
 	})
 	return c.frame
 }
@@
 	if e, ok := err.(ErrorExt); ok {
 		c := &customError{
 			Msg:          msg + e.Error(),
 			cause:        e.Cause(),
 			wrapped:      err, // preserve full chain for errors.Is/errors.As
 			status:       status,
 			shouldNotify: true,
 		}
 
-		c.stack = e.Callers()
+		c.stack = append([]uintptr(nil), e.Callers()...)
+		if _, ok := err.(*customError); !ok {
+			c.frame = append([]StackFrame(nil), e.StackFrame()...)
+		}
 		if n, ok := e.(NotifyExt); ok {
 			c.shouldNotify = n.ShouldNotify()
 		}
 		return c
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@errors.go` around lines 231 - 244, When wrapping an existing ErrorExt in
customError, preserve any precomputed/sanitized frames from the original
ErrorExt instead of unconditionally replacing them with Callers(); change the
logic in the ErrorExt branch to: if the concrete type is *customError keep
current behavior, otherwise if the ErrorExt implements a public frame accessor
(e.g. an interface like StackFrame() []StackFrame or similar used by your
project) copy that into c.stack; fall back to e.Callers() only if no such
accessor exists, and still preserve cause, wrapped, status and shouldNotify as
before (references: ErrorExt, customError, Callers(), shouldNotify).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@errors.go`:
- Around line 93-99: The stack trimming currently happens lazily in
customError.StackFrame() via resolveFrames(c.stack), which reads the global
basePath and causes instability/races when SetBaseFilePath changes; fix this by
capturing and freezing the basePath at error creation time (e.g., add a
basePathSnapshot or pre-trim frames when the stack is captured) and make
StackFrame() use that frozen value instead of reading global state; update the
logic around resolveFrames, customError.stack initialization, and any places
that create customError so the basePath is stored/used at capture time
(reference: customError.StackFrame, resolveFrames, and SetBaseFilePath).
- Around line 261-264: SetMaxStackDepth currently ignores out-of-range n and
leaves the previous value; change it to normalize/clamp the input into the valid
range [1,256] before storing so callers always see a deterministic result.
Inside SetMaxStackDepth, compute a clamped value (e.g., if n < 1 set to 1, if n
> 256 set to 256), then call atomicStackDepth.Store(int32(clamped)) and remove
the conditional that silently drops invalid inputs. Ensure you still use int32
conversion when storing.

---

Outside diff comments:
In `@errors.go`:
- Around line 231-244: When wrapping an existing ErrorExt in customError,
preserve any precomputed/sanitized frames from the original ErrorExt instead of
unconditionally replacing them with Callers(); change the logic in the ErrorExt
branch to: if the concrete type is *customError keep current behavior, otherwise
if the ErrorExt implements a public frame accessor (e.g. an interface like
StackFrame() []StackFrame or similar used by your project) copy that into
c.stack; fall back to e.Callers() only if no such accessor exists, and still
preserve cause, wrapped, status and shouldNotify as before (references:
ErrorExt, customError, Callers(), shouldNotify).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95f27bb7-7645-4329-a6d2-1089f6945e67

📥 Commits

Reviewing files that changed from the base of the PR and between 59721f1 and ee14468.

📒 Files selected for processing (1)
  • errors.go

Comment thread errors.go
Comment thread errors.go
- Snapshot basePath in captureStack so lazy resolution uses the value
  from error creation time, not access time (eliminates race)
- resolveFrames takes basePath parameter instead of reading global
- Document SetMaxStackDepth accepts [1, 256]
- Add TestSetMaxStackDepth (valid, zero, negative, over-256)
- Add TestStackFrameConsistency (repeated calls, Callers parity)
- Update existing depth tests for new default of 16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errors.go
Comment thread errors_test.go
Comment thread errors_test.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
errors.go (1)

233-246: ⚠️ Potential issue | 🟠 Major

Preserve the inner stack-frame context when re-wrapping ErrorExt.

This branch only copies e.Callers(). If the inner error was captured with a non-empty basePath, the wrapper resolves the same PCs with c.basePath == "", so StackFrame() flips from trimmed paths back to absolute paths after a wrap. For non-*customError ErrorExt implementations, you can also lose already-materialized frames entirely. That shows up downstream in notifier/notifier.go:379, which serializes StackFrame() directly.

💡 One way to keep wrapped output stable
 	if e, ok := err.(ErrorExt); ok {
 		c := &customError{
 			Msg:          msg + e.Error(),
 			cause:        e.Cause(),
 			wrapped:      err, // preserve full chain for errors.Is/errors.As
 			status:       status,
 			shouldNotify: true,
 		}

-		c.stack = e.Callers()
+		c.stack = append([]uintptr(nil), e.Callers()...)
+		if inner, ok := e.(*customError); ok {
+			c.basePath = inner.basePath
+		} else {
+			c.frame = append([]StackFrame(nil), e.StackFrame()...)
+			c.frameOnce.Do(func() {})
+		}
 		if n, ok := e.(NotifyExt); ok {
 			c.shouldNotify = n.ShouldNotify()
 		}
 		return c
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@errors.go` around lines 233 - 246, When re-wrapping an ErrorExt in the branch
that builds a new customError, preserve the inner error's stack/frame context
and any basePath used to trim frames: if the wrapped error (e) exposes basePath
or already-materialized frames (e.g. is a *customError or implements accessors
for basePath/frames), copy those into the new customError fields (e.g.
c.basePath = e.basePath or via an accessor, and copy any cached/materialized
stack frames rather than only calling e.Callers()), so StackFrame() continues to
return the same trimmed/flattened frames after wrapping; keep wrapped: err as-is
to preserve errors.Is/errors.As behavior and still update shouldNotify from
NotifyExt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@errors.go`:
- Around line 135-154: resolveFrames may return more frames than intended
because runtime.CallersFrames can expand PCs into multiple inlined frames;
enforce the original depth cap by stopping after processing len(pcs) logical
frames. Modify resolveFrames (which currently iterates until frames.Next()
returns false) to count appended frames and break once the count reaches
len(pcs) (the number of PCs captured by captureStack), ensuring StackFrame slice
length never exceeds the original depth limit while still trimming file paths
and extracting function names via splitFuncName.

---

Outside diff comments:
In `@errors.go`:
- Around line 233-246: When re-wrapping an ErrorExt in the branch that builds a
new customError, preserve the inner error's stack/frame context and any basePath
used to trim frames: if the wrapped error (e) exposes basePath or
already-materialized frames (e.g. is a *customError or implements accessors for
basePath/frames), copy those into the new customError fields (e.g. c.basePath =
e.basePath or via an accessor, and copy any cached/materialized stack frames
rather than only calling e.Callers()), so StackFrame() continues to return the
same trimmed/flattened frames after wrapping; keep wrapped: err as-is to
preserve errors.Is/errors.As behavior and still update shouldNotify from
NotifyExt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 55e2f075-a48c-4904-9b0c-ccea2094be68

📥 Commits

Reviewing files that changed from the base of the PR and between ee14468 and bd7b39a.

📒 Files selected for processing (3)
  • README.md
  • errors.go
  • errors_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment thread errors.go
…t assertions

- Copy basePath from wrapped ErrorExt so path trimming works on re-wrapped errors
- Cap resolveFrames output at len(pcs) to handle inlining frame expansion
- Test assertions use Callers() length (PCs) instead of StackFrame() length
- Relax 1:1 PC-to-frame assumption in TestStackFrameConsistency
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
errors.go (1)

243-246: Freeze the wrapped PCs instead of aliasing them.

Line 243 stores the slice returned by e.Callers() directly. Because ErrorExt is exported, another implementation can legally reuse or mutate that backing array, which would change this wrapper's lazy StackFrame() result after construction. Copy once here so the wrapper owns an immutable snapshot.

📌 Small defensive copy
-		c.stack = e.Callers()
+		callers := e.Callers()
+		c.stack = append([]uintptr(nil), callers...)
 		if ce, ok := e.(*customError); ok {
 			c.basePath = ce.basePath
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@errors.go` around lines 243 - 246, The wrapper stores the slice returned by
e.Callers() directly (assigned to c.stack), which lets external implementations
mutate the backing array later; instead, make a defensive copy of the PCs when
constructing the wrapper so the wrapper owns an immutable snapshot used by
StackFrame(); update the assignment in the constructor/initializer that sets
c.stack to copy the slice (e.g. allocate a new slice and copy elements from
e.Callers()) while leaving the customError, basePath, and rest of the logic
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@errors.go`:
- Around line 22-23: The package-global basePath is written unsafely by
SetBaseFilePath and still read without synchronization when snapshotting (e.g.,
where StackFrame or the snapshot logic reads basePath around line ~131); update
the snapshotting/read sites to access basePath via a synchronized mechanism (use
the existing atomicStackDepth pattern or an atomic.Value / mutex) so
SetBaseFilePath stores the new string atomically and the snapshot/read path
loads it atomically before building the snapshot; reference and protect the
basePath symbol and ensure SetBaseFilePath and the snapshotting code use the
same synchronization primitive.
- Around line 83-89: Callers() exposes raw PCs and some consumers (Rollbar and
Sentry code paths that call runtime.CallersFrames() and the test
notifier_rollbar_airbrake_test) iterate frames without capping, risking
unbounded expansion; update those consumers to stop iterating after they've
yielded as many frames as were in the original pcs slice by adding an explicit
count check (e.g., track yieldedFrames and break when yieldedFrames >= len(pcs))
or reuse the existing resolveFrames() approach which already enforces maxFrames;
specifically modify the loops that call runtime.CallersFrames() in the notifier
Rollbar and Sentry handlers and in the notifier_rollbar_airbrake_test to check
len(frames) >= len(pcs) (or equivalent yieldedFrames >= len(pcs)) and break to
prevent frame growth.

---

Nitpick comments:
In `@errors.go`:
- Around line 243-246: The wrapper stores the slice returned by e.Callers()
directly (assigned to c.stack), which lets external implementations mutate the
backing array later; instead, make a defensive copy of the PCs when constructing
the wrapper so the wrapper owns an immutable snapshot used by StackFrame();
update the assignment in the constructor/initializer that sets c.stack to copy
the slice (e.g. allocate a new slice and copy elements from e.Callers()) while
leaving the customError, basePath, and rest of the logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6257793b-91b6-4c92-8abc-989aae82d7e6

📥 Commits

Reviewing files that changed from the base of the PR and between bd7b39a and 901a0f6.

📒 Files selected for processing (2)
  • errors.go
  • errors_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • errors_test.go

Comment thread errors.go Outdated
Comment thread errors.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errors.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread errors.go
@ankurs ankurs merged commit bf7870a into main Mar 31, 2026
11 checks passed
@ankurs ankurs deleted the feat/lazy-stack-capture branch March 31, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants