* test(ai): end every realtime relay test's upstream before the next
The fake upstream now answers response.create with response.created first,
as OpenAI does, so closing those sessions no longer waits out the settle
window.
* feat(ai): ask realtime clients to reconnect before a restart
Open realtime sessions get a puter.reconnect event when the drain starts,
with close_in_ms set to the drain window, and new sessions are still refused
until the 1012 close at shutdown. A host extension can announce a restart
early by emitting serverRestartAnnounced; that only sends the notice, so a
false alarm leaves the server working normally. A session gets the notice at
most once a minute.
onServerDrainStart now receives the drain duration.
* fix(ai): retry realtime reconnects for about a minute and wait out offline browsers
Five attempts gave up after about 15 seconds, shorter than a server restart.
The conversation makes ten attempts with the backoff capped at 10 seconds,
and while the browser reports being offline it waits for the online event,
up to two minutes per reconnect, instead of spending attempts. close() ends
either wait and removes its listener.
* feat(ai): restore recent conversation context after a realtime reconnect
The provider starts a fresh session on every reconnect, so the conversation
lost all earlier items. The SDK now records finished items it can recreate
from the provider's own events: user text, user speech with a transcript,
assistant replies as text, and function calls with their outputs. After the
new session acknowledges its settings, and before ready fires, it adds back
the newest of them, up to 40 items and 48 KiB, which stays under the relay's
upload burst.
Assistant replies are cut to what the user saw or heard: the deltas shown
before an interruption, and the share of the transcript matching the audio
played before a truncation or a drop. Deleted items, unfinished replies,
images, speech without a transcript and incomplete function calls are left
out, as are function outputs whose call falls outside the window. Restored
copies take over their new item IDs. A refused item is reported once as
context_restore_failed without failing the reconnect.
* feat(ai): move a realtime conversation to a new session on the restart notice
On puter.reconnect the conversation waits until it is idle: no reply
requested, generating or playing, no user turn being spoken or transcribed,
no function call awaiting its output and no acknowledgment pending. Then it
reconnects through the usual path, emitting reconnecting (code 1012) and
ready with reconnected: true, without an error. Until then the session keeps
working, and if the server closes it first the normal 1012 reconnect takes
over. close() at any point stops the wait.
* docs(ai): realtime restart notice, context restore and reconnect limits
* test(ai): realtime reconnects against the local server
The mock provider answers with item events and whole replies like OpenAI.
New cases cover a dropped connection (cut through a TCP proxy so the client
sees 1006), a host-announced restart, and a draining server refusing the
reconnect; the first two check the provider receives the restored items
before the next turn. The browser media test waits for captured audio and
the reply instead of a fixed sleep. The live suite gains a tool and text
context round trip across a restart notice.
* ci: run the live realtime conversation tests with the AI provider suite
They need the OpenAI key, which only this workflow has, and skip themselves
without it.
* fix(ai): open the replacement before leaving a realtime session on the restart notice
The handover closed the current session and then reconnected, so every
notice cost a second or two in which sends failed and microphone audio was
dropped. On a single server that refuses new sessions while it drains, the
retries ran out during the drain and the conversation closed sooner than if
it had stayed. The conversation now opens a replacement session as soon as
the notice arrives and switches only once it is open and the conversation is
idle. A refused replacement is retried with backoff while the current
session keeps working; when the server closes it first, the held replacement
is used right away.
The idle check also missed some in-progress work:
- a reply created while the user was still speaking ended the user's turn;
speech now lasts until its audio is committed or cleared, and a committed
turn waits for the reply server turn detection creates
- only the newest function call was checked; every finished call since the
last user message needs its output, and calls cut off by a cancellation
don't count
- native session.update and conversation.item.create events the provider
hadn't acknowledged could be lost; the switch waits for them, up to the
acknowledgment timeout for items
- only the newest transcription was waited on; every committed item is
- clearing playback didn't recheck the switch
Restored items stay internal: the new session's copies of them no longer
reach listeners. At a reconnect, every queued item the user didn't hear is
cut, not just the one playing. A notice from a failed reconnect attempt is
dropped.
* test(ai): realtime replay after split settings, and a draining server refusing the replacement
The relay accepts the context replay right after two full-size settings
updates. Against the local server, a draining relay refuses the replacement
session while the current one keeps answering.
* ci: build the SDK in the live realtime step so it stands on its own
* fix(ai): a failed realtime reconnect attempt no longer poisons the next
An attempt that dropped after sending its session.update but before the
acknowledgment left that event ID queued, so the next attempt's
acknowledgment was matched to it and timed out, and so on until the
conversation closed. Turn state is now reset after each failed attempt.
* fix(ai): bound the realtime handover's waits and stop retries that can't succeed
- The first replacement opens within a randomized 3 seconds of the notice,
so sessions notified together don't all open at once.
- A replacement refused with a code retrying can't fix, such as
insufficient_funds, ends the attempts; the server's close still falls back
to the usual reconnect.
- A held replacement that gets an error or its own server's notice is closed
and replaced.
- A held replacement used after a drop gets a 3 second acknowledgment
timeout, since its socket may have died with the network.
- close() during the switch closes the held replacement too.
- The reply to a committed turn and each transcription hold the switch for
at most 10 and 30 seconds, like app items already did, and an app item is
released only by an added item of its type, role and call ID.
* docs(ai): the realtime restart notice isn't guaranteed before every restart
* chore: move to Node 26
Docker image and CI run on Node 26 (bookworm-slim, same Debian base as before).
Node 26's bundled undici reads the global dispatcher from the
`undici.globalDispatcher.2` slot, so the guard's 10-minute timeout wrap on
`.1` no longer reached plain fetch() and provider calls fell back to the
5-minute headers timeout. The guard now wraps every present slot and tracks
wrapped dispatchers in a set, since compose() can return a Proxy that writes
a marker property through to the original dispatcher.
* test(puter-js): disable Node's web storage in test workers
Node 26 defines its own localStorage global, undefined without a storage
file, and vitest's jsdom environment doesn't replace globals that already
exist, so jsdom tests saw localStorage as undefined.
The suite was CPU-bound on one runner, and about half of that CPU went to
re-importing the backend for every test file.
- Run the suite as 4 shards and merge their blob reports for coverage.
`test (pr)` stays as the required check and fails if any shard fails.
- Drop the per-PR base leg; pushes to main publish the coverage PRs
compare against.
- Files that boot a server without module mocks or extensions run in a
`shared-worker` vitest project (`isolate: false`), so the backend is
imported once per worker. testSharedWorkerSetup.ts resets the redis
mock, configContainer and extensionStore between files.
Skipping the whole matrix job via a job-level `if` collapses it to a
single check with the unevaluated name template ("test (\${{
matrix.artifact }})") instead of expanding to "test (base)"/"test (pr)",
since there's no matrix context to fill in when the job never runs. That
check never matches a required-status-check pinned to "test (pr)", so
PRs that skip backend/puterjs testing block forever waiting on a check
that will never be reported. Gate the steps instead so the matrix always
expands with stable names, doing no real work when irrelevant.
- CODEOWNERS: default owners @Salazareo @ProgrammerIn-wonderland @jelveh
@jfcastro92, plus @reynaldichernando for src/docs.
- dependabot.yml: ignore semver-minor/major npm updates, patch only.
- backend-tests.yaml / puterjs-tests.yaml: trigger unconditionally and gate
the real work behind a dorny/paths-filter job instead of a workflow-level
path filter, so the checks always report (skipped when irrelevant) and
can be marked required. Also pins matrix job names to the artifact label
instead of the branch ref, which previously made the check name change
per PR and unusable as a required check.
Changes are:
- global egress metering
- remove file egress cost
- introduce file op cost for the per request cost s3 has
- enforce fs read/download etc to through 402 when out of usage; allow for subdomains
- enforce kv metering when out of usage through 402; allow for workers
- jsdoc as source of truth for puter.js types
- kv driver caching for get and batchget operations with decreased costs
- declare rate + concurrency limits on every route and driver that lacked one
- add acquireConcurrent for websocket connections and the DAV mount
- bucket AI models by identity key only; keep resold duplicates of any vendor
- skip recently-failed provider routes; cap the fallback chain at 3 attempts
- let full-access access tokens bind a worker to an app their own user owns
- cache resolved subscriptions so tiered limits don't add a round trip
* test: tests for puter.js
* fix: ship lockfile for coverage devDeps; tolerate missing base coverage
npm ci failed on CI because package.json gained the babel/istanbul
devDependencies without the matching package-lock.json update. Also make
the coverage workflow's base leg best-effort so a base ref that predates
the coverage script reports without the comparison column instead of
failing the run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci: add docs build workflow
Add CI workflow that runs npm ci and npm run build in src/docs
whenever files under src/docs change in a pull request.
Closes#3225
* ci: also trigger docs build when workflow file changes
* ci: address docs build workflow review
* chore: upgrade git-date (used for release notes generator)
* chore: update vitest, vitest plugins, and svgo
* chore: run `npm audit fix`
* chore: run `npm audit --force`
* meta: empty commit to run git tests
* fix: tests gone because of a... syntax error?
I question GitHub's judgement of handling a syntax error in the file
that defines tests to run on PRs by pretending everything is fine and
passing all PRs. That to me seems like the worst possible way to handle
that. I think they should have done it not like that.
* fix: only set authToken if present for apps
* fix: keep bootstrap in url for app to do whatever
* fix: tests
* fix: app merging
just cleaning up how merging subdomain and canon apps work, namely, persisting data better and making sure its temp alias are deleted when appropriate
* fix: tests oom
* Remove DevConsoleService and related dev console widgets
Eliminates `DevConsoleService` and `NullDevConsoleService`, along with all code that registers or manages dev console widgets across the backend. Developer messages and notifications are now logged directly to the console. Removes related commands and test mocks, simplifying the developer experience and reducing code complexity.
* Remove MinLogService and related registration code
Deleted MinLogService.js and removed its registration from SelfHostedModule.js. This cleans up unused or deprecated logging functionality.
* Remove `DevSocketService` and related registration
* Increase test job timeouts in CI workflow
* Remove dev environment widget ops counter
Eliminated increment and decrement of the dev_batch-widget ops counter in BatchExecutor. This simplifies the code and removes dev-only UI tracking logic from the batch operation flow.
* Remove console Puter logo feature and related code
Deleted the src/fun/logos.js file and removed all references to the Puter logo display in WebServerService and documentation. This streamlines the server startup output and eliminates unused or unnecessary code.
* Update WebServerService.js
* Standardize console banner formatting in services