Skip to content

fix(Tensor): scalar in-place arithmetic mutates storage in place#980

Merged
pcchen merged 2 commits into
masterfrom
fix/scalar-inplace-ops-preserve-storage
Jul 8, 2026
Merged

fix(Tensor): scalar in-place arithmetic mutates storage in place#980
pcchen merged 2 commits into
masterfrom
fix/scalar-inplace-ops-preserve-storage

Conversation

@yingjerkao

Copy link
Copy Markdown
Collaborator

Problem

Part of #906. t += 1.0 (and -=, *=, /= with any scalar or cytnx::Scalar) replaced the tensor's Storage with a freshly computed one:

this->_impl->storage() = cytnx::linalg::Add(*this, rc)._impl->storage();

Any view sharing that storage (from permute(), contiguous reshape(), from_storage()) silently detached, while t += tensor mutated in place — the two paths disagreed on aliasing semantics.

Fix

Scalar RHS now routes through the same iAdd/iSub/iMul/iDiv kernels via a shape-{1} wrapper tensor (the kernels already broadcast a length-1 RHS and mutate LHS storage in place). All 48 scalar specializations rewritten; the Tensor/Tproxy/Sproxy specializations are untouched.

Deliberate behavior changes

  • Views sharing storage stay attached (the bug fix).
  • LHS dtype is preserved: Float tensor += 1.0 stays Float (previously promoted to Double); integer LHS truncates fractional scalars — both matching the existing tensor-RHS in-place behavior and numpy's scalar rules.
  • real_tensor ⊙= complex_scalar now throws instead of silently promoting the tensor to complex.

The GPU iAdd path can still rebind on promotion; that remains tracked under #906.

Testing

Seven new gtests: storage-sharing preserved for all four ops (including through a real non-contiguous permute() view), dtype preservation, integer truncation pinned, real ⊙= complex throws for all four ops, and cytnx::Scalar RHS. Red-state verified against the old code (a shared view read 0 after += — update lost). Full suite passes at baseline.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request optimizes scalar in-place arithmetic operations on Tensors by wrapping scalars into rank-1 Tensors to reuse existing in-place kernels, resolving storage sharing issues. It also corrects operator names in mismatch error messages and adds comprehensive unit tests. The review feedback highlights a potential performance bottleneck in the helper function _scalar_as_rank1_tensor, where creating a temporary CPU tensor and transferring it to a GPU device for every scalar operation can introduce significant overhead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/Tensor.cpp
Comment on lines +553 to +559
template <class T>
Tensor _scalar_as_rank1_tensor(const T &rc, const int device) {
Tensor s({1}, Type.cy_typeid(rc), Device.cpu);
s.storage().at<T>(0) = rc;
if (device != Device.cpu) s = s.to(device);
return s;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Creating a temporary Tensor on CPU and then calling .to(device) on it introduces a significant performance bottleneck, especially when device is a GPU. This involves allocating GPU memory (cudaMalloc) and performing a host-to-device copy for every single in-place scalar operation (e.g., a += 1.0;), which is extremely slow and defeats the purpose of in-place operations.

Consider optimizing this by checking if device is GPU and reusing a pre-allocated or cached scalar tensor, or by bypassing the tensor wrapper entirely for CPU/GPU scalar operations where possible.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 11.47541% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 31.84%. Comparing base (82a9904) to head (dfe688b).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/Tensor.cpp 6.89% 32 Missing and 22 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #980      +/-   ##
==========================================
- Coverage   31.85%   31.84%   -0.01%     
==========================================
  Files         230      230              
  Lines       33107    33117      +10     
  Branches    13840    13846       +6     
==========================================
  Hits        10547    10547              
- Misses      15532    15549      +17     
+ Partials     7028     7021       -7     
Flag Coverage Δ
cpp 31.48% <11.47%> (-0.01%) ⬇️
python 61.84% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 32.57% <11.47%> (-0.02%) ⬇️
Python bindings 24.03% <ø> (ø)
Python package 61.84% <ø> (ø)
Files with missing lines Coverage Δ
src/linalg/iArithmetic_visit.hpp 83.33% <100.00%> (+9.52%) ⬆️
src/Tensor.cpp 21.98% <6.89%> (+2.09%) ⬆️

... and 7 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 82a9904...dfe688b. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/Tensor.cpp
// arithmetic reuses the iAdd/iSub/iMul/iDiv kernels (which broadcast a
// length-1 RHS and mutate LHS storage in place). See #906.
template <class T>
Tensor _scalar_as_rank1_tensor(const T &rc, const int device) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be moved to linalg to benefit the linalg functions. If the naming style in the current file is messy, the name of the new function should follow the coding style and avoid prefixing with _.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps a good opportunity make scalar as rank 0 tensors? It would make operations such as trace much more consistent.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That will make Scalar heavier than a simple std::variant and we may have to triage scalar in all operations consuming tensors

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No, I meant rank 0 tensor should exist and be "scalar + shared storage + device"; see comment #1016 (comment)

  • Scalar is similar to to Python builtin number
  • Rank-0 tensor is similar to numpy rank 0 ndarray

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Proposal: defer this to the outcome of #1016 rather than changing it here. Where the scalar-wrapper helper should live (and what it's named) is entangled with the "rank-0 tensor vs. shape-{1} convention" design question now tracked there — #1016's option B explicitly includes exactly the relocation you suggested (a shared linalg-level scalar_tensor() helper as the single implementation point, no _ prefix), while option A would make this helper obsolete altogether. Two stacked PRs also touch this same helper (#992 makes it CPU-resident to fix a GPU UB; #1015's typed-storage direction may reshape it again), so moving it now would just multiply churn.

If that's acceptable, could you re-review/dismiss the requested-changes gate so #980 + #992 (the CUDA use-after-free / UB fix stacked on this) can land? The rename+relocate then happens once, in its final form, per the #1016 decision.

Posted by Claude Code on behalf of @pcchen

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good concrete argument — captured in #1016 (the design-question issue for rank-0 vs. the shape-{1} convention), where it weighs for option B/C (keep Scalar a lightweight variant, as #1011 builds) against option A (true rank-0 tensors). Let's keep the design debate there so it accumulates in one place; no change on this PR.

Posted by Claude Code on behalf of @pcchen

@pcchen

pcchen commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Fixes the real #906 bug: t += 1.0 did storage() = Add(*this, rc).storage() — computing out-of-place at a promoted dtype and swapping the Storage handle, silently detaching every view sharing that storage — while t += tensor mutated in place. All 48 scalar specializations (+4 Scalar overloads) now route through the in-place iAdd/iSub/iMul/iDiv kernels via a shape-{1} wrapper. LGTM once rebased.

Correctness — verified

  • The iOp + shape-{1} route is sound: iDiv et al. explicitly special-case a length-1 RHS (iDiv.cpp:17 skips the shape check for {1}) and dispatch in-place on LHS storage — contiguous and non-contiguous paths — with an aliasing guard (irrelevant here; the wrapper is always fresh). Same established path DenseUniTensor::normalize_ uses. ✅
  • _scalar_as_rank1_tensor is correct for CPU + GPU (build on CPU, set element, .to(device)); Tensor::from_storage used by the tests exists (Tensor.hpp:558). ✅
  • Perf is a side win: the old path allocated a full N-element result per scalar op; the new one allocates one element.
  • Tests are strong: storage-sharing pinned via from_storage (with a sharp comment on why copy-ctor aliasing couldn't observe the bug) and through a real non-contiguous permute() view; dtype preservation; integer truncation; real⊙=complex throws ×4; Scalar RHS; corrected error text. Red-state verified; CI on the pre-conflict commit: 15 pass, only advisory codecov red. ✅

Findings

  1. 🟠 CONFLICTING — trivial rebase. Checked the merge-base: src/Tensor.cpp and iArithmetic_visit.hpp have no master-side changes; the only collision is the trailing test-append in tests/Tensor_test.cpp (vs. fix(Tensor): harden reshape validation (multiple -1, 0-dim with -1, strong exception guarantee) #976's reshape tests). I'll resolve it (keep both), as with fix(Tensor): harden reshape validation (multiple -1, 0-dim with -1, strong exception guarantee) #976/fix: drop C complex.h from pybind TUs; rename macro-colliding template param #981.
  2. 🟡 Changelog entry needed for the deliberate behavior changes: dtype no longer promotes (Float += 1.0 stays Float; Int64 += 2.7 truncates) and real ⊙= complex throws instead of promoting. numpy-consistent and tensor-RHS-consistent, but downstream code relying on silent promotion will change results.
  3. 🟢 Positive cross-PR interactions: making operator/=<double>/<Scalar> dtype-preserving fixes the /=-half of the Float→Double Lanczos regression flagged on feat(api)!: norm()->Scalar, underscore convention for mutators, unified permute/reshape syntax #1000 (the out-of-place clone()/norm() sites there still promote), and gives Block/BlockFermionic normalize_ (block /= Scalar) the dtype-preservation guarantee that previously only Dense had.

On the three inline comments

Posted by Claude Code on behalf of @pcchen

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the issue-comment-reply. You can try again by commenting /gemini issue-comment-reply.

yingjerkao and others added 2 commits July 8, 2026 10:11
t += 1.0 previously replaced t's Storage with a freshly computed one,
silently detaching every view sharing that storage, while t += tensor
mutated in place. Scalar RHS now routes through the same
iAdd/iSub/iMul/iDiv kernels via a shape-{1} tensor.

Behavior changes: dtype is preserved (Float += 1.0 stays Float, as the
tensor-RHS path already behaved), integer LHS truncates fractional
scalars instead of promoting, and real ⊙= complex now throws instead
of silently promoting the tensor.

GPU note: the iAdd GPU path still rebinds on promotion; that remains
tracked under #906.

Co-Authored-By: Claude <noreply@anthropic.com>
…rors

ApplyInplaceArithmeticOp reported 'Cannot perform real+=complex' for
all four ops; *= , -= and /= now name themselves. These messages became
user-visible for scalar RHS via the #906 fix (real tensor op= complex
scalar now throws instead of silently promoting).

Known GPU limitation (from the same review, needs a CUDA machine, not
fixed here): iMul/iDiv GPU paths throw for non-contiguous LHS while
iAdd/iSub handle mappers, so gpu_tensor.permute(...) *= scalar throws;
and the shape-{1} scalar wrapper does a per-call H2D copy. Tracked as a
follow-up under #906.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pcchen
pcchen force-pushed the fix/scalar-inplace-ops-preserve-storage branch from 8fdf81c to dfe688b Compare July 8, 2026 02:11
pcchen pushed a commit that referenced this pull request Jul 8, 2026
Follow-up to #906/#980. Fixes the two GPU gaps that could not be
addressed on a CUDA-less machine, plus two related memory-safety bugs
found while validating on a CUDA device.

- Regression: iMul/iDiv threw for a non-contiguous GPU LHS. Route a
  broadcast scalar through the kernel (the rconst path scales every
  physical element, so layout mappers are irrelevant). cuMul/cuDiv --
  unlike cuAdd/cuSub -- ignore the mappers, so a genuine non-contiguous
  tensor*=tensor / tensor/=tensor now throws loudly instead of silently
  pairing mismatched elements.

- Efficiency: the shape-{1} scalar wrapper now stays CPU-resident.
  For a length-1 RHS the GPU kernels read the scalar with a host-side
  dereference and pass it by value, so there is no per-call H2D copy
  (and the previous GPU-resident scalar made that host dereference UB).
  cudaSetDevice now targets Lt.device(), and the device check permits a
  host-resident length-1 scalar against a GPU LHS.

- Safety: real op= complex on GPU reinterpreted the real output buffer
  as complex (out-of-bounds write). Guarded device-independently so it
  throws like the CPU path.

- Safety: a LHS narrower than the promoted dtype (e.g. float_gpu *= 2.0)
  had the promoted-width result written into the narrower buffer (OOB;
  float += 1.0 yielded 0). Now computed in the promoted dtype and
  truncated back, matching CPU element-wise semantics.

Adds tests/gpu Tensor.Gpu* covering the non-contiguous *=//= regression,
host-scalar values, storage sharing, dtype preservation, real op= complex
throwing, and the non-contiguous tensor-tensor guard (no underscores per
#857). Verified on RTX 4070 Ti (CUDA 13): 7/7 new GPU tests, 8/8 CPU
scalar tests, and the existing GPU iSub/iMul/iDiv all-types suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@pcchen pcchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. The fix is verified: all 48 scalar specializations (+4 Scalar overloads) now route through the in-place iAdd/iSub/iMul/iDiv kernels via a shape-{1} wrapper — the kernels explicitly special-case a length-1 RHS and mutate LHS storage in place, so storage-sharing views stay attached (the #906 bug), and the old full-N-element allocation per scalar op is gone. Dtype preservation, integer truncation, and the real ⊙= complex rejection are all pinned by red-state-verified tests, including through a genuine non-contiguous permute() view. CI is functionally green on the rebased head (only advisory codecov red).

Two coordination notes, both agreed in the threads:

  1. Land #992 immediately after this — on CUDA, this PR's .to(device) wrapper is read host-side by the GPU kernels (UB); #992 (stacked on this branch, hardware-validated) fixes that plus the non-contiguous GPU scalar regression. The pair should merge back-to-back.
  2. The helper relocation/rename requested in review is deferred to #1016 (the rank-0-vs-{1}-convention design issue) rather than churned here — it's exactly option B's "single implementation point" helper there, and #992/#1015 touch the same code. The rename+move happens once, in its final form, per that decision.

Behavior changes (dtype no longer promotes; real ⊙= complex throws) are deliberate, numpy-consistent, and need a changelog entry alongside the other in-place-semantics rulings.

Posted by Claude Code on behalf of @pcchen

@pcchen
pcchen dismissed IvanaGyro’s stale review July 8, 2026 12:05

Dismissing per the deferral proposed in the review thread: the helper relocation/rename is folded into the #1016 design decision (it is option B's single-implementation-point helper; #992 and #1015 touch the same code), so it will be done once, in its final form, per that outcome rather than churned here. Unblocks the #980#992 pair (#992 fixes the CUDA host-deref UB stacked on this branch). — dismissed by Claude Code on behalf of @pcchen

@pcchen
pcchen merged commit f9deeb7 into master Jul 8, 2026
18 of 19 checks passed
@pcchen
pcchen deleted the fix/scalar-inplace-ops-preserve-storage branch July 8, 2026 12:06
pcchen added a commit that referenced this pull request Jul 8, 2026
Empty commit: the pull_request workflows last ran against the pre-merge
#980 base branch (since deleted); this fires a fresh full run (BuildAndTest,
DownstreamFindPackage, wheels) with base=master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ue-division (#941)

Reimplements the deferred half of #1015 on current master, which had since
refactored the same six arithmetic files for rank-zero support (#1026),
zero-extent (#1043), and type_promote (#984).

- Convert CPU out-of-place AND in-place Add/Sub/Mul/Div/Cpr/Mod to typed
  std::visit over as_storage_variant() with storage_cast<TO> and typed
  *InternalImpl<TO,TL,TR> kernels; the dead legacy dispatch tables are removed.
- True division (#941): `/` and `/=` yield a floating result (int/int -> Double)
  via make_floating_point_dtype; the rank-zero / broadcast output still routes
  through #1026's init_broadcast_binary_output (seeded with the floating dtype).
- Mixed-dtype in-place TENSOR ops promote the LHS via storage_as_type_or_replace
  (no more truncation into the narrower LHS storage -- #941's core bug).
- Weak-scalar (#980, per #1015's binding ruling): `tensor op= python-scalar`
  (a rank-0 RHS via scalar_as_rank0_tensor) PRESERVES the LHS dtype for
  Add/Sub/Mul and follows true-division for Div (Int64 /= 2.0 -> Double,
  Float /= 3.0 -> Float), keyed on Rt.rank()==0 in DispatchInplaceArithmeticCPU;
  complex-into-real still throws.
- floordiv conforms to #1049 (scalar `ut // 2.0` kept, `ut1 // ut2` raises);
  #1015's original unbind-all is dropped.
- normalize_() now promotes an integer block to Double under true division; its
  stale "stays Int32" comment is updated. Mod keeps an eager is_complex guard so
  a zero-extent complex Mod still throws (ZeroExtentLinalgTest).

CPU only; GPU stays on the legacy dtype tables behind the unchanged public API
(#1013 tracks the GPU conversion).

Testing: full openblas-cpu (non-ASan) test_main 1784 passed / 11 skipped /
0 failed -- MixedDtypeArithmetic regressions, weak-scalar Tensor.ScalarInplace*,
ZeroExtentLinalgTest, and DenseUniTensorTest true-division/normalize all green;
clang-format v14 clean.

Reimplements ff30d00, deferred from #1015 (#941).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 15, 2026
…ic (#941)

Address PR #1056 review (Codex P2): DispatchInplaceArithmeticCPU inferred
numpy weak-scalar semantics from `Rt.rank() == 0`, but a user-provided
rank-0 tensor (e.g. cytnx.zeros([])) is also rank 0. That made
`int_tensor += rank0_double_tensor` truncate into the narrower lhs, while
the out-of-place op and a shape-[1] tensor RHS promote -- an inconsistency,
since rank-0 tensors are first-class tensor values in Cytnx.

Thread an explicit `rhs_is_weak_scalar` flag from the scalar in-place
operators (which wrap the scalar via scalar_as_rank0_tensor) through
iAdd/iSub/iMul/iDiv into the dispatcher, instead of guessing from rank. A
python-scalar RHS preserves the lhs dtype (numpy weak scalar, #980); any
genuine tensor RHS -- rank-0 included -- promotes (#941). The public
iAdd/iSub/iMul/iDiv gain a defaulted `rhs_is_weak_scalar = false` param, so
existing callers are unaffected.

Adds a pytest pinning both the rank-0-tensor promotion (verified to fail on
the pre-fix rank-based detection) and the contrasting python-scalar
preservation. Also folds in the PR #1056 pass-by-value style fixes in this
file (ApplyInplaceArithmeticOp element params, kernel len/rhs_is_scalar,
uint64_t len local).

NOTE (numerical behavior): a new observable mixed-dtype rule -- an in-place
op against a rank-0 tensor operand now promotes on CPU. Flagging per the
type-promotion guardrail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
yingjerkao added a commit that referenced this pull request Jul 17, 2026
#1013)

Problem: the iAdd/iSub/iMul/iDiv in-place complex guard rejected EVERY
real-LHS / complex-RHS combination. That wrongly blocked a genuine complex
*tensor* RHS from promoting the real LHS to complex — behavior the
out-of-place operator and the #1013 typed dispatch already support.

Fix: narrow the guard to fire only for a complex python *weak scalar*
(numpy weak-scalar semantics, #980/#1015 preserve the LHS dtype, so a
complex scalar genuinely cannot be stored in a real tensor). A genuine
complex tensor RHS now promotes Lt's storage to complex via the existing
type_promote_t / storage_as_type_or_replace path. The guard stays
device-independent: the GPU kernel's complex-into-real branch silently
returns zero instead of throwing, so it relies on this host-side rejection.

Testing: new CPU tests (InplaceRealOpComplexTensorPromotes,
InplaceRealOpComplexWeakScalarRejected) with independent hand-computed
complex expected values, incl. a rank-0 genuine-tensor RHS; new GPU test
(gpu_inplace_real_op_complex_weak_scalar_rejected) plus real<-complex
promotion coverage in the InplacePromote sweep and literal cases. Both new
CPU tests confirmed to fail on the pre-fix guard. clang-format v14 clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

4 participants