Commit Graph

62482 Commits

Author SHA1 Message Date
Alix-007 d577cb2fe9 fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM (#96031)
* fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM

Nextcloud Talk talks to self-hosted servers whose HTTP responses are not
trusted to be small. The send and reaction paths buffered three external
bodies without any byte cap:

- success JSON via await response.json()
- send error text via await response.text()
- reaction error text via await response.text()

A hostile or misbehaving Nextcloud endpoint could stream an unbounded body
(no content-length) into memory, pressuring or hanging the plugin/provider
path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error
bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow.
The 'message sent but receipt JSON unreadable -> unknown' fallback is
preserved (an over-limit body now also routes through the existing catch).

This is the symmetric counterpart to the #95103/#95108 response-limit
campaign, reusing the shared @openclaw/media-core helpers (newly re-exported
from plugin-sdk/response-limit-runtime for plugin consumers).

* fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface)

Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime
pushed the public plugin-SDK export count past its surface budget, failing
plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the
Nextcloud Talk send/reaction error bodies through the already-public
readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the
bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is
unchanged for callers; no new plugin-SDK surface is introduced.

Success JSON still reads through readResponseWithLimit (16 MiB cap). The
committed bounded-response-reads Vitest suite continues to prove the caps
hold against 17 MiB streamed bodies with no content-length.

* fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON

The send success receipt parsed JSON by hand via readResponseWithLimit + a
local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)),
duplicating the shared provider-http helper that the sibling room-info.ts and
bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing
shared helpers locally.

Swap the hand-rolled block for the one-stop
readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which
reads through the same bounded reader and throws on overflow/malformed JSON, so
the outer try/catch still keeps the "unknown" receipt and behavior is
equivalent. The error path keeps readResponseTextLimited (text, not JSON).
2026-06-28 10:30:17 -04:00
Alix-007 f1e4980a97 fix(embedding): bound OpenAI-compatible embedding response reads (#96868)
* fix(embedding): bound OpenAI-compatible embedding response reads

* test(embedding): avoid loop-condition lint in stream proof
2026-06-28 10:28:03 -04:00
Alix-007 2d2a50c00d fix(discord): bound REST response body to prevent OOM flood (#95412)
The Discord REST main response path read the body with an unbounded
await response.text() before JSON-parsing it. A controlled or hijacked
endpoint could stream an arbitrarily large body and exhaust memory (OOM).

Wrap the read in the canonical readResponseWithLimit helper with an 8 MiB
cap (well above any legitimate Discord JSON payload) plus an idle timeout
tied to the request timeout, so the stream is cancelled at the cap or on
stall instead of buffering unbounded. Normal payloads still parse fully.

This mirrors PR #95108 which bounded the analogous Anthropic Messages
error-response read with the same helper.
2026-06-28 10:20:37 -04:00
xingzhou 4d292caaa4 fix(plugin-sdk): follow paginated live model catalogs (#97012)
* fix(plugin-sdk): follow paginated live model catalogs

* fix(plugin-sdk): bound paginated live catalog discovery

* fix(plugin-sdk): reject incomplete live catalog pagination

* fix(plugin-sdk): resolve live catalog next links after redirects

* fix(plugin-sdk): preserve redirect auth boundary in catalog pagination
2026-06-28 10:08:36 -04:00
Dallin Romney 08d15ec32d test: stabilize startup session migration flake (#97370)
* test: stabilize startup session migration flake

* test: cover voice call doctor session ids
2026-06-28 04:29:41 -07:00
Dallin Romney 39c4c02653 test: link Tool Search gateway QA coverage (#97374) 2026-06-28 04:28:32 -07:00
snowzlmbot 8d168c836a fix(agents): preserve structured tool result visible text (#97268)
* fix(agents): preserve structured tool result visible text

* fix(agents): address structured tool result review blockers

* fix(agents): cover real structured tool result shapes

* fix(agents): preserve canonical tool error statuses

* fix(agents): harden structured result rendering

* fix(agents): preserve typed structured results

* fix(agents): bound provider result media

---------

Co-authored-by: snowzlmbot <293528334+snowzlmbot@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
2026-06-28 02:38:43 -07:00
xydt-tanshanshan d17b970bb5 [AI] fix(plugins): recognize document-extractors as a capability kind… (#91597)
* [AI] fix(plugins): recognize document-extractors as a capability kind in inspect-shape

PluginCapabilityKind did not include "document-extractors", causing
plugins that declare contracts.documentExtractors (like document-extract)
to show capabilityCount=0 and shape="non-capability" in plugins inspect.

Add "document-extractors" to PluginCapabilityKind and read from
plugin.contracts.documentExtractors in buildPluginCapabilityEntries().

Related to #91539

* [AI] test(plugins): add document-extractors shape contract coverage

Add a test case verifying that plugins declaring
contracts.documentExtractors are classified as
plain-capability shape with capabilityCount=1
and capabilities including the document-extractors kind.

Addresses ClawSweeper P2 review finding on PR #91597.

* [AI] chore: rebase on main to refresh CI

* test(plugins): fold extractor into shape matrix

---------

Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
2026-06-28 02:06:10 -07:00
NIO 38ddcef78f Fix/zalo bound api json response reads (#97277)
* fix(zalo): bound Bot API JSON response reads via readProviderJsonResponse

* test(zalo): keep API proof in focused coverage

---------

Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
2026-06-28 02:04:22 -07:00
NIO 7ba9212665 Fix/discord bound probe getme json reads (#97278)
* fix(discord): bound probe getMe JSON response reads

* test(discord): add oversized probe getMe JSON regression

* test(discord): add loopback proof for bounded probe getMe reads

* fix(scripts): satisfy oxlint in discord probe proof script

* test(discord): keep probe proof in focused coverage

---------

Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
2026-06-28 01:57:20 -07:00
Peter Lee 6c7a6ff1c4 fix(cron): propagate cleanupCliLiveSessionOnRunEnd to isolated cron CLI branch (#97227)
* fix(cron): propagate cleanupCliLiveSessionOnRunEnd to isolated cron CLI branch

* test(cron): add CLI interim retry coverage for isolated cron cleanup flag

Verify cleanupCliLiveSessionOnRunEnd is passed on both the initial and
retry CLI runs during isolated cron interim-ack retry loops. Proves the
inner boundary is safe: each runCliAgent call creates a fresh context,
so cleanup cannot affect the retry's live session.

* fix(cron): remove unused variable in interim retry test

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

* test(cron): trim redundant cleanup retry coverage

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
2026-06-28 01:55:56 -07:00
Bek 9c95abd49d fix: seed Slack thread context after reset (#97100) 2026-06-28 02:36:53 -04:00
Dallin Romney 119dc4bd82 test: promote OpenAI HTTP QA coverage (#97369) 2026-06-27 22:28:31 -07:00
Dallin Romney 69af58ba26 ci: log macOS Swift build phases (#97151) 2026-06-27 22:06:56 -07:00
Galin Iliev c6ade83a5c fix: avoid stale dashboard child context budgets (#97332)
* fix(gateway): avoid stale child session context tokens

* fix(gateway): avoid stale child session context tokens

---------

Co-authored-by: Galin Iliev <Galin.Iliev@microsoft.com>
2026-06-27 21:31:18 -07:00
Josh Avant 07b934901a fix: scanned PDF pages reach chat vision models (#97354)
* fix: forward scanned PDF page images to chat models

* fix: remove stale reply option cast
2026-06-27 23:01:08 -05:00
Gio Della-Libera 1b0766080a Add hosted catalog source profile validation (#95969)
Merged via squash.

Prepared head SHA: 6dc6ca154c
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-27 20:56:54 -07:00
Andy Ye 830467bc93 fix(imessage): stage remote media for plugin claims (#91803) 2026-06-27 19:36:21 -07:00
Gio Della-Libera d1b917120a Persist hosted catalog snapshots in state (#95964)
Merged via squash.

Prepared head SHA: 372ec63c99
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-27 19:01:22 -07:00
Dallin Romney 78f7de01c1 test: link local e2e qa coverage wrappers (#97150) 2026-06-27 18:34:36 -07:00
Gio Della-Libera 3630d502eb Doctor: expose gateway runtime findings (#97075)
* feat(doctor): expose gateway runtime findings

* fix(doctor): redact gateway health targets
2026-06-27 17:17:49 -07:00
Dallin Romney 7bbd09047b docs: simplify macOS app overview (#97120)
* docs: simplify macOS app docs

* docs: preserve macOS app detail links

* docs: address macOS review feedback
2026-06-27 16:04:30 -07:00
Shakker c53dbcaf4d fix: surface delegated Testbox proof status 2026-06-27 23:53:53 +01:00
Shakker 461e551e85 test: clarify delegated Testbox proof status 2026-06-27 23:53:52 +01:00
Galin Iliev dc575d148a fix: page sessions_history beyond truncated tails (#97101)
* Add sessions history offset pagination

* Fix sessions_history pagination after tool caps

* Fix sessions_history PR CI blockers

* Update sessions_history prompt snapshots

* Fix offset history projection windows

---------

Co-authored-by: Galin Iliev <5711535+galiniliev@users.noreply.github.com>
2026-06-27 15:30:49 -07:00
Gio Della-Libera 12685ee6b7 Add hosted catalog snapshot fallback (#95877)
Merged via squash.

Prepared head SHA: 5f71635bba
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-27 15:26:49 -07:00
Gio Della-Libera 36722014ef Doctor: expose configured plugin install findings (#96171)
* feat(doctor): expose configured plugin install findings

* fix(doctor): satisfy configured plugin mapper lint
2026-06-27 14:18:50 -07:00
zengLingbiao 1b8b8500ce fix(auto-reply): truncate user-facing text on UTF-16 boundary (#97299)
Summary:
- The PR imports `truncateUtf16Safe` and uses it for the Codex usage-limit preview and verbose working-label truncation paths in auto-reply.
- PR surface: Source +2. Total +2 across 2 files.
- Reproducibility: yes. Current main uses raw `slice(0, N)` at both reported user-facing truncation sites, and ... ludes terminal before/after output showing dangling surrogates before the fix and safe truncation after it.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head 74a0a32ed9.
- Required merge gates passed before the squash merge.

Prepared head SHA: 74a0a32ed9
Review: https://github.com/openclaw/openclaw/pull/97299#issuecomment-4820038635

Co-authored-by: zenglingbiao <zeng.lingbiao@xydigit.com>
Approved-by: takhoffman
2026-06-27 20:39:32 +00:00
Gio Della-Libera c29e1fe764 Add hosted external catalog feed loader (#95868)
Merged via squash.

Prepared head SHA: 76da9328af
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-27 13:20:05 -07:00
Ayaan Zaidi c52adf7505 test(telegram): cover retained preview chunk gaps 2026-06-27 12:30:57 -07:00
Ayaan Zaidi 199700de26 fix(telegram): replay retained preview gaps 2026-06-27 12:30:57 -07:00
Hannes Rudolph b14a95b3fd docs: revert v2026.6.9 changelog update (#97306)
This reverts commit ebf1ba70d5.
2026-06-27 13:04:51 -06:00
Hannes Rudolph ebf1ba70d5 docs: update changelog for v2026.6.9 (#97124) 2026-06-27 12:37:53 -06:00
Hannes Rudolph 78d70230b6 docs: align v2026.6.10 changelog heading (#97297) 2026-06-27 12:37:24 -06:00
shengting 98ed83f848 fix(model-fallback): don't rethrow provider-side AbortErrors as user cancellations (#90908)
* fix(model-fallback): don't rethrow provider-side AbortErrors as user cancellations

When the LLM API closes the connection mid-stream, the fetch layer
surfaces AbortError("This operation was aborted") with no external
abort signal triggered. The old guard `shouldRethrowAbort()` returned
false for these errors (because isTimeoutError matched the message),
so they fell through to the fallback loop but were never retried —
the error propagated up and produced SILENT_REPLY_TOKEN in group
sessions, permanently silencing the topic.

Replace the guard with a direct check: only rethrow AbortError when
the external abort signal is actually set (user/gateway cancellation).
Provider-side AbortErrors without an external signal now fall through
to the next fallback candidate, giving the system a chance to recover.

* fix(cron): forward abort signal into runWithModelFallback

Thread the cron executor's abort signal into the shared
runWithModelFallback call so that cron timeouts and cancellations
stop the fallback chain instead of retrying with the next candidate.

Previously, the run callback checked params.abortSignal?.aborted and
threw, but runWithModelFallback itself had no signal — so the new
guard in model-fallback.ts could not distinguish a caller abort from
a provider-side AbortError and would retry silently.

Also adds a focused regression test verifying the signal is forwarded.

---------

Co-authored-by: Shengting Xie <shengting@openclaw.ai>
Co-authored-by: yayu <yayu@yayuMacStudio.local>
2026-06-27 20:53:16 +03:00
Gio Della-Libera 1bdde66950 Doctor: expose plugin registry findings (#96169)
Merged via squash.

Prepared head SHA: cf4399955e
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
2026-06-27 10:00:50 -07:00
llagy009 2720ac06b7 fix(duckduckgo): guard out-of-range numeric HTML entities (#96583)
decodeHtmlEntities decoded numeric entities with String.fromCodePoint(parseInt(...)) without a range check, so an out-of-range entity such as &#99999999; or &#x110000; threw RangeError and made the whole results page fail to parse. Validate the code point is within 0..0x10FFFF and keep the original entity text otherwise. Add a regression covering decimal and hex out-of-range entities plus a valid astral entity.
2026-06-27 09:37:58 -07:00
miorbnli ce15f348bb fix(telegram): use idempotent retry context for delete/reaction (#96612)
reactMessageTelegram and deleteMessageTelegram passed context: "send" to
isRecoverableTelegramNetworkError, which disables message-snippet matching
(allowMessageMatch defaults to false only for "send"). Both operations are
idempotent (setMessageReaction / deleteMessage are safe to repeat), yet a
transient snippet-only network error (e.g. "socket hang up", "undici network
error" with no error code) was not retried — stricter than polling/webhook/
unknown, which all default allowMessageMatch to true. Users saw spurious
reaction/delete failures on transient network errors.

Add delete | react to TelegramNetworkErrorContext (additive) and use them at
the two callers. The helper default (context !== "send") is unchanged, so
delete/react now match polling/webhook/unknown. sendMessage keeps "send".

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 09:31:52 -07:00
llagy009 e5c3c59c67 fix(synology-chat): truncate sanitized input on UTF-16 boundary (#96574)
sanitizeInput truncated long messages with String.slice(0, 4000), which
can cut through an astral character's surrogate pair (e.g. an emoji at
the 4000-char boundary), leaving a lone surrogate in the sanitized text
passed downstream.

Use truncateUtf16Safe so truncation never splits a surrogate pair,
keeping the existing 4000-char budget and '... [truncated]' suffix.

Adds tests asserting the truncated output stays UTF-16 well formed and
that a supplementary-plane character is preserved when it fits.
2026-06-27 09:31:33 -07:00
llagy009 2e881ab1c6 fix(googlechat): truncate approval card text on UTF-16 boundary (#96573)
truncateText sliced the approval card text paragraph with String.slice,
which can cut through an astral character's surrogate pair (e.g. an emoji
straddling the 1797-char limit), leaving a lone surrogate in the card
text sent to Google Chat.

Use truncateUtf16Safe from the plugin SDK so truncation never splits a
surrogate pair, keeping the '...' suffix and the existing length budget.

Adds tests asserting the truncated Command card text stays UTF-16 well
formed and that an astral character is preserved when it fits.
2026-06-27 09:31:26 -07:00
llagy009 90c20d15c2 fix(slack): truncate approval mrkdwn on UTF-16 boundary (#96576)
truncateSlackMrkdwn cut approval Block Kit mrkdwn (Command/Request/
plugin description) with String.slice(0, maxChars - 1), which can split
an astral character's surrogate pair at the 2600-char preview limit,
leaving a lone surrogate in the chat.postMessage/chat.update payload.

Slice with sliceUtf16Safe so truncation never splits a surrogate pair,
keeping the existing ellipsis suffix and length budget.

Adds tests asserting exec command and plugin request mrkdwn stay free of
lone surrogates, plus a BMP regression keeping the existing limit.
2026-06-27 09:31:20 -07:00
llagy009 cb8bc71ff8 fix(whatsapp): elide auto-reply text on UTF-16 boundary (#96580)
elide truncated text with String.slice(0, limit) on a UTF-16 code-unit
index, so an astral character straddling the limit was cut into a lone
surrogate; the truncated-char count was also computed from the fixed
limit rather than the actual kept length.

Truncate with truncateUtf16Safe so a surrogate pair is never split, and
derive the truncated-char count from the kept length so the annotation
stays accurate.

Adds tests asserting no lone surrogate when the limit lands inside an
emoji and that a complete astral character is kept when it fits.
2026-06-27 09:31:16 -07:00
llagy009 b5c662f4f5 fix(codex): keep CLI session preview text on code-point boundaries (#96582)
truncateText shortened the cached lastMessage preview with value.slice(0, max - 3), which can cut a surrogate pair in half and emit a lone surrogate into the codex CLI session list JSON. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add regressions for both the history.jsonl and sessions/**/*.jsonl preview paths.
2026-06-27 09:31:00 -07:00
llagy009 d693ed4af3 fix(qqbot): truncate reminder job name on code-point boundary (#96575)
generateJobName truncated reminder content with String.slice(0, 20) on
a UTF-16 code-unit index, so an astral character (e.g. an emoji) landing
on the boundary was cut into a lone surrogate, producing a malformed
cron job name.

Truncate with the shared truncateUtf16Safe helper so a surrogate pair is
never split, keeping the existing 20-unit budget and ellipsis suffix.

Adds a test asserting the truncated job name contains no lone surrogate.
2026-06-27 09:30:52 -07:00
llagy009 6c5a9fde9f fix(msteams): truncate reflection prompt on UTF-16 boundary (#96578)
buildReflectionPrompt truncated the thumbed-down response with
String.slice(0, 500) on a UTF-16 code-unit index, so an astral
character straddling the 500-char cap was cut into a lone surrogate in
the reflection prompt built for the LLM.

Use truncateUtf16Safe so truncation never splits a surrogate pair,
keeping the existing 500-char budget and '...' suffix.

Adds tests asserting the prompt stays UTF-16 well formed when truncating
and that a boundary emoji is preserved when it fits.
2026-06-27 09:30:26 -07:00
Vincent Koc b8e3de1160 fix(telegram): recover stalled ingress spool claims (#97118)
* fix(telegram): drain ingress with native queue claims

* fix(telegram): bound native claim drain snapshots

* fix(telegram): recover pid-reused ingress claims

* fix(channels): block claimed candidate lanes

* fix(telegram): recover stalled ingress spool claims

* test(telegram): cover native claimNext drain stalls

---------

Co-authored-by: Dallin Romney <dallinromney@gmail.com>
2026-06-27 09:14:14 -07:00
mikasa b9c64142e2 fix(agents): keep missing tool results on current model (#95543) 2026-06-27 19:06:56 +03:00
Milosz Jankiewicz 84bcd500c9 feat(xai): route OAuth login through device-code flow (#97249)
Route xAI OAuth through device-code sign-in so remote and headless hosts do not need a localhost callback. Preserve the legacy manual `xai-device-code` auth choice/method as a compatibility alias to the same device-code flow.

Also migrate stale xAI token endpoints on refresh and fail fast on structured refresh errors while keeping retries scoped to detected HTML/Cloudflare challenge responses.

Verification:
- `node scripts/run-vitest.mjs extensions/xai/index.test.ts extensions/xai/xai-oauth.test.ts`
- `node scripts/run-vitest.mjs src/cli/models-cli.test.ts -t 'maps --device-code'`
- `node scripts/run-vitest.mjs src/commands/auth-choice.test.ts -t 'removed provider auth choice'`
- Crabbox local-container live smoke on exact head `fef3cb24afb01cd1f69cf04ef67ed11d71dfadb3`: xAI discovery and device authorization returned 200.
- `$autoreview` after the live smoke: clean.

Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
2026-06-27 10:02:57 -06:00
ly-wang19 f857e8d66e fix(terminal): wrap long wide-char words by visible width, not code-point count (#96746)
wrapNoteMessage measures every fit decision in visible columns, but its
splitLongWord fallback (for a single word longer than the line budget) sliced
the word into groups of maxLen code points. maxLen is a visible-column budget,
so a run of wide characters (CJK / fullwidth / emoji, 2 columns each) produced
lines up to twice the budget — e.g. a 24-char CJK word at maxWidth 20 emitted a
40-column line.

Accumulate grapheme visible width (the same unit visibleWidth uses) and start a
new segment when the next grapheme would exceed maxLen, so every wrapped segment
fits the budget; a single grapheme wider than the budget still emits, preserving
progress. ASCII wrapping is unchanged.

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:50:45 -07:00
clawSean a048aeae16 fix(qa-lab): treat claude-cli as Anthropic-family for live turn timeouts (#96567)
The live-frontier turn-timeout resolution only matched `anthropic/` models,
so `claude-cli/*` routes fell into the 120s fallback bucket instead of the
180s Anthropic floor, and claude-cli Opus missed the 240s Opus floor — even
though claude-cli serves the same Anthropic Claude models.

Recognize claude-cli as Anthropic-family via a small `isAnthropicFamilyModel`
helper, mirroring the existing `provider === "anthropic" || provider === "claude-cli"`
precedent in the aimock and mock-openai servers.

Co-authored-by: clawSean <260045960+clawSean@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 08:47:33 -07:00