Repository navigation
Conversation
vars is now any object or null, and data is any JSON-serializable value. Generated OpenAPI request types and DTOs have optional fields, object-typed values, and nullable strings, none of which satisfy a recursive Json type. TypeScript cannot prove serializability for those shapes, so mutate() validates at runtime: a cycle, a function, a Date, or a value that does not serialize to an object is rejected with a named error. The 4 KB cap stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Native enqueue() is one synchronous disk write, so a promise that never
settles is a native bug. mutate() now races it against a timer, 10 s by
default (configure({ enqueueTimeoutMs })). On timeout it rejects with an
error naming the key and id and logs a warning, and delivery for that id
is not held open. If native did persist the entry, its outcome still
reaches the handlers, and a same-id retry resumes rather than duplicates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ative contract
From the JS interface review. The 4 KB vars cap was a leftover from the
old ctx field; the generated-ops design puts the request body in vars, so
the default is 1 MB and configure({ maxVarsBytes }) sets it. Meta gains
deliveries, native's count of how many times an outcome was delivered, so
an app can decide a poison policy; the library never gives up on its own.
The response parser receives vars, so a decoder that needs an id runs in
the parser path, where a throw is a terminal onError, not a replay.
The codegen spec comments now pin the contract the native slices
implement: enqueue resolves after every staged copy is on disk and rejects
with E_RUNNING, E_FILE_MISSING, or E_STORAGE; same-id rules for a changed
body; the exact set of rows getRequests returns; cancel of an unknown id
is a no-op; ackEvents is void and idempotent; updateHeaders bumps a header
generation; attempt events are live-only. RequestRow gains nextAttemptAt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Findings from the Android and iOS review that land on the JS side:
- vars and data cross the bridge as JSON strings (varsJson, dataJson).
React Native on iOS drops null-valued object keys, so an object form
loses fields such as { status: null }. A "null" body is a real body; a
bodiless request omits data.
- A GET with a body is rejected at mutate(). DELETE with a body stays.
- E_INVALID joins the enqueue rejection codes for input native cannot
send: non-http(s) URL, bad header names or values, GET with a body,
parts that do not tile the moved file.
- deliveries counts deliveries that reached a JS listener; an outcome
journaled with no listener starts at 0 and is not emitted live.
- A different url or method is a different body. attempts count the
current generation. cancel of a settled entry also forgets its
unacknowledged outcomes. updateHeaders also replaces same-named part
headers. A paused entry past expiresAt settles at resume.
- AttemptEvent.outcome narrows to completed | error; pause, cancel, and
supersede emit no attempt event.
- The watchdog text says a timeout means native did not answer, and that
staging a large file body takes time.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e written Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android implements the v10 contract pinned in the codegen spec. The v9 chunked manifest store generalizes into QueueStore: one durable entry per mutate() (id, key, vars, descriptor, staged body, state, attempts, bytes, expiresAt, header generation, deliveries) written tmp+fsync+rename before any attempt. BodyStaging writes JSON and multipart bodies to library files, copies single-file bodies, and moves chunked sources as v9 did. WorkManager runs one worker per entry. WorkerOps applies the retry table from the plan: accept rules with bodyIncludes, transient network/5xx/408/ 429 with jittered backoff and nextAttemptAt, 401/403 parking with a header generation that updateHeaders bumps, per-request exempt lists, expiry at expiresAt with bytes kept. The transfer semaphore (4) and chunked window (3) stay. Pause gates the queue; cancel settles a live entry and forgets a settled one; ack is void and idempotent and forgets only a matching generation. The journal writes every outcome before onSettled emits it; onState carries a full RequestRow, onProgress is throttled 1 s / 10 min, onAttempt is live-only. RequestIndex backs the synchronous getRequests. First launch imports v9 journal entries as legacy rows and cancels v9 work. Logic lives in JVM-testable classes (QueueController, WorkerOps, EnqueueRules, EntryTransitions); the module and workers are thin shells. 217 unit tests, including crash-mid-write cases for the store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reliability: begin() applies an own-generation journal record instead of re-running, so a crash between the journal write and the store update cannot double-send. A failed journal write on settle holds the record in memory and retries; nothing is acked that was not written. cancel() stops the work in a finally block, journals before it saves, and applies an existing record of its generation rather than adding a second outcome. Listening state and the deliveries count are decided under one journal lock, and a live settle goes to the listener that drained, not the newest module instance. Contract: vars and data arrive as JSON text; "null" is a body; GET with a body rejects E_INVALID. Attempt events are completed or error only; pause, cancel, and supersede emit none. A settled entry's cancel forgets its unacked outcomes. attempts reset only on reopen. Under pause only an accepted response settles. A same-id enqueue over a legacy row adopts its v9 manifest. The response body cap applies while streaming. A prune never deletes an eventId a row or a new record names. Rows carry live bytesSent. Platform: a headless run stopped at JobScheduler's 10-minute limit takes the transient path with backoff. Header validation messages carry the name and offset, never the value. README documents the headless limit and the allowBackup requirement. Simplification: one EntryRun attempt path shared by simple and chunked transfers; classifyFailure is the only failure rule; dead code removed. Tests: 289 (was 217), with a TransferHost seam for the run loop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test cleared the listener with stopListening(listener), but from the second iteration on the listener was the previous iteration's owner, so nothing was cleared. When the settle won the race, the journal stamped one live delivery and the drain counted a second one. CI failed about one run in five. Now each iteration clears whatever listener is set and asserts that none is set before the race starts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When the cancel record was journaled but the entry save then failed, cancel() rejected with E_STORAGE. The cancel had already happened: the worker stopped, the cancelled outcome went to JS, and the ack, the next cancel(), or the boot sweep applies the record. A rejection told JS that nothing changed. Now cancel() resolves in that case, as iOS does. It rejects only when the journal write fails, and then nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS implements the same contract over a background URLSession. QueueStore generalizes the v9 chunked manifest into one durable entry per mutate(), written with fsync before any task is created. BodyStaging writes JSON and multipart bodies to files, copies single-file bodies, and moves chunked sources, so every upload comes from a file as the background session requires. QueueCoordinator owns the entries: enqueue with the same-id rules and E_RUNNING/E_FILE_MISSING/E_STORAGE codes, one task per attempt keyed by (id, generation, attempt), TaskMap reconciliation at relaunch and through handleEventsForBackgroundURLSession, the retry table with delayed tasks for backoff, 401/403 parking with header generations, expiry, pause, cancel, and forget-after-ack. The journal writes before onSettled emits; onState, onProgress (throttled), and onAttempt follow the TypeScript payload types. RequestIndex backs the synchronous getRequests. First launch imports v9 journal entries as legacy rows. The pure half of the module is a SwiftPM package (ios/Package.swift) so `cd ios && swift test` runs 138 host-side tests, including crash-mid-write. The podspec excludes the package files and test sources from the pod. The example app builds for the simulator. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reliability: reconcile applies unacked same-generation records to a live row before the task loop, so a failed entry save after the journal write cannot re-issue a settled request. deliveries counts only deliveries that reached a JS listener: journaled at 0 with no listener, not emitted live. A chunked part with a pending replay holds its slot for the grace period. A completion that lands before reconcile still advances the attempt ordinal. A part-file build error settles error/file only when the blob is missing or short; otherwise it refills after backoff. The background completion handler releases as soon as the awaited replay lands, and the grace timer only closes the wait it opened. Contract: vars and data arrive as JSON text; NSNull handling for data is gone; E_INVALID covers non-http(s) URLs, bad header names or values, and GET with a body. url and method are part of the body fingerprint. attempts reset only on reopen. A paused entry past expiresAt settles at resume; the expiry check runs after classification. A same-id mutate on a waiting retry retries now. updateHeaders patches part headers. Rows carry live bytesSent; legacy rows report 0/0. Attempt events are completed or error. Simplification: dead fields removed (TaskMap.Meta.accept, isChunkedPart, fileUnreadable, lifetimeMs, descriptorJSON, allDormantManifests). Tests: 170 (was 138), including relaunch and pending-replay cases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Owner decision, matching Android: a cancel whose journal write fails rejects with E_STORAGE and changes nothing; the caller may call again. The hold-and-retry path stays for settle outcomes only. A cancel of a settled entry now forgets atomically: the id directory is set aside by one rename, the journal files are deleted, and a failure puts the row back and rejects. A crash between the two steps is finished or undone at launch. 178 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A background URLSession raises an Objective-C exception, not an error, when uploadTask(with:fromFile:) cannot read the file. Swift cannot catch it, so the app ends. v9.0.1 fixed this for the v9 code (Sentry DIANA-19VW). This applies the same guard to the v10 transport. The transport now throws when the session cannot open the file. Each caller decides what that means: - A simple entry whose staged body is gone settles error 'file', as the missing-body check before it already does. - A simple entry whose body still exists is unreadable for now (data protection while the device is locked). It goes back to queued and is issued again after a backoff. - A chunked part follows the failed part-file rule: a short blob settles error 'file'; otherwise the window refills after a backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example app is the device test harness. It is rewritten on the v10
API: definitions at module scope (JSON POST, multipart, chunked with
chunkPlan, bodiless DELETE, no-vars GET), configure() with a headers
provider, a queue list from getRequests() and the state and progress
feeds, an attempt log, and controls for every step of the two device
scripts: cancel, pause, resume, wifiOnly, updateHeaders with a typed
value, same-id resume and replace, a short expiresAt, and a section of
outcomes journaled before this launch for the kill-and-relaunch check.
The local Express server answers by path segment (401, 404, 409, 503,
slow, oversize) and writes chunked parts at their offsets. The stale
Podfile.lock is regenerated for RN 0.84.1, the AppDelegate wires the
background completion handler, and the Android manifest declares and
requests POST_NOTIFICATIONS. The example README carries the device
script in UI terms.
Library: createUploadClient({ native }) accepts a fake TurboModule, and
react-native-background-upload/src/testing exports createFakeNative(),
an in-memory Spec with settle(), seeded rows, recorded calls, and ack
tracking, so a consumer tests definitions and handlers against the real
registry and delivery. CI gains a macOS job that runs swift test.
Docs: CHANGELOG 10.0.0 covers all slices; README gets an "Upgrading from
v9" section, iOS platform notes, a "Testing your definitions" section,
and a corrected AppDelegate snippet (@import for .m, header search path
for .mm). Version 10.0.0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both changes come from the Diana readiness audit. Diana's Wi-Fi setting
applies to captures only, and Diana pauses captures only while field
note traffic keeps flowing. A queue-wide gate cannot express either.
RequestDescriptor.wifiOnly overrides the queue setting for that entry;
an entry that does not set it follows setWifiOnly, so toggling the
setting still moves queued captures. Both natives evaluate the
constraint per attempt and persist the per-entry value.
pause(scope) and resume(scope) take { keys?: string[] }. No keys means
the global gate, as before. Keys add to or remove from a persisted set.
An entry is paused when the gate is on or its key is in the set; each
entry that moves emits one state event; a scoped resume does not free
an entry the other gate still holds; an entry enqueued into a paused
scope starts paused. Pause produces no outcome and no attempt event.
The fake native in src/testing models both. Tests: JS 189, Android 307,
iOS 201.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The promotion PR says the script measures the time from mutate() to the first send, which decides the deferred in-process fast path. The step now exists, on both platforms, using the log clocks the harness prints. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
## Description
A chunked upload keeps 3 part tasks in the iOS background session. It
creates the next part task when one finishes, and that usually happens
while the app is in the background. Apple's docs say the system starts a
task created in the background at its own discretion and ignores
`isDiscretionary = false`. So after the first 3 parts, a large file can
wait for the system, for example until the device charges.
This adds `ios: { queueAllParts: true }` to the request descriptor. With
it, the library creates every part task at `mutate()`. A `mutate()` in
the foreground then leaves iOS nothing to defer. Without it, nothing
changes.
Before: a 40-part capture had 3 part tasks in the session, and parts 4
to 40 were created later in the background.
After, with `queueAllParts`: all 40 part tasks are in the session after
`mutate()`.
## What to look at
- `ChunkedCoordinator.refill` uses every part as the window when the
entry sets the flag.
- `QueueEntry.queueAllParts` is optional, so entries saved by 10.0.0
decode with the window of 3.
## Decisions and trade-offs
- **Each part task has its own part file.** With every part queued, the
upload needs the file's size again in free space. The README and the
type doc say so.
- **A failed part is retried as a new task.** A retry created in the
background is still at the system's discretion. The README says so.
- **iOS only, under `ios`.** Android runs parts in a worker, not a
background session. Android ignores the field.
- **The flag without `parts` is rejected in JS.** It would do nothing,
and the library rejects fields that would be dropped in silence.
## Test Plan
- `swift test` in `ios/`: 210 tests pass. New tests cover every part at
enqueue, `false` keeping 3, a relaunch that recreates lost parts past 3,
a parser reject, and decoding an entry saved without the field.
- `yarn jest`: 191 tests pass, with new tests for forwarding and
validation. `tsc` is clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QFiCVD9pHXvcx712ubCLpR
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KDL4Nr8RSovs72UACtNu82
## Description iOS starts a task created while the app is in the background at its own discretion and ignores `isDiscretionary = false` for it ([Apple docs](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/isdiscretionary)). 10.1.0's `ios.queueAllParts` only fixed this for parts after the first 3, and only for new uploads. Retries, a `mutate()` from a background wake, and tasks a relaunch rebuilds are still created in the background, and uploads already queued stay deferred. This does two things: - **Every part task at `mutate()`.** The window of 3 and `ios.queueAllParts` are gone. There is no opt-out. - **Replace background tasks on activation.** Each task records in the TaskMap whether the app was active when it was created. When the app becomes active, every task created while it was not, that has not sent a body byte, is cancelled as superseded and created again. A simple request keeps its attempt ordinal and request id. Both kinds keep what is left of a backoff. Tasks from earlier versions have no tag, so they count as background tasks. The first activation after the upgrade replaces them, which unsticks uploads already waiting on the system without any app change. ## What to look at - `QueueCoordinator.replaceBackgroundTasks`, `ChunkedCoordinator.replace`, and `issue(replacing:)`. - "Active" means `didBecomeActive` to `willResignActive`, set without a hop to the coordinator queue. A task created while the app is inactive but visible counts as background, so the worst case is one extra replacement. - If a replaced task lands before its cancel takes effect, the existing paths handle it: a part is accepted and its replacement stopped, and a simple request settles and its replacement is cancelled. There are tests for both. ## Decisions and trade-offs - **Breaking:** `mutate()` with `ios` now rejects as an unknown field. Diana's PR that sets `ios.queueAllParts` needs to drop it. - **Disk:** every part file exists at once, so a chunked upload needs the file's size again in free space until the parts are sent. - **Only unstarted tasks are replaced.** A task with `countOfBytesSent > 0` is never cut off. A simple request with an empty body is left alone, because 0 bytes sent can't tell whether it went. - **Still a gap:** while the app stays in the background, a retry waits for the system until the next activation. iOS doesn't let us close that. ## Test Plan - `yarn test`: 190 pass. `tsc` is clean. - Swift host tests run in CI (no Swift toolchain where this was written). New `CoordinatorForegroundTests` cover replacement of simple and part tasks, tasks kept when created active or already sending, resign-active tagging, backoff carry-over, a replaced task that lands anyway, paused entries, and a relaunch that replaces untagged tasks from an older build. Chunked tests were updated for a task per part. - Not yet run on a device. --- _Generated by [Claude Code](https://claude.ai/code/session_01FuzycfW82c1DKeSSwzuc9w)_ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KDL4Nr8RSovs72UACtNu82
Vydia#54 was squash-merged before its last two commits were pushed, so they never reached master. This PR carries them, rebased onto 11.0.0. **Review fixes for replacing background tasks.** Replacement now runs 2 s after the app becomes active and skips tasks that have a response. A superseded simple task whose body landed completes the entry, even from an older attempt. Reconcile never adopts a superseded task, and progress only counts from the task that owns it. **Batch the entry and TaskMap writes for part tasks.** With every part queued at mutate(), each part task saved the whole entry (which holds every part's URL) and each cancel rewrote the whole TaskMap. A 200-part upload did hundreds of full-file writes on the coordinator queue, which delegate callbacks wait on. Now `enqueueParts` builds the part files and saves the entry once before any task exists, `cancelTasks(keys:purpose:)` writes every purpose in one TaskMap write, and part replacements are grouped per entry. Each task still writes its TaskMap entry before it resumes. A commit moves the CHANGELOG notes from the released 11.0.0 section to Unreleased. **Fixes from review of this PR.** A replaced simple task whose body landed while the entry was parked for auth now completes the entry. Only the latest scheduled replace runs, so a quick background-and-return still waits the full 2 s. A part left without a task when `enqueueParts` stops mid-batch keeps its remaining backoff on refill. Each has a test that fails without the fix. `swift test` (233) and `yarn test` (190) pass locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018kyo6onjB1N53r2W6XJHWd --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kyo6onjB1N53r2W6XJHWd
Device testing of Sitemate Mobile on an iOS simulator killed the app while mutate() was writing an 8.4 MB body for a new id. The half-written body-*.tmp was still on disk two relaunches later. BodyStaging removes its tmp on every failure path, but a process death between creating the tmp and saving entry.json leaves it, and nothing reclaimed a directory with no row: QueueStore.sweep(entry) only ran after a successful enqueue of that id. reconcileAll now sweeps once every grace wait has ended, right after it releases the background completion handlers: - Every non-legacy entry in the index gets QueueStore.sweep(entry), the same sweep enqueue runs after a save. It keeps the entry's bodyPath and its current-incarnation part files, and removes other bodies, .tmp files, and stale-incarnation part files. That covers a same-id replace that died before its save or before its sweep. This generalises the ChunkedCoordinator.reconcile precedent (store helpers decide orphans from what reconcile knows is live) instead of adding a parallel scheme. - New QueueStore.sweepEntryless(): a directory with no entry.json at all loses body-*, part-* and *.tmp files, then the directory itself if rmdir finds it empty. Safety: - Live and recoverable entries keep their files: a claimed directory is one with an entry.json, and an entry's referenced body is never removed. The sweep waits for reconcile's grace waits, so an entry awaiting a daemon replay is swept only after that replay has settled it or it has been re-issued, and its body survives either way. - In-flight writes: staging, saves and part-file writes all run on the coordinator's serial queue, as does the sweep, so no write of this process is ever half done when it runs. - Left alone deliberately: a blob-* or v9 blob in an entry-less directory (the caller's file was moved, not copied, into it, and adoptableBlob lets a same-id retry resume from it), any directory holding a v9 manifest.json (a dormant v9 upload), a directory whose entry.json exists but is unreadable, and legacy rows. - A failed removal is logged with NSLog and skipped. The sweep runs after issuing, so it can never fail or delay an upload. sweep(entry) now logs instead of using try?. Tests: an orphan body with no owning entry is removed, and a read-only orphan directory is skipped without blocking the upload. A live entry keeps its body while its stray bodies go. An entry in its grace window keeps its body, nothing is swept until the grace ends, and a body staged by this process during the wait survives. A store test covers what sweepEntryless keeps. The three coordinator tests fail with the sweep call removed. Adopting v11.0.1 (this branch is from openspacelabs-main at v11.0.1; 4 commits after v10.1.0: d3f38f3, 7a4b186, 3741d9d, 45495c8): - Breaking API: `ios` is gone from RequestDescriptor. `ios.queueAllParts` is removed, not renamed. mutate() with any `ios` key rejects with 'unknown descriptor field "ios"' in JS validation and never reaches native. Nothing replaces it: every chunked part task is now always created at mutate(). No other option, export or event changed, and Android is untouched. - Behaviour, iOS only: * Every part of a chunked upload is queued at once, so the upload needs the file's size again in free space until its parts are sent. Paced by httpMaximumConnectionsPerHost = 4 per session. * The TaskMap tags each task with whether the app was active when it was created. 2 s after didBecomeActive, every background-created task that has sent no body byte and has no response is cancelled as superseded and created again. A simple request keeps its attempt ordinal and request id, and both kinds keep their remaining backoff. Tasks from older builds have no tag, so the first activation after the upgrade replaces them, which unsticks uploads iOS had deferred. * Reconcile never adopts a superseded task. A superseded simple task whose body landed completes its entry instead of being re-sent. * Part-task entry and TaskMap writes are batched per mutate()/replace. * Known gap: while the app stays in the background, a retry still waits for the system until the next activation. The library wires didBecomeActive and willResignActive itself. No AppDelegate or Podfile change is needed beyond the usual pod install for changed native code. - Dashpivot Mobile (branch fm/dpm-upload-poc, POC at 7b6e4d73e): must drop `ios: { queueAllParts: true }` at app/sync/uploads/asset-upload-queue.ts:52, or every large-asset mutate() rejects. It also pins openspacelabs/react-native-background-upload directly, not the site-mate fork, so the bump should point it at this fork. Large chunked assets need the extra free space noted above. - Sitemate Mobile (SMM-3293, origin/fix-26.16/SMM-3293-durable-upload-outcomes, pinned to the fork at e364399): uses no `ios` field and no `parts`, so only the pin changes. It gains the foreground replacement automatically. - Both apps need a native rebuild (pod install) for the bump. Neither app is changed here.
Keeps the v11.0.2 tree as-is. The previous main (Vydia 7.x) history is retained through this merge and on vydia-7x-legacy; both apps pin by SHA so neither is affected.
Author
|
Superseded. The v11 line now lives in a proper fork of openspacelabs at site-mate/react-native-background-upload-osl, where upstream is a remote and their releases merge normally. Importing their history into this repo left two unrelated lineages sharing only an ancient ancestor, which is what made this PR need a history rewrite in the first place. This repo keeps serving the SHAs both apps pin today (SMM c26be72, DPM b8ce380); nothing in production moves. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the fork up to date with openspacelabs upstream and puts our patch on
main.Contents
v11.0.2Why
mainhas been sitting on the 2024 Vydia line while all current work happened on the v11 line. This makesmainthe branch to use.Safety
The previous
mainis preserved onvydia-7x-legacy. Both apps pin by commit SHA, not branch, so neither Sitemate Mobile (c26be72) nor Dashpivot (b8ce380) is affected; those commits stay reachable onvydia-7x-legacy.🤖 Generated with Claude Code