Commit Graph
7404 Commits
Author SHA1 Message Date
Nariman Jelveh 763742dbc0 fix(embed): read the app browser's session from the cookie when its storage is empty (#4516)
Safari keys a framed origin's local storage by the top-level origin, so the
app browser embedded on a sibling subdomain finds no auth_token_v2 and shows
"Apps are unavailable". The session cookie is still sent there, so the embed
now falls back to /get-gui-token and keeps the token in memory.
2026-11-05 23:41:38 -07:00
Daniel Salazar 7f0cd1689b fix(embed): only sibling subdomains not served by core can frame the app browser (#4515)
/embed/apps now names the referring origin in frame-ancestors only when it
is exactly one label under the domain and that label is not one the server
answers itself (api, dav, js, www, native-app hosts) or the host of
api_base_url. Nested subdomains are refused too.

The query-string redirect now checks the raw URL instead of the parsed
query, so every request carrying a query string is redirected.
2026-11-05 23:10:34 -07:00
Daniel Salazar 9d22f39928 Ask for notification permission from Puter's dialog when the click has expired (#4514)
On a website, puter.notifications.enable() threw user_gesture_required
without prompting once the calling click's activation was gone: after
puter.auth.signIn() or requestPermission() opened a popup in the same
click, after a few seconds of awaited work, or when called on load. Apps
that ignored the error showed nothing.

It now shows Puter's consent dialog in that case and asks the browser
from its Continue click; Cancel resolves false. Calls made while an ask
is open share it. PuterDialog gets an onContinue option and a show()
that launch() also uses. Inside Puter's own desktop page the call still
throws, since it always runs from a click.

The push guide also notes that Chrome stops showing the prompt after
three dismissals and that its quieter messaging shows a bell icon.
2026-11-05 23:10:06 -07:00
Nariman Jelveh ef185c04ca Open apps from the dashboard's Home tab at their own URL (#4513)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Recently used apps on the Home tab opened /app/<name> in a new tab, which
loads the desktop and launches the app from there. They now open the way
Apps tab tiles do: a new tab at the app's index_url through launch_app's
newTab, or in this page when the browser refuses the tab. The Apps tab's
open-window lookup moves to helpers/focusAppWindow.js so both share it,
and the shared launch lives in helpers/dashboardAppLaunch.js, outside the
app browser so the embed bundle doesn't pull in launch_app.
2026-11-05 21:30:52 -07:00
Nariman Jelveh dec7d1fd96 Load the app browser embed's script from beside the GUI bundle (#4511)
The embed page always loaded /dist/apps-embed.min.js from the server,
while the GUI bundle and the embed build are published to gui_bundle's
location (a CDN in production). The server's /dist copy can be stale, so
the embed ran an old build. It now loads apps-embed.min.js from
gui_bundle's directory, keeping /dist when gui_bundle isn't set.
2026-11-05 21:07:05 -07:00
Nariman Jelveh fb9e7d70c6 Open apps from the embedded app browser at their own URL (#4510)
* Open apps from the embedded app browser at their own URL

Selecting an app in the /embed/apps browser now opens it in a new tab at
its index_url, with the same launch params the dashboard's tab launch
uses, instead of the GUI's /app/<name> landing. The tab is opened inside
the click and without noopener; the app's SDK asks the embed for its
token through the opener, so the token stays out of the URL.

Apps that need the desktop (built-in and godmode apps, private apps the
user can't open, or a failed token request) still open /app/<name>, with
the tab's opener cleared. Website shortcuts are unchanged.

The app URL, the app token request and the /rao report move out of
launch_app into helpers without desktop globals (appLaunchUrl.js,
appLaunchApi.js), so launch_app and the embed build the same URL.
uuidv4 moves to a helper the embed can import.

* Tag app launches with utm_medium and a utm_campaign naming the launch surface

Apps now get utm_medium=referral and utm_campaign=desktop, dashboard, or
app_browser_embed alongside utm_source=puter.com, so their analytics can
tell where in Puter they were opened from.
2026-11-05 20:31:30 -07:00
Nariman Jelveh 819e77bb4c Open dashboard apps and files in a new tab at the app's index_url (#4508)
* Open dashboard app tiles in a new tab at the app's index_url

Clicking an app tile in the dashboard now opens the app in a new browser
tab at its index_url, with the same query params (puter.app_instance_id,
puter.app.*, puter.auth.token, puter.domain, puter.api_origin,
puter.locale, puter.args, ...) its in-page iframe gets.

launch_app takes a newTab option: it runs the normal launch (private
access, file signing, token) and sends the caller's tab to the built URL
instead of opening a window. The tile click opens the blank tab inside
the click so popup blockers allow it, and closes it if the launch fails.

* Hand an app tab its token through the opener, not the URL

An app opened in its own tab no longer gets puter.auth.token in its URL,
which kept the token in browser history, and which puter.js ignored anyway
outside an iframe.

The GUI now holds the token for the tab it opened. Once the app loads,
puter.js (standalone, with puter.app_instance_id in its URL and an opener)
asks its opener for it at the configured GUI origin, takes only a reply
from that window at that origin, and then clears window.opener. The GUI
answers once, only to that tab at the origin it sent the tab to, and
drops the offer after two minutes. Sign-in and implicit auth wait for the
handoff, then fall back to the normal sign-in flow when no token came
(opener closed, slow load, redirect to another origin).

Built-in apps join explorer in opening in-page, since they belong to the
desktop.

* Open files from the dashboard Files tab in a new tab

Opening a file in the Files tab (double-click, Enter, tap, or the Open
menu item) now opens its app in a new browser tab, the same way app
tiles do. The tab is opened inside the gesture and handed to open_item,
which passes it to launch_app or closes it when the file opens no app.
Web links reuse the tab, cut off from the dashboard. A second file of a
multi-select Enter, whose tab the popup blocker refuses, opens in-page.

The opened file's signed read/write URLs don't expire, so a tab gets
them through the opener handoff with its token instead of in its URL;
the URL keeps only puter.item.uid as a marker. puter.js delivers the
handed-over item to onLaunchedWithItems / onItemsOpened, and
wasLaunchedWithItems answers true while it's on its way.

launch_app now closes a tab it was given whenever the launch fails, so
callers don't each have to.

* Open "Open With" apps from the Files tab in a new tab

Picking an app under Open With in the dashboard Files tab now opens it
in a new browser tab, like opening the row. The tab is opened in the
menu click, or, when the "always open with" prompt comes first, right
after it's answered, since that click counts as a fresh user gesture.
If the popup blocker refuses the tab, the app opens in-page. The desktop
keeps opening in-page.

* Wait for a GUI-opened tab's token in getUser, whoami and getProfile

A tab the GUI opens for an app gets its token over postMessage moments
after puter.js loads. These calls short-circuited on a missing token, so
an app reading the user at startup failed with a 401 (or a null profile)
even though the token was on its way. They now wait for the handoff
first, the same way sign-in already does.

* Close an app's blank tab before alerting that its launch failed

A launch denied for a private app, or one whose token couldn't be
fetched, awaited its alert while the blank tab it was given stayed in
front. The alert renders on the dashboard behind it, so the user was
left on an empty tab until they went looking. The tab is now closed
before the alert shows.

* Launch an app tile in-page when the browser refuses its tab

When window.open returned null (a popup-blocking setting, or a context
that can't open tabs), clicking an app tile did nothing at all. It now
falls back to the in-page launch it had before, as opening a file from
the Files tab and Open With already do: an open window is surfaced,
otherwise the app opens maximized with the tile's open morph.

* Open no tab for a file opened from the Trash

Opening a file in the Trash only explains that it can't be opened, but
the Files tab still opened a blank tab for it first, which then closed
again: a flash of a new tab before the alert. Trashed files (by the
path a shortcut resolves to) now get no tab.

* Don't count a tab launch as one that can report interactivity

launch.ipc_tracked on app-is-ready is the denominator for the
app-interactive series: launches that could produce one. An app opened
in a new tab never signals over IPC, yet was recorded as tracked, so
every tab launch counted as an interactive span that never came. Tab
launches are now recorded as untracked.

* Leave openerLaunch_ unset when there is no GUI opener to ask

requestLaunchFromOpener resolved null asynchronously even when there was
no opener at all, so openerLaunch_ was briefly set on every boot with an
app instance id in the URL. A reloaded app tab (opener already cut off,
URL still marking the opened item with puter.item.uid) reported
wasLaunchedWithItems() as true to code running in the same task as the
boot, though no item would ever arrive. With no opener to ask it now
returns null synchronously and nothing is left pending.

* Call an app tab launch off when the user closed its tab

Closing the blank tab while the launch's fetches were in flight didn't
stop the launch: it still navigated the closed tab, offered it a token,
and recorded the app as opened (recent apps and /rao). The launch now
checks the tab before sending the app there and ends as not launched,
without an alert, when the user has closed it.

* Keep a same-origin opener instead of asking it for a GUI launch

Any top-level page with an app instance id and an opener asked that
opener for its launch, waited out the timeout when no answer came, and
then nulled window.opener. A page an app opened from its own origin (a
pop-out of itself, say) can't have the GUI as its opener, yet it waited
five seconds before its launch item and sign-in, and lost its link to
the app window that opened it. An opener readable from this page, on an
origin other than the GUI's, is now left alone and not asked.
2026-11-05 19:27:46 -07:00
Daniel Salazar 7355eaa77b feat(realtime): run the host prompt check per response and log its cost (#4507)
Each response that starts runs `ai.prompt.validate` on the text the client
sent since the previous check (instructions, message text and transcripts,
function call output), and its usage follows as `ai.prompt.cost-calculated`
under the same completion id. A refusal closes the session with
`permission_denied`. Nothing runs when no host listens for the check.
2026-11-05 18:05:43 -07:00
Nariman Jelveh d55aac1f98 Add embeddable Puter toolbar and shared app browser (#4019)
* Add embeddable Puter toolbar and shared app browser

Provide deployment-scoped iframe routes, a responsive host helper, read-only app browsing, saved-account switching, and new-tab account and upgrade links. Validate account sessions before switching and keep credentials inside the GUI origin.

Verified with 182 targeted tests, production GUI build, backend type checking, lint, and a real-backend browser smoke test. Translation checker could not run because tools/check-translations.js is missing.

* Prevent embedded toolbar from scrolling when menus open

* Stop double-encoding translated toolbar labels

i18n() already HTML-encodes its result, so wrapping it in escape() again
turned accented labels into literal entities (e.g. "Schlie&szlig;en") for
screen readers, and .text() did the same for the error message.

* Load only the preferred language in the toolbar and app browser embeds

Importing i18n.js pulled every translation into each embed, making the
toolbar bundle 1.1 MB. The embeds now bundle English and fetch the user's
language on demand (toolbar 1.12 MB -> 179 KB, app browser 1.18 MB -> 237 KB).
The desktop bundle keeps all translations.

* Keep toolbar buttons in place when a menu opens near the viewport edge

On phones the expanded frame was clamped to an 8px viewport margin, which
moved the whole toolbar row (by the header padding minus 8px) under the
user's finger. The frame now shrinks to fit beside and below the collapsed
toolbar, and only falls back to shifting when there isn't enough room.

* Don't pull focus back into the toolbar when the host page closes its menu

Clicking a host text field while a toolbar menu was open closed the menu and
then refocused the toolbar button, so the host field lost focus and typing
went nowhere. Focus is now restored only while the toolbar document has it.

* Close toolbar menus when the empty part of the expanded frame is pressed

While a menu is open the toolbar frame covers host content beside the
buttons; presses there reached neither the host nor the toolbar's outside
click handling, so the menu stayed open. They now close it.

* Omit the empty email line in the toolbar account menu

Temporary accounts and saved accounts without an email rendered an empty
element, adding a stray gap under the username.

* Stop user-hosted sites from framing the toolbar and app browser embeds

frame-ancestors allowed every *.<domain> subdomain, which on the default
self-hosted layout includes user sites (*.site.<domain>) and private apps
(*.app.<domain>), letting any hosted page clickjack sign-out and account
switching. The policy now names the embedding page exactly, from the
request's referrer, and refuses the user-content hosting domains.

* Use the dashboard danger color for the toolbar sign-out confirmation

The hardcoded red had 3:1 contrast on the dark panel, below WCAG AA for
14px text. The dashboard's danger token is 4.8:1 in light and 6:1 in dark.

* Drop the dashboard's hamburger clearance from the embedded app browser

At narrow widths the dashboard pads the search 60px down to clear its fixed
menu button. The embeds have no such button, so the toolbar's app panel
lost 90px of a ~500px frame to empty space above the search.

* Keep the toolbar's Upgrade button legible in dark mode

Upgrade was filled with the link color, which dark mode lightens to
#6b8cff: 3.1:1 under white text. Use the dashboard's primary-button fill
(4.6:1 in both themes) and its hover color, and drop the button classes
nothing styles on this page.

* Keep the toolbar's apps icon legible on hosts in the other theme

The toolbar follows the OS theme but draws on the host page, so in dark
mode on a light host its icon was #d4d4d4 on white (and the reverse on a
dark host). Give the apps toggle a surface of its own, like the avatar
beside it, and the session-unavailable notice one too.

* Let the toolbar sign out of a session that already ended

When the session was ended elsewhere (revoked, or signed out in another
tab), the anti-CSRF and logout requests answer 401 and sign-out failed
on every retry, stranding the stale token. Treat 401 as already signed
out and finish clearing it locally, as the desktop does; other failures
still keep the session and show the error.

* Keep the toolbar's slot following the host layout while a menu is open

The helper reserved the toolbar's space with a span frozen at the
frame's resolved size and margins, so a host that right-aligns it with
margin-left: auto (or sizes it in percentages or media queries) left the
open menu anchored to a stale spot, overflowing the header, after a
resize or rotation. Reserve it with an empty clone of the frame instead,
which the page's own rules keep laying out; if they target
data-puter-toolbar, which the clone drops so hosts still find one frame,
hold the frame's measured size.

* Reuse isOrgSeat for the toolbar's Upgrade check

The toolbar re-implemented the dashboard's organization-seat test
inline. Use the shared helper so both surfaces agree on who sees an
upgrade offer.

* Point comments at AppBrowser.js, where the Apps tab code now lives

The constants and methods these comments name moved out of TabApps.js
when the app browser became shared.

* Restore the license header on the new embed and toolbar files

TabApps.js lost its AGPL header when its code moved to AppBrowser.js,
and the new embed, toolbar, and host-helper modules were added without
one; every other GUI source file carries it.

* Remove the embeddable toolbar, keeping the standalone app browser

Drop /embed/toolbar, its page and host helper, their styles, strings and
tests, and the dashboard's ?upgrade=1 deep link that only the toolbar's
Upgrade button used. /embed/apps no longer allows the GUI origin to frame
it with a wildcard over deployment subdomains, which existed only for the
toolbar's nested app browser, and its bootstrap config no longer carries
the domain the toolbar read.
2026-11-05 17:19:05 -07:00
Daniel Salazar c6987d9606 feat(notifications): push unless a page is visible and focused (#4506)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
A push is skipped only when a page that got the notification live was
visible and focused. Hidden, unfocused or unanswering pages no longer hold
it back, and pages no longer show browser notifications themselves, so the
push reaches every subscribed browser.

- Receipts carry the page's visibility and focus as of arrival; the settle
  gains `handled`. `showsOwn` is gone.
- Pages send `notif.handled { uid }` on the socket a notification arrived on
  when they got it while visible and focused. Core relays it as the
  node-local `notif.handled` event (60 a minute per user) for extensions.
- puter.js drops the page-shown notification path; push-sw.js drops its
  page-show message. Handlers still run live in the page.
- The desktop registers /sw.js only when push is turned on, updates it when
  subscribed, and removes one left without a subscription. Its hidden-tab
  banners are gone.
- Docs describe the focus rule.
2026-11-05 14:43:33 -07:00
Filip Kujundžić 796b42c74d Merge pull request #4475 from HeyPuter/fix/openai-cache-write-tiers 2026-11-05 21:27:46 +01:00
404oopsandClaude Opus 5.5 e2043a5e58 Merge remote-tracking branch 'origin/main' into fix/openai-cache-write-tiers
Main's docs cleanup (#4504) moved the prompt caching docs under
per-provider sections; the OpenAI paragraphs now live under
OpenAI models > Prompt Caching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:55:55 +01:00
Filip Kujundžić 8b98c7ada8 Merge pull request #4503 from HeyPuter/codex/openai-image-default
feat(ai): add GPT Image 2.6 and default to Flare
2026-11-05 19:53:05 +01:00
Filip Kujundžić b27e84d181 Merge pull request #4457 from HeyPuter/fix/convo-bounded-recovery
Bound conversation recovery and report omitted replay context
2026-11-05 19:46:10 +01:00
404oopsandClaude Opus 5.5 add0e7be7d fix(convo): report routine context trimming as context_trimmed
A reconnect that left older items out of the 40-item, 48 KiB restore window
fired an error event with context_restore_failed, the code a provider
refusal uses, with the count only in the message. Every reconnect of a long
conversation raised an error that apps treating error as fatal would surface.

Trimming now fires a nonfatal context_trimmed event, before ready, with the
count in omittedItems. context_restore_failed is kept for a provider refusal
of a restored item or a failed send of the replay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:28:02 +01:00
404oops b5d221ede6 fix(convo): bound recovery attempts and report omitted context 2026-11-05 19:28:02 +01:00
404oops 3137a631a7 fix(realtime): reuse settled credit for post-record continuation 2026-11-05 19:28:02 +01:00
404oopsandClaude Opus 5.5 57706d8093 fix(realtime): bound unacknowledged audio by duration, not a byte cap
The per-commit cap of 1,280,000 bytes assumed G.711, so a PCM16 session
(48,000 bytes/s, the SDK's format) with transcription on was closed with
1013 after about 27 seconds without a commit: silence under server VAD,
one long utterance, or a long assistant reply.

Unacknowledged audio is now a duration at the provider-confirmed input
format (the lower rate while a requested format is unconfirmed). Without
turn detection it counts all audio since the last commit or clear; with
server VAD only speech from speech_started, plus the buffer after an
explicit commit, since the provider drops silence. Each started 160 s
claims one spare item as its hold instead of closing the session; a
commit, clear or input_audio_buffer_commit_empty returns the holds to the
pool. If the upstream closes first, every claimed item is billed. The
settle no longer commits silence.

The relay test file now expires the in-memory per-IP pre-auth window
after each test; every test connects from loopback and the added tests
pushed the file past 120 upgrades a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:27:54 +01:00
Reynaldi ChernandoandClaude Opus 5.5 fec1d1916f Merge remote-tracking branch 'upstream/main' into codex/openai-image-default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:03:08 +07:00
Juan Fernando CastroandReynaldi Chernando da369039e0 docs: complex teams recipe (PUT-1986) (#4095)
* docs: complex teams recipe (PUT-1986)

Showing which teammates have a file open, built from a folder shared with the
team plus `puter.events` watching it. There is no presence API, so the recipe
is about composing the two.

One use case per the recipe guidance. Uses the masked path from
`listShared()` so members address the owner's folder, keys presence files on
`uuid` rather than username, and re-reads the folder on each event so a missed
delivery cannot leave the list wrong.

* refine

---------

Co-authored-by: Reynaldi Chernando <reynaldichernando@gmail.com>
2026-11-06 00:53:25 +07:00
404oopsandClaude Opus 5.5 4f36152850 fix(realtime): hold transcription credit per item at the selected model
Input transcription reserved 64 items up front at the costliest model's
price and largest cost factor, about $7 before factors per session. Ordinary
subscribers couldn't enable it, and concurrent sessions were refused while
balance was left.

Each item now holds its own credit at the selected transcription model's
budget and cost factor. Two spare items are reserved before configuration or
audio is forwarded and refilled as the provider commits items, within the
64-item pending cap. A commit with no spare reserves its own item. A model
change releases the spares and reserves new ones at the new price; committed
items keep their hold. When the account can't cover another item, client
reads pause while the session's own holds may still free credit, and the
session is refused with insufficient_funds once it holds none.

Audio awaiting a commit acknowledgement is now tracked separately from pool
capacity and reset by each acknowledged commit or clear. If the upstream
closes before that acknowledgement, the audio bills as one item. Previously
the fallback required an untouched window, so a lost acknowledgement after an
earlier completed item went unbilled; the cumulative byte count could not
simply be reused without charging earlier work twice. The uncommitted-audio
cap is now 160 seconds of audio instead of a per-window total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:49 -07:00
404oops 86db96d587 fix(realtime): reserve and settle pending transcription work 2026-11-05 10:23:49 -07:00
404oops 214ffb6605 fix(realtime): evict revoked credentials and recheck paid eligibility 2026-11-05 10:23:48 -07:00
404oops 871cc2cf87 fix(realtime): bound upgrades before credential admission 2026-11-05 10:23:48 -07:00
404oops ba002be0f7 fix(convo): reject interrupt readiness failures asynchronously 2026-11-05 10:23:47 -07:00
404oops 2832926f8d test(realtime): isolate credential attempt budgets between cases 2026-11-05 10:23:47 -07:00
404oops 1ab1c04760 fix(realtime): redact structured provider diagnostics 2026-11-05 10:23:47 -07:00
404oopsandClaude Opus 5.5 63a98f89ff test(metering): find the actor key in any meterIncr argument
The buffered write now passes the claim key too, which moves the counter
key's position. The image provider's write-ordering test matched it by
index and stopped seeing the write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oops d2cef27085 perf(metering): avoid retries for growing buffers 2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 74b60c1227 fix(metering): type the array read's retry callback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 2ed3e78dfa fix(metering): keep buffered and store reads from missing or doubling usage
Three reads still got a flush wrong:

- A write re-sent after its reply was lost, found already applied, left
  the claim without touching a warm cached base, so balance reads stayed
  short of what the store held. The claim and the base are now dropped
  together, and the next read seeds a fresh base from the store.
- A read with no cached base took the buffer, then the store; a flush
  that wrote a batch and left its claim in between had it counted twice.
- An exact read added another deployment's claim but not the usage that
  arrived behind it, which that read can't flush, so a charge crossing
  the allowance went entirely to the allowance.

Reads that go to the store now take everything buffered (delta and
claim), read the store consistently, and read the buffer again, retrying
when it shrank in between. Single and array reads and exact reads all
use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 22ebd353b8 fix(metering): read the claim before the store in an exact read
`readExact` read the store and the outstanding claim side by side. When
a flush wrote a batch and took it off its claim between the two, the
store read had the counter from before the write and the claim read had
nothing, so the batch was missed: 10 units recorded, 0 read.

The claim is now read first. An amount gone from it by then was written
before the store read, which is consistent, so the read has it. When the
claim changes during the store read, the batch may be in both, so the
read is retried, up to three times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 7551e12370 fix(metering): count claims on the write path and read missing bases exactly
The running total `incr` returns and `readExact` also missed usage a
flush had claimed but not yet written. Both feed how a charge is split
between the allowance and purchased credit, so an account could be
charged to its allowance for usage that had already used it up. Both
now add the counter's outstanding claim.

A read or seed with no cached base asked the store with an ordinary
read, which could miss a write a settle had just made; it now reads
consistently. A write-through after a cache failure drops the cached
base, which doesn't have the write, so the next read reseeds it.

The dirty-set reconcile test no longer depends on how many flush cycles
earlier tests ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 6e29e32fe9 fix(metering): count usage mid-flush in balance reads
A flush takes a counter's buffered amounts aside before writing them
onward, and reads only looked at the live delta and the cached base.
From the claim until the settle replaced the base, those amounts were
missing from every read: one store write normally, up to the orphan
sweep if the write failed. Balance checks, credit reservations among
them, could see an account as having more than it does.

Reads now add the counter's outstanding claim. A batch written onward
leaves the claim and joins the cached base in one step, so it stays
counted until the settle replaces the base with the store's view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:46 -07:00
404oopsandClaude Opus 5.5 54bfc0ffe5 test(puter-js): prove microphone chunks hit the closing-socket gap
The browser test now counts the chunks the transport refused while the
socket read as closing, and requires at least one, so it can't pass
without exercising the gap. The socket stays closing until the
connection drops instead of going back to open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:45 -07:00
404oopsandClaude Opus 5.5 c27883134b fix(puter-js): keep microphone capture when the socket closes first
A conversation still reads as connected for a moment after its socket
starts closing. A microphone chunk sent in that moment got
`session_closed`, and the capture handler treated that like a real
media failure: it stopped the tracks and the AudioContext, so the
replacement session after the reconnect had no microphone.

The built-in microphone now drops a chunk the transport refuses with
`session_closed` or `session_not_ready`, as it already did for
`queue_full`, and keeps capturing. A reconnect resumes sending; a final
close still stops capture through the conversation, and processor or
other errors still release it. Dropped chunks aren't replayed, and
public sends keep reporting the transport error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:45 -07:00
404oopsandClaude Opus 5.5 cf4c1f6b64 fix(puter-js): end the conversation on an oversized event mid-reconnect
A server event over the 4 MiB limit closes the socket with 4009, which
ends a connected conversation. During a reconnect attempt the same close
was reported as `session_closed`, which is retried, so the conversation
tried ten more times and finally closed with the original drop code.
A replacement session opened after a restart notice was reopened again
and again for the same reason.

A 4009 close now ends the conversation during a reconnect too, reporting
4009, and a replacement session that hits it isn't reopened. Tests cover
both, a multibyte event of exactly 4 MiB, and a whole retrieved item
through the relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:44 -07:00
404oopsandClaude Opus 5.5 305d20ab1c fix(puter-js): accept provider events up to the relay's 4 MiB cap
The conversation transport refused any server event over 64 KiB with a
terminal 4009 close, the limit meant for what the SDK sends. The relay
forwards provider events up to 4 MiB, and a session.updated echo or a
whole audio item easily passes 64 KiB, so valid conversations ended.

Incoming events now have their own 4 MiB limit, matching the relay,
counted in UTF-8 bytes (a string short enough that it can't exceed the
limit isn't encoded to check). An event over it still closes with 4009
and doesn't reconnect. Uploads keep their 64 KiB cap, and malformed
events are still ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:44 -07:00
404oopsandClaude Opus 5.5 0ce6b0646a perf(metering): stop releases from churning the reservation version
Every release bumped the balance version, so an account's own short-lived
holds (each realtime session probes one and releases it at once) could
keep its other reservations reading a stale version until they were
refused. The usage a hold stands for already bumps the version when it's
recorded, before the hold is released, and a release without usage
leaves the balance as it was, so releases no longer bump it.

The admission read also uses the cached add-ons record when it shows no
purchased credit left, which can only understate the balance; it reads
consistently only while credit remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:44 -07:00
404oopsandClaude Opus 5.5 6b5c4f3d9e fix(metering): refuse a reservation whose balance never holds still
A reservation that found the balance changed three times in a row used
to skip the version check on its fourth attempt, which brought back the
race the check closes; an account could provoke that itself. It now
waits a short randomized time between attempts and refuses after five.

A missing version key now starts from a random value, so a version read
before the key expired can't match its successor. Credit grants,
allowance changes, manual usage corrections and a usage write that
fails partway also bump the version, and the store refuses an amount
it can't hold instead of storing a negative hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:44 -07:00
404oopsandClaude Opus 5.5 d6fdc5f268 fix(metering): reserve AI credit only when it fits, in one step
`reserveAiCredits` checked the balance and then took an unconditional
hold, so concurrent requests could all pass the check before any hold
was visible; two 16.2M µ¢ holds were admitted against a 20M µ¢ balance.

The hold store now takes a hold only if it fits beside the live holds,
in one Lua script on the actor's hold key. A per-user version, bumped
when usage is recorded and when a hold is released, lets the script
refuse a balance read that a settlement has since overtaken; the
service reads the balance again and retries, and skips the version
check on its last attempt rather than refuse an account whose balance
keeps moving. Both keys share the actor's hash tag, so the script runs
on Redis Cluster. The balance read for this check reads purchased
credit consistently.

`reserveCreditsIfAffordable` carries this through the service, and the
AI facade calls it once with the cost factor applied. `reserveCredits`
keeps its unconditional semantics. When the cache is unreachable, the
balance alone decides and nothing is held, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:44 -07:00
404oopsandClaude Opus 5.5 4f3f946941 fix(realtime): judge a refusal by the plan it was decided on
A plan or funds refusal was classified by a plan lookup made after the
decision. A fallback plan is cached for only a few seconds, so when it
ran out in between, the refusal was judged against the recovered plan
and still ended the session with a terminal 1008. The plan is now also
checked just before each decision, and a refusal is retryable if either
check found a fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:43 -07:00
404oopsandClaude Opus 5.5 4e36672b6b test(puter-js): fail credential lookups until the relay hits one
The reconnect test made the next credential lookup fail once, but the
SDK's own socket.io connection could take that failure before the
relay's reconnect attempt did, so the relay never saw a failed lookup
and the test failed in CI. Lookups now fail until the relay has refused
an attempt on one, then recover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:43 -07:00
404oopsandClaude Opus 5.5 6b0b530ab1 fix(realtime): retry instead of refusing when the plan lookup fails
When the billing lookup failed, metering answered with a provisional
fallback plan, so the relay refused the session with
`subscription_required` or, against the fallback's allowance,
`insufficient_funds`, both terminal 1008 closes, and the SDK showed the
upgrade prompt to a paying account. A plan or funds refusal decided on a
provisional plan now sends `internal_error` and closes with 1011, at
connection and mid-session, so the client retries once the lookup
recovers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:43 -07:00
404oopsandClaude Opus 5.5 68b8f341e9 test(realtime): cover a revoked session credential
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:43 -07:00
404oopsandClaude Opus 5.5 c3756d3ac3 fix(realtime): tell transient server failures apart from bad credentials
A credential lookup that threw (a database or cache outage) was treated
as no actor, so the session got `invalid_api_key` and a 1008 close. That
is terminal for the SDK, which then ended an established conversation
that only needed to reconnect. A failed lookup now gets `internal_error`
and closes with 1011, which the SDK retries; missing, invalid and
revoked credentials keep `invalid_api_key` and 1008. Nothing is cached
for the failed lookup, and the cause is logged server-side only.

A session whose usage records pile up behind metering now closes with
1013 instead of 1008, still with `rate_limit_exceeded`. Queued usage is
still recorded and its holds released. An unexpected setup failure now
sends `internal_error` before its 1011 close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:43 -07:00
404oopsandClaude Opus 5.5 0c927ac5a4 fix(realtime): listen for client socket errors before refusing a session
A session refused while the server drains returned before its client
error and close listeners were registered, so a frame that errored the
socket during the close handshake (an oversized one, for example)
emitted an unhandled `error` and ended the process. The listeners now
go on as soon as the session exists, ahead of every refusal.

The 404 answer to an upgrade for another host also ends a raw socket
that has no error listener once Node hands it over; it now has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:23:42 -07:00
Neal Shah 64c3b3c0ce subdomain and worker recipes complex, writing full stack applications with both (#4206) 2026-11-05 21:33:21 +07:00
Reynaldi Chernando e9a4c7a461 ai docs cleanup (#4504)
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
* ai docs cleanup

* fixes
2026-11-05 19:15:32 +07:00
Filip Kujundžić be9cd9eb89 feat(ai): add GPT Image 2.6 and default to Flare 2026-11-05 11:13:43 +01:00