synheart_core 0.24.0
synheart_core: ^0.24.0 copied to clipboard
Flutter SDK for the Human State Interface (HSI) 1.3 — unified collection of wearable, behavior, and phone signals with consent-gated cloud upload.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased #
0.24.0 - 2026-11-12 #
Added #
WearSample.hrvSdnn: SDNN from the wear source, in ms.
Changed #
synheart_wearminimum raised to 0.7.0 (>=0.7.0 <0.8.0). Earlier versions copy Apple Health's SDNN into the RMSSD field, sohrvRmssdwould still carry SDNN. 0.7.0 also encrypts its session cache with AES-256-GCM and no longer returns empty reads right afterinitialize()on Android.
Fixed #
WearSample.hrvRmssdno longer carries SDNN when the source has no RMSSD (Apple Health). ReadhrvSdnnfor that value.- Deprecation messages no longer name 0.12.0 as the removal version.
0.23.0 - 2026-11-04 #
Changed #
synheart_wearminimum raised to 0.6.2 (>=0.6.2 <0.7.0). Since 0.6.1 it asks Apple Health and Health Connect for read access only; 0.6.0 asked for read and write, so an app had to declareWRITE_*permissions andNSHealthUpdateUsageDescriptionit never used. Apps that do not write health data themselves can now drop them. The example no longer declares them. 0.6.2 reads overnight HRV and HR spread from the samples taken asleep, which the Health Connect backfill pushes into the overnight baselines; with 0.6.1 they were whole-day figures.
Fixed #
- HSI reaches apps without biosignals consent. Every completed window was
dropped before
onHSIUpdate,onStateUpdateandgetSessionHsiWindows()unless biosignals (or research) consent was granted, so an app collecting only behavior or phone context never saw its digital axes. A window is now delivered when any collection category (biosignals, behavior, phone context) or research is granted. With nothing granted, windows are still dropped. - Resting HR baseline gets the resting-HR record. A night's
cardiac.resting_hr_bpmwas the mean HR asleep even when HealthKit, Health Connect or a vendor reported a resting HR. The two now stay apart:cardiac.overnight_hr_bpmis the mean HR asleep (the Sleep Score's HR term),cardiac.resting_hr_bpmthe resting-HR record (Recovery and Readiness). A source with one overnight figure, such as WHOOP's resting HR, still fills both. A daytime resting HR on its own, such as Garmin's, fills resting only. - Steps are pushed only for days that are over. A night's push carried
the wake day's step count while that day was still going, so a partial
total was stored as the day's figure.
HealthConnectRuntimeSinknow also pushes steps for finished days that have no night. - Daily values are no longer dropped when no session is running.
srmPushWearableDailyandsrmRefreshWearableReferencebuild the pipeline first. Health-store pulls and vendor syncs usually run with no session open, and the runtime refused every value with-1: on iOS a whole pull of history reached the baselines as nothing. - Garmin
hrv.updatedevents carry last night's average HRV asrmssd_ms. They carried the 7-day average when Garmin sent one; that is nowweekly_avg_rmssd_ms.
0.22.1 - 2026-10-30 #
Changed #
- Install hints now recommend runtime 0.46.0. It reports each output no
finer than the granularity your app's HSM Schema declares: a
bandoutput carries the middle of its band (0.17,0.5or0.83) with the band named inmeta.synheart.bands, and adirectiveoutput is left out of the frame and listed instate_withheld. It keeps stored windows, session summaries and history only as long as the schema'sretentionallows, and when you publish a new schema version it brings what is already stored on the device in line with it. No binding changes; runtimes from 0.45.0 up still initialise.
0.22.0 - 2026-10-30 #
Requires runtime 0.45.0 or newer (C ABI 4.2); older runtimes are refused at
load. Install it with synheart install runtime --version 0.45.0. The daily
score inputs changed shape with ABI 4.2 and neither shape parses on the other
runtime, so there is no fallback.
Migrating #
ReadinessScoreInput.baselineDaysis nowbaselineMaturity(0..1): the lower of the two confidences inSynheart.dailyBaselineRefs()(BaselineRefs.maturity), or0when it returns null.SleepScoreInputgainstonightMidpointHandbaselines. Pass the night's local sleep midpoint (null when the source gives no start and end time) andSynheart.dailySleepBaselines(); without the baselines the score stays in its cold-start mode. Only the two newestpriorsNewestFirstnights are read now, for sleep debt.RecoveryScoreInput.baselines: take them fromSynheart.dailyBaselineRefs()rather than building them fromwearableReference.
Added #
SynheartConfig.modelBackend: choose the engine's model backend.ModelBackend.kinematicOnnxlets the learned kinematic model report activity, postural and locomotion state while every other head keeps its deterministic formula;ModelBackend.rulepackOnlyis the engine default. Null sends nothing and leaves the runtime's default. Needs a runtime with themodel_backendconfig key and ONNX support.Synheart.dailyBaselineRefs()andSynheart.dailySleepBaselines(): the daily scores' personal baselines, read aftersrmRefreshWearableReference().SleepBaselines,DailyRef,BaselineRefs.fromJsonandBaselineRefs.maturity.DailyMetric.overnightHrBpm(cardiac.overnight_hr_bpm, the Sleep Score's heart-rate baseline),DailyMetric.activeMin(motion.active_min, pushed once the day is over) andDailyMetric.restRmssdMs(cardiac.rest_rmssd_ms, written by the engine; do not push it).
Changed #
Baselinesscores each night against the runtime's baselines: Sleep readsdailySleepBaselines()with the night's midpoint, Recovery and Readiness readdailyBaselineRefs().- The nightly push adds
sleep.midpoint_h(only for a night with a start and end time) andcardiac.overnight_hr_bpm. The one overnight heart-rate figure is pushed as bothcardiac.overnight_hr_bpmandcardiac.resting_hr_bpm, so Recovery and Readiness keep a resting-HR baseline. HealthConnectRuntimeSinkpushes each night's asleep mean HR ascardiac.overnight_hr_bpmand itssleep.midpoint_hfrom the recorded asleep span, andcardiac.resting_hr_bpmfrom Health Connect's resting-HR record, the asleep mean standing in without one. It pushed the day's HR median as resting HR: a daytime figure. It also pushesmotion.active_min(Health Connect exercise sessions) for each day that is over. Needs synheart_wear withovernightHrBpmandfetchActiveMinutes.HealthConnectBackfillResult.dimensionsPushedanddaysIngestedcount only values the runtime accepted. They counted every attempt, so an import with no session running reported values it never stored.AppleHealthDailyAggregatorgroups asleep stage blocks into the main night and pushes itssleep.midpoint_handcardiac.overnight_hr_bpm(the mean HR inside the asleep blocks). Apple's resting HR still goes only tocardiac.resting_hr_bpm.
0.21.0 - 2026-10-27 #
Recommends runtime 0.43.0 (C ABI 4.1): synheart install runtime --version 0.43.0.
Runtime 0.42.x (C ABI 4.0) still loads; on it schemaNeedsConsent() is false and
hsmSchemaConsent() null.
Added #
Synheart.schemaNeedsConsent(): ask again when the app's HSM Schema gains an input. A newer version of the app's published HSM Schema can declare an input the person was never asked about. The runtime keeps it off until they consent, and now says so:schemaNeedsConsent()is true while the schema declares channels the person has not consented to and was not asked about when they last chose, andhsmSchemaConsent()returns them as anHsmSchemaConsent(schemaVersion,channels). Show the consent form again when it is true. A channel the person declined is not asked about again. Needs a runtime with C ABI 4.1; with an older one, or none,schemaNeedsConsent()is false andhsmSchemaConsent()null.
0.20.0 - 2026-10-27 #
Requires runtime 0.42.0 or newer (C ABI 4.0); older runtimes are refused at
load, and runtime 0.42 is refused by every earlier SDK. Install it with
synheart install runtime --version 0.42.1. Expect one cold start on the
first launch after upgrading: 0.42 cannot read a baseline snapshot from an
earlier runtime, and refuses an earlier session-state snapshot too (the
engine logs code 7003 and starts the stateful heads cold). Neither is an
error to handle.
Migrating #
Synheart.rollDay()is removed. The engine closes each local day itself at the first input of the next. Attach Strain before local midnight withattachStrainScore(), or after it with the newattachClosedDayStrain().- Baseline snapshots.
exportRuntimeSRMSnapshot/loadRuntimeSRMSnapshotandlongitudinalSnapshotJson/loadLongitudinalSnapshotare replaced by one opaque pair,exportBaselineSnapshot()/loadBaselineSnapshot(). The runtime now seals the baselines to its own data directory and restores them at startup, so a host that keeps that directory can drop its snapshot code. srmPushWearableDailytakesprovider, aDailyMetricname and the localdatethe value belongs to, and returns the runtime's code (0,1004,5002,-1). The old dimension names (hrv_rmssd,resting_hr,sleep_need, …) are refused; eachDailyMetrickeeps the old unit.confidenceandfidelityare gone (vendorSummaryreplacesfidelity).srmTriggerWearableRecomputeis nowsrmRefreshWearableReference({asOf}): it rebuilds the reference from the dates beforeasOf(default today).- Backfill callbacks.
PushDailyCallbacktakesprovider,metric,date,valueandvendorSummary, matchingsrmPushWearableDaily.
Added #
DailyMetric: the twelve daily metric names the runtime accepts.Synheart.setInputCapture()andsetCallActive(), level reports that hold across sessions;pushInputsJson()for the runtime's input envelope;srmSetDayType();forgetBaselines();drainBaselineEventsJson(),baselineProfileJson(),baselineRhythmJson(),baselineWindowDigestJson()andbaselineRequirementsJson();registerBaselineDirection()andbeginBaselineRestBlock();attachClosedDayStrain(). All are optional lookups.BehaviorConfig.reportInputCapture(default on). The SDK reports its in-app capture running while it tracks behaviour and the app is in front, so a still window can be certified rest and teach the resting heart rate and HRV. Turn it off when the host captures input elsewhere and reports it itself.
Changed #
- Every sample push carries its provider and the device's UTC offset at the
sample's own time, so samples on either side of a daylight-saving change
land on the right local day.
pushHrand the accelerometer pushes accept aprovider. device_classdefaults to"auto"when the host declares none: an undeclared class now files behaviour baselines underunknown. Declaring it no longer resets other baselines; one snapshot holds every class.input_observable: falseis declared for a host without a behaviour module, whose still windows are input-free.Baselineswrites its nightly values under the wake date and refreshes the reference once per night.
Fixed #
- The Apple Health backfill filed daily values under the UTC date; it now uses the local date, so a night ending after local midnight east of UTC is no longer filed a day early.
- Registration that fails with
STORAGE_UNAVAILABLE(a Keychain / Keystore locked at boot) reinstalls the crypto callbacks and re-reads the identity before trying again, and registers only if the device is still unregistered, so a seconddevice_idis never minted over the first.
0.19.0 - 2026-10-26 #
Breaking for two APIs. Synheart.getSyncStatus() is removed: use
Synheart.syncStatus(). ReadinessScoreInput now takes recoveryScore and
sleepScore (plus optional alignment, cold-start flags and baselineDays)
in place of the workload, fatigue and history contexts, which are removed.
Both are detailed below. Runtimes from 0.20.0 to 0.41.x are supported; 0.41.0
is recommended.
Added #
Synheart.attachRecoveryScore()andSynheart.attachReadinessScore(): daily scores reach the cloud. Each computes the score, ascomputeRecoveryScore/computeReadinessScoredo, and queues it to ride the next emitted HSI window as therecovery/readinessreading, which is how a daily score is uploaded. Until now nothing queued them: the compute calls are stateless, andattachRecoveryScoreTodayonly feeds personalization. CheckattachRecoveryScoreAvailable/attachReadinessScoreAvailable; a runtime built without the physiological feature set returns null.Synheart.syncStatus(): typed sync status. Returns aSyncStatuswith the runtime 0.39.0 fields (syncSpaceId,pendingCount,lastSuccessMs,srkReady,accountOwned). Fields an older runtime never sent staynull.device_countis gone from the runtime; read the roster withsyncListDevices().Synheart.syncPairingStatus(token): pairing status. The device showing a pairing code can now tell when the other device joined. Returns aSyncPairingStatus(waiting,joined,expiredorunknown); poll it every 2–3 s until the state is terminal orexpiresAtpasses. Returnsnullon a runtime withoutsynheart_core_sync_pairing_status(before 0.31.6); checkSynheart.syncPairingStatusAvailable.
Changed #
- README sync example now compiles (
checkSyncReadinesstakes a requiredoperation), and the sync API list is complete. Sync carries baseline snapshots only; another device's HSI windows and session summaries do not arrive.
Removed #
Synheart.getSyncStatus(), deprecated since 0.11 and promised for removal in 0.12.0. UseSynheart.syncStatus().SyncStatusis now the richer type above;SyncStatus(enabled: ...)still constructs.
Fixed #
- Readiness computes again (breaking model change). The SDK's readiness
models no longer matched the engine: since engine 0.12.0 (runtime 0.20.0,
this SDK's minimum) readiness takes Recovery, Sleep and overnight HR/HRV
against baseline, and the old input (recovery, acute workload, fatigue,
history) failed every compute with
missing field sleep_score. The models now mirror the engine:ReadinessScoreInputtakesrecoveryScore,sleepScore, an optionalOvernightAlignment,recoveryColdStart,sleepColdStartandbaselineDays.ReadinessScoreResultaddssleepAnchor;ReadinessComponentsisrecovery,sleepand the three alignment values;ReadinessFactoris the engine's list.ReadinessBandkeeps its names, which are each band's recommendation;wireis now the engine's (low,reduced,normal,high), and an unknown band follows the score instead of defaulting torest.- Removed
AcuteWorkloadContext,FatigueContext,HistoryContextandReadinessScoreInput.fromRecovery. Baselinesbuilds the input from the night's Sleep Score, its Recovery Score and overnight HR/HRV against the wearable reference. A night with no Sleep Score gets no readiness.attachVendorWorkoutstill records strain but no longer recomputes readiness, which does not use workload.
Baselinesuploads Recovery and Readiness. The scores it settles after each sleep ingest are now queued for upload, not only computed.- A relaunch no longer re-sends or double-counts nights already sent.
Re-pulling the last month of nights attached each one again: the newest
night was uploaded a second time and every night was pushed onto the
engine's 7-night sleep ring again, skewing its median.
Baselinesnow keeps a ledger of the last night queued per score kind, and whether an emitted frame carried it, inexportRecoveryStateJson(). A night already sent with the same score rebuilds local history but is not attached again; a score that no frame carried before the app stopped is queued once more; an older night never replaces a newer one in the runtime's single pending slot. Hosts that already persistexportRecoveryStateJson()need no change.
0.18.1 - 2026-10-23 #
Changed #
- Install hints now recommend runtime 0.41.0. It computes only the outputs the app declared, instead of computing every output and leaving the undeclared ones out of what it reports. Runtimes from 0.20.0 up still initialise.
0.18.0 - 2026-10-22 #
Deprecated #
CoreRuntimeBridge.wellnessJson(). Current runtimes no longer compute a Wellness score, so it returns null. Usestressfrom the HSI stream for a live view relative to the person's baseline, andrecoveryorreadinessfor the daily one.
Added #
-
Collectors follow the runtime's per-channel decision live. The wear, phone and behavior modules now ask the native runtime (
synheart_core_allows_channel) before collecting a channel, whateverstrictChannelConsentis set to. The runtime refuses a channel the user did not consent to, or one the app did not declare as a data input, so the SDK no longer collects data that would be dropped on ingest. A phone collector whose channel is refused is not started at all, so it raises no permission prompt. Without a runtime, or with one older than 0.39.0, nothing changes. -
SynheartConfig.strictChannelConsent: consent enforced per channel. Off by default. With it on, the SDK passesstrict_channel_consentto the native runtime, which then accepts a sample only when its own consent channel is granted (phone_context.device_motionfor phone motion,behavior.app_contextfor app-context events, and so on) and, when cloud consent is configured, present in the verified consent token. The SDK's modules read the same per-channel decision from the runtime (synheart_core_allows_channel) before starting a sensor, so a channel the user turned off stays off even when its category is on, and phone motion is checked againstphone_context.device_motion. Needs a runtime with C ABI 2.1; on an older runtime the modules keep their category checks.
Changed #
- Install hints now recommend runtime 0.40.0. It keeps collection and the
state it reports to the data inputs and outputs the app declared, and no
longer computes a Wellness score. Per-channel decisions
(
synheart_core_allows_channel, C ABI 2.1) need runtime 0.39.0 or later. Runtimes from 0.20.0 up still initialise; older than 0.39.0 they log as older than the bindings expect, andstrictChannelConsenthas no effect on them.
0.17.2 - 2026-10-15 #
Fixed #
- iOS: the runtime's data directories are excluded from iCloud / iTunes
backup and device migration. Everything in them is sealed with keys held
in Keychain items that never leave the device, so a restored copy could
never be decrypted. It also brought back the runtime's
.smk-provisioned-<subject>markers without their key, and the runtime then refused to mint a new storage master key on the new device (ERR_SECURE_STORAGE_UNAVAILABLE). The defaultApplication Support/synheart-coredirectory and everySynheartInstancedata directory are now markedisExcludedFromBackup, through a newai.synheart.core/platformmethod channel on the iOS plugin. Android needs no change here: the app excludes its data withallowBackup="false"anddataExtractionRules.
0.17.1 - 2026-10-15 #
Changed #
- Requires
synheart_auth^0.1.12 (was ^0.1.7). 0.1.12 recovers an Android secure store restored from a backup or device transfer: the restored store was sealed by a Keystore key the new install doesn't have, so every secure read failed (AEADBadTagException) and device registration and storage master key setup kept failing until the app's data was cleared. It also makessignRequestsign with the device key the runtime registered. Apps on 0.17.0 could otherwise resolve an oldersynheart_authwithout either fix.
0.17.0 - 2026-10-14 #
Changed #
-
Research mode forwards raw phone motion by default.
SynheartMode.researchandresearchBaselinenow turn onBehaviorConfig.emitRawMotionSamples, so the runtime receives measured accelerometer samples without the host opting in. An explicittruestill enables it in any mode (SynheartConfig.resolvedEmitRawMotionSamples). -
PhoneModule's collectors no longer emit fabricated data. Motion, screen state, app switches and notifications came fromRandom()timers; with no real platform source they now emit nothing. -
Requires
synheart_behavior0.4.1. Its Android collector samples the accelerometer at 50 Hz; 0.4.0 usedSENSOR_DELAY_NORMAL(~5 Hz), a tenth of the rate the motion features are designed for. 0.4.1 also ships an Android manifest that merges a notification listener service and theBIND_NOTIFICATION_LISTENER_SERVICEandREAD_PHONE_STATEpermissions into every host; see thesynheart_behavior0.4.1 changelog to remove them. -
Install hints now recommend runtime 0.38.0. The SDK was verified end to end against it on Android and iOS, it records its ABI version (2.0), so
synheart doctorcan confirm compatibility after installing it, and it is the first release withsynheart_core_last_error_json. Runtimes from 0.20.0 up to 0.38.0 still initialise; they log as older than the bindings expect. -
BREAKING (behavior):
Synheart.initializenow throwsSynheartRuntimeException(kind: incompatible) for an incompatible runtime. An ABI outside 1.0–2.x, or a runtime below 0.20.0 that reports no ABI, used to be logged as "core runtime bridge unavailable" while init carried on without HSI. Migration: run the command inexception.installCommand(synheart install runtime --version <ver>), or catch the exception where you callinitialize. A missing, wrong-architecture or unloadable runtime still does not throw. -
The "native runtime not loaded" warning no longer tells you to run
flutter clean; it prints the specific reason and the install command.
Added #
- Every runtime failure explains itself.
Synheart.lastNativeErrorreturns aSynheartNativeErrorfor the most recent failed native call:code,message,retryable, plusownerKind(NativeErrorOwner),recoveryKind(NativeErrorRecovery),recoveryCall,reason,argument,hint,op,errorIdandreport. It is read in the same isolate as the failing call, including calls the bridge runs on a background isolate. Requires a runtime with ABI 2.0 (synheart_core_last_error_json); on older runtimes it stays null. SynheartNativeException, the base ofSyncNativeException, so onecatchhandles every structured runtime failure.SyncNativeErroris now a typedef ofSynheartNativeError; existing code compiles unchanged.CoreRuntimeBridge.lastCreateErrorsays why the runtime handle could not be created (for exampleCONFIGURATION_INVALID).startSession()'s "native session failed to start" error now names the runtime's code, hint, recovery and error id instead of guessing the cause.- Runtime ABI 2.x is accepted alongside 1.x (
RuntimeCompat.maxAbiMajor): ABI 2.0 removed only symbols these bindings never call.
Removed #
- The unused binding for
synheart_core_set_log_callback, which runtime ABI 2.0 no longer exports.
Added (runtime compatibility) #
SynheartRuntimeExceptionandSynheartRuntimeErrorKind(notInstalled,wrongArchitecture,incompatible,loadFailed), withmessage,runtimeVersion,runtimeAbi,requiredAbi,recommendedVersion,installCommandandcause.Synheart.runtimeErrorand aruntimeErrorkey inSynheart.runtimeDiagnostics()give the reason the runtime is unavailable.- ABI-based runtime compatibility. Runtimes 0.33.0 and later report an ABI
in
build_info; the SDK requiresRuntimeCompat.requiredAbi(1.0) and addsRuntimeCompatStatus.incompatibleAbiandRuntimeCompatResult.abi. Earlier runtimes fall back to the version check. synheart_runtime.jsonat the package root states the required ABI, minimum and tested runtime versions for the synheart CLI. It also carriesabi_max_major(2), the highest ABI major the bindings accept, so a reader of the manifest does not reject a valid ABI 2.x runtime.CoreRuntimeBridge.lastError(behindSynheart.lastNativeError) describes the most recent call: a call that succeeds clears it, and a failure the runtime did not describe leaves it null rather than keeping an older error.
0.16.1 - 2026-10-07 #
Added #
SynheartInstance.createAsync/CoreRuntimeBridge.createAsync- create a runtime without blocking the host's UI. Native runtime creation (store open and migrations, cloud connector, identity restore) is a blocking call of 0.5-1.5 s on a mid-range Android phone, and since Flutter 3.29 the Dart isolate shares the platform main thread, socreatefroze rendering for that long.createAsyncruns only that call on a background isolate and wires the instance (logging, storage and crypto callbacks) on the calling isolate exactly ascreatedoes, falling back to an on-thread create if an isolate cannot run. Additive:createis unchanged.- Not for time-critical creation: on some devices background isolates queue
behind other isolate work rather than run in parallel, so wall-clock time can
exceed the synchronous call. The personal runtime created by
Synheart.initializetherefore stays synchronous.
Changed #
synheart_wearminimum raised to 0.5.1 (>=0.5.1 <0.6.0). On iOS the wear source's 1 s heart-rate stream ran every tick as a HealthKit query per metric type over the last 30 days and fed the 30-day mean heart rate in as the current one; 0.5.1 bounds real-time reads to the last 2 minutes, at most every 10 s. The HR samples this module receives on iOS are now recent ones.
0.16.0 - 2026-10-01 #
Added #
Synheart.ingestStatus()— why a session has not been delivered. Surfaces the runtime'ssynheart_core_ingest_status: queue depth, consent readiness, rows evicted at the cap, rows refused for want of credentials, the timestamp of the last successful upload, and the background loop's stall reason. Together these separate "still uploading" from "stalled" from "collected and then dropped", which a host previously could not tell apart at all. Looked up optionally, so an older vendored runtime returns null rather than failing.
Added #
labEnsureMetadataAsync, so a host can keep the metadata upload off its UI isolate.labEnsureMetadataperforms the round-trip on the calling isolate. It short-circuits when the payload hash is unchanged, so nearly every call is free and the blocking one is rare — which is exactly what makes it hard to place correctly: a caller cannot tell beforehand whether this is the call that uploads, and on a slow link the one that does runs long enough for the platform to raise a not-responding dialog. The new form runs the same call on a helper isolate and returns the samemeta_id. The synchronous form is unchanged and still correct off the UI isolate.
Added #
WearConfig.autoStartPlatformHealth, to keep the platform-health reader off until it is wanted. Defaulttrue(unchanged behaviour). Withfalse, granting biosignals consent no longer starts the platform-health source (HealthKit / Health Connect): no permission dialog and no history read until the host callsSynheart.startWearCollection(). For hosts that push heart rate themselves (pushWearHr— a watch relay, a BLE strap) and use platform health only when the user picks it. Without it, a host that re-grants a remembered consent at launch showed the Health Connect dialog on every launch. Behind it:WearModule(autoStartOnConsent:)andWearModule.requestCollection();startWearCollection()now requests collection even when the module is already running.
0.15.0 - 2026-09-25 #
Added — per-instance HSI delivery #
SynheartInstancecan now receive every HSI window it completes.setHsiListener,clearHsiListener,drainHsiandisHsiBufferedare the per-instance equivalent of the staticSynheart.onHSIUpdate, which reaches the personal runtime only. A host reading a second instance's output hadtick()'s return value alone — butstartSessionalso starts the runtime's own 1 s background tick loop on the same pipeline, and a window that loop closes first never comes back fromtick(). Those windows were still emitted (and uploaded), just unreachable from Dart. The listener subscribes to the engine's broadcast, so it sees windows from both paths; a host that also readstick()deduplicates. Buffered delivery on runtime ≥ 0.31.1, push callback on older runtimes. No new native calls: the bridge was already per-handle.
Added — host notification support #
Synheart.runtimeBehaviorEventStream. Every native behavior event in the rich form the personal runtime receives it (notificationactionandsource_app, scroll direction, tap duration), so a host feeding a secondSynheartInstance, which has no collectors, can forward what it needs withpushBehaviorEvent. A facade-level broadcast: one subscription outlives the behavior module being rebuilt.HostDeclarations.notificationsObservablesendsnotifications_observable(runtime ≥ 0.32.0). Absent, the runtime resolves it totrueon Android and desktop, so a host without a running notification listener must declarefalse. Older runtimes ignore it.
Fixed — notification follow-ups counted as arrivals #
- A notification's later outcome no longer reaches the runtime as a new
arrival. Android's collector reports a notification on arrival
(
received) and again with its outcome (ignoredafter 30 s, oropened); the engine counts every notification event as an arrival, so an ignored notification counted twice, inflating the notification rate, Interruption Pressure and the labnotification_count. Follow-ups (isNotificationFollowUp) are no longer pushed to the runtime; the publicbehaviorEventStreamstill carries them. Calls need no filter: both platforms emit one call event, at its outcome (synheart_behavior ≥ 0.4.1).
Changed #
synheart_sessionconstraint^0.2.0→^0.3.0. Picks up the Android watch-relay fix: the relay no longer receives watch messages while no session runs. 0.3.0 is a minor bump because it addsSessionMode.typing; an app with an exhaustiveswitchoverSessionModemust handle the new value.
0.14.0 - 2026-09-23 #
Added — runtime version gate #
- The SDK now states which runtime its bindings assume and checks it at
init.
RuntimeCompat.writtenAgainst(0.31.1) andRuntimeCompat.minimum(0.20.0) are compared againstbuild_info.core_runtimewhen the bridge loads; the result is logged and exposed asSynheart.runtimeCompatibility. Below the minimum,initializerefuses with aStateErrornaming the fix (synheart install runtime); between minimum and written-against it warns once. Until now nothing in this package recorded the runtime version the hand-written C ABI surface was written for, so every behavioural change in the runtime'sSDK-CONTRACT-CHANGES.mdwas invisible to a consumer.
Fixed — hot-restart abort with an active session (runtime ≥ 0.31.1) #
- HSI delivery is now buffered (pull-based) when the runtime supports it.
A Flutter hot restart destroys the Dart isolate and its
NativeCallables, but the native runtime, its tokio workers and the HSI listener survive in the process; the next completed window was dispatched through a dangling pointer and aborted the app (Callback invoked after it has been deleted). Continuous background sensing keeps sessions active across the restart, so on Android this reproduced on every restart mid-session. On a runtime ≥ 0.31.1 the bridge now callssynheart_core_init_hsi_bufferedinstead of registering a callback and drainssynheart_core_drain_hsion a 1 s pump (CoreRuntimeBridge.hsiDrainInterval; ringhsiBufferCapacity, default 64 — the oldest frame is evicted when full).Synheart.tick/tickAlldrain in the same turn, so a host that ticks itself sees no added latency; delivery stays deduplicated byhsi_id. Older runtimes fall back to the push callback unchanged.Synheart.isHsiDeliveryBufferedandSynheart.droppedHsiFramesexpose the mode and the runtime's eviction counter. Same pattern as the buffered logging path. - Stream callback teardown uses
synheart_core_clear_stream_callbackwhen exported (≥ 0.31.1) and closes the trampoline immediately; older runtimes keep the retire-until-coreFreepath. The stream callback itself is still a pushed function pointer — 0.31.1 adds clear-only, no buffered mode — so a hot restart during an active RAMEN stream remains exposed.
Added — research instance fan-in #
SynheartInstancecan now be given an app identity and keystroke context.pushAppForeground,pushContextEvent,pushContextEventJsonandsupportsRichBehaviorEventsare the per-instance equivalents of the staticSynheart.*calls, which reach the personal runtime only. A host running a second (research) instance had no way to feed either, so every research window resolved to theUnknownapp category — an all-zero interpretation-mask row — and carriedcontext_label: UKwith no evidence behind it, starving CFI / Cognitive Load's digital term, Valence's friction index and the behaviour-only Stress path on every research row. No new native calls: both route through the existing bridge symbols.
Fixed — data correctness #
-
Accelerometer samples were pushed in m/s² into an API that takes g, so every sample reached the engine ~9.81x too large.
synheart_behavior'sMotionSampleis documented as m/s² with gravity included (Android's rawTYPE_ACCELEROMETER); the engine'spush_acceltakes g and multiplies by9.80665internally. A phone at rest reported ~9.81 and the engine stored ~96 m/s². It cleared the runtime's own ±50 sanity gate because that gate runs on the pre-multiply value, so nothing ever complained.The SDK now converts at the push boundary. What this was breaking:
activity_stateandlocomotion_state(magnitude and jerk cut-points were all off by the same factor), the RulePack still-gate — which never saw stillness, so no personal physiological baseline could accumulate at all — and any host readingmeta.synheart.motion.accel_rmsback out for its own rest detection, which could never satisfy a low-motion clause.postural_statewas unaffected (APE is an angle, and angles are scale-invariant).
Added — the context evidence channel #
-
ContextEventInput— typed payloads forpush_context_event, the channel that feeds the person-relative context window and socontext.deviation.*. Those deviations are the only source of Cognitive Load's friction index (CFI), and before this the mobile stack pushed nothing onto this channel, sopause_elevation,err_elevationandscroll_deviationwere structurally zero on every window regardless of how much the person interacted.It is a second channel, not an alternative to
pushBehaviorEvent: the two write to different runtime buffers with different consumers, so one event on each per user action is correct and is not a double count. -
BREAKING:
Synheart.pushContextEventnow takes aContextEventInputinstead of aMap<String, dynamic>. Existing call sites fail to compile rather than warn; that is deliberate, because the old map form documented a shape the runtime never accepted, so every such call site is a call site that was silently doing nothing. The shape it documented was{ts_ms, app_id, category}; the runtime instead deserializes this channel into an externally-tagged keyboard / pointer / shortcut enum with no app-category variant, so every such push was rejected. The failure was invisible: a rejected payload returns the same1as a runtime built without theapp-contextcargo feature. The raw escape hatch is nowSynheart.pushContextEventJson(Map).The wire shape is pinned against the runtime's documented form by
test/context_event_input_test.dart, because getting it wrong is silent. -
The SDK derives context events from native gestures automatically — scroll to
Mouse/Scrollwith its direction, swipe toMouse/Movewith a distance. Native taps are deliberately not forwarded: Android's input collector reports every keystroke as atap, so forwarding them would inflateN_clickwith typing while leavingN_key— the denominator oferr_rate = N_corr / N_key— at zero. Keyboard evidence must come from the host's text layer viaContextEventInput.textChange, which is the only place an insertion can be told from a deletion. Send both directions.
Added — foreground app identity #
-
BehaviorEventInput.appForeground(tsMs, app)andSynheart.pushAppForeground(app)— the foreground resolve, as opposed to theappSwitchedge. This kind had no Dart factory at all, which meant app identity could not reach the engine by any route:app_switchonly fires on a transition, and the native collector reports both of its ids as null.With no identity the runtime's
current_appstaysNone,Noneresolves to theUnknownapp category, andUnknown's interpretation-mask row is all zeros — so CFI / Cognitive Load, StressB, Mental FatigueBand Focus's deviation sub-terms all read0for a person who was working the whole time. -
ForegroundAppReporterruns the resolve for the life of a session: once at session start, then on a 30 s heartbeat (shorter than the default 60 s HSI window, so no window goes without an identity). It is gated on app lifecycle by anAppLifecycleListenerthe SDK owns: the heartbeat stops onhidden/paused/detachedand resumes with an immediate resolve onresumed, because the default source names this app and continuing to assert it from the background is worse than silence — the engine would attribute another app's window to this one.inactiveis deliberately not a stop: a call banner or the app switcher is not leaving. A headless host with no boundWidgetsBindinglogs once and runs ungated, which is correct there since it has no foreground to leave. Repeats are safe — the engine treats an unchanged app as a steady-state observation and does not bump the switch count, so a heartbeat cannot fabricate fragmentation. -
BehaviorConfig.reportForegroundApp(defaulttrue),foregroundAppId, andforegroundAppSource. The default source reports the host's own application id, resolved fromforegroundAppId, elseDeviceAuthConfig.packageName, elseSynheartConfig.appIdwhen it looks like a package name rather than a Synheart-issuedapp_…id. ImplementForegroundAppSourceover Android'sUsageStatsManager(permissionPACKAGE_USAGE_STATS) to report what is actually in front; iOS exposes no equivalent API, so the self-report is the ceiling there.
Added — the daily loop #
Synheart.attachStrainScore(), bindingsynheart_core_attach_strain_score_json. This symbol had no Dart binding, androllDaydoes not score for you — it validates the index and folds the day into the longitudinal baselines, and that fold clears the values Strain is computed from. So a host that rolled without attaching first got no Strain score, ever, while the load itself still reached the baselines: nothing looked broken except a score that was always absent. Call it beforerollDay.
Changed #
TypingSessionData's documentation no longer claimsnumber_of_backspace/number_of_deletefeed CFI. They feedTypingFluency; CFI's correction sub-component readscontext.deviation.err_elevation, which comes from the context channel. The advice to send them was right, the reason was wrong, and it would have sent someone hunting in the wrong place.
Example app #
- The
push_context_eventdemo sent the payload shape that never parsed. It is now two buttons for the two real channels — declare foreground app, push a context event — with the counters separated. - The typing probe now pushes a keyboard context event per change alongside its windowed summary, so CFI has a correction rate with a real denominator.
_rollDayIfNeededattaches the Strain score before rolling.- A lateness budget is now declared unconditionally when the host profile is
declared. It is not optional for this host: the engine's HSI window is aligned
to the first signal rather than to a fixed grid, so a host aggregating into
its own fixed-grid micro-windows has one micro-window straddle every HSI
boundary — stamped before it, flushed after it, accepted by the ingest gate
and then read by no window. That is ~1/6 of the typing evidence lost every
window with no counter to show it. Declaring a budget requires declaring a
sensing mode too, because
parse_sensingaccepts"auto"only as a top-level string; the mode now defaults to continuous on Android and episodic on iOS, matching what the toggle already asserts.
0.13.0 - 2026-09-04 #
Changed — BREAKING #
- Potentially blocking Device Sync APIs now return
Future<Map<String, dynamic>?>and execute their native FFI work on a tracked background isolate:syncCreateSpace,syncGeneratePairing,syncJoinSpace,syncRecoverSpace,syncLeaveSpace,syncListDevices,syncRevokeDevice,syncDeleteSpace, andsyncClearLocalSpace. Callers mustawaitthese methods. Sync operations are serialized per runtime handle so lifecycle mutations, roster reads, andsyncNowcannot race. Fast local status and readiness snapshots remain synchronous.
Added #
- Core v0.24 device identity lifecycle support:
Synheart.reattestDeviceAuth()refreshes attestation without rotating the installed device identity, andSynheart.logoutDeviceAuth()clears that identity and its sync membership. Instance-based hosts can usereattestDevice()andlogoutDevice(). The new native operations require Core v0.24 ABI support. Synheart.ensureDeviceAuthRegisteredOrThrow()preserves typed native failures, including account mismatch, so hosts can route recovery correctly.
Changed #
Synheart.logout()now awaits native device logout before wiping local SDK data and revoking consent. Hosts must await it before removing their own account credentials. Older runtimes retain the legacy local-wipe fallback.reregisterDeviceAuth()is deprecated in favor ofreattestDeviceAuth(). Registration is for first-time setup; repair preserves the existing identity.
Fixed #
- Coalesce concurrent device-auth initialization and serialize native registration, re-attestation, logout, and sync operations per runtime handle.
- Compare restored and configured canonical subjects to avoid treating a restored identity as a different account and registering it again.
- Preserve typed native identity errors instead of masking them with the unsigned development fallback; surface device-auth worker failures explicitly.
- Keep native handles alive until tracked background FFI calls finish during disposal, preventing use-after-free while sync work is in flight.
0.12.0 - 2026-08-24 #
Added #
- Device-signed, non-streaming Syni chat and session APIs under
Synheart.syni?.service: chat, list/get sessions, list messages, and close. - Typed Syni service responses/errors, sticky session continuity, serialized
chat turns, and delivery-unknown signaling for safe reconciliation. The
service requires a native Core runtime that exports the
synheart_core_syni_*ABI (runtime 0.21.0 or newer); older runtimes remain usable and report the service as unavailable.
0.11.1 - 2026-08-20 #
Fixed #
-
Android
compileSdkraised from 34 to 36. The package sat below the compileSdk of the plugins it ships alongside and below Flutter's own default, which made it the odd one out in any consumer's build graph.This does not change the minimum supported Android version:
minSdkstays at 24.compileSdkis what the module is built against, not what it requires at runtime.If your build fails with
requires libraries and applications that depend on it to compile against version 36 or later, upgrading this package alone will not fix it. That check runs per consuming module, and the artifacts naming it are transitive AndroidX dependencies (androidx.health.connect,androidx.browser,androidx.core) pulled in by the sibling plugins. Your app module needs the newer compileSdk:// android/app/build.gradle.kts compileSdk = flutter.compileSdkVersion // 36 on current Flutter
0.11.0 - 2026-08-14 #
Changed — BREAKING #
-
The SDK no longer carries a built-in API host.
ApiEndpoints.defaultBaseUrlwashttps://api.synheart.ai; it now defaults to empty and comes fromSYNHEART_BASE_URL. Publishing a company host inside every copy of the package made it the destination for any build that forgot to name one, including forks and self-hosted deployments.Action: set
SYNHEART_BASE_URL(dart-define, or--dart-define-from-file) on any build that talks to the cloud. Apps that already set it are unaffected.With nothing configured,
api_base_urlis now omitted from the runtime config rather than sent empty, and the native runtime applies its own default.apiBaseUrlConfigured(config)reports whether an explicit origin was supplied.
Added #
HSIAxes.focusQuality,interruptionPressure,interactionModeandhasDigital, parsed fromaxes.digital[]. These are derived from interaction rather than physiology, so they resolve on hardware with no biosignal source. Previously the whole domain was discarded at the parse boundary.SyncNativeError.reason,retryAfterMsanddetail, withisUnsupported/isMisconfigured/isPolicyRefusal.reasonnow appears intoString().doc/INTEGRATION.md— ordered walkthrough from platform prerequisites to first upload.
Fixed #
startSession()reported success when the native runtime failed to open a session. A null native result fell through to the Dart-only path, minting acore_<millis>handle — so the host saw a session id and acollectingstate for a session the runtime never opened, with no HSI and no error. It now throws aStateError; the Dart-only path is reached only when no native runtime is present.flushIfEligiblereported "cloudUpload consent not granted" wheneverhasConsentreturned false. Once cloud is configured the runtime denies every consent type until a consent token is issued, so the message blamed a consent screen that was already correct. It now names the real cause.hasConsentaccepts either consent-key spelling; the Dart fallback path only understood camelCase.- The Syni cloud origin resolved through a hard-coded literal instead of
ApiEndpoints, overridingSYNHEART_BASE_URLfor that one caller.
Hardening release from a full-repo review. No new features.
Released as a MINOR, not a patch: this removes and changes public API, and
^0.10.1 would have resolved a patch automatically into apps that use it.
Headline: the SDK could not initialise at all without cloud credentials.
initialize() returned a null native runtime — no HSI, no consent store, no
storage — and then logged "Initialization complete". Local-only operation, a
documented configuration, has never worked in this line until now.
Fixed #
- Local-only initialization was completely broken.
_configurehardcodedingest: {enabled: true, ...}, so a host with noCloudConfigfailed insidesynheart_core_newwithERR_NOT_CONFIGURED: cloud connector org_id must not be empty when HSI ingest is enabled. The handle came back null and the SDK continued as if nothing were wrong.device_auth.enabledwas hardcoded the same way, making the runtime reject crypto-callback registration with a misleading ERROR for every local-only host. Both are now gated on being configured. Hosts that supply aCloudConfigwith anorgId, or aDeviceAuthConfig, are unaffected — the gates evaluate to the previous values. - A dev build with no dart-defines silently talked to production.
SyncConfig.baseUrldefaults toApiEndpoints.defaultAuthBaseUrl, a const alias for theSYNHEART_AUTH_BASE_URLdefine, which is empty unless set. The empty value made the runtime fall back to its own hardcoded production URL.api_base_urlis now resolved explicitly throughresolvedAuthBaseUrl. Same endpoint, no longer by accident. - Packaging: the published archive was 70 MB and contained the proprietary
native runtime.
.pubignorereplaces.gitignorefor publishing, so the.gitignorerules coveringios/SynheartCoreRuntime.xcframework(a symlink the podspec creates into the consumer app — pub follows it) andexample/synheart/vendor/did not apply. The archive carried both framework slices plus their dSYM DWARF blobs. Now ~610 KB, with a CI check that fails the publish job if native artifacts or an oversized archive reappear. flushUploadsblocked the UI isolate. The native call performs its HTTP round-trip underblock_on; it now runs on a background isolate like every other network-touching FFI call in the bridge.- A failed
initialize()could never be retried. The init completer was left in place after an error, so every subsequent call received the same failed future — a transient cause was permanent short ofdispose(). - HSI reached only the
onHsicallback, neveronHSIUpdate/onStateUpdate, for hosts pushing throughpushWearHr/pushVendorHrv. Both producers now share one consent-gated delivery path. (The long-standing claim that the native callback "doesn't fire on iOS" no longer holds against runtime 0.19.2 — verified on device — which is why that path needs the duplicate suppression below.) - Duplicate HSI delivery.
ingest_batch_jsonBOTH broadcasts on the runtime'sstate_tx— which drivessetHsiCallback— and returns the same payload to Dart. With both producers wired to one delivery path, every window completed by a per-event push arrived twice: doubleonHSIUpdate/onStateUpdate, double session-buffer entries, double downstream counters. Now suppressed byHsiDeliveryDeduperonmeta.ids.hsi_id(RFC-IDENTITY-0001), the same key the runtime's own ingest connector dedupes on. It keeps a bounded set of recent ids rather than a single last-id slot, because the producers do not always interleave asA, A— the sync return and the async callback can be separated by a background-tick window, givingA, B, A. A payload without an id is delivered rather than dropped: losing a window is worse than repeating one. - A session could start with nothing able to collect. The runtime path of
startSession()performed none of the checks the Dart fallback path did.startSession()now requires at least one enabled feature whose matching consent is granted — both halves of a pair. Consent alone is not enough: enabling onlywearConfigwhile granting onlybehaviorleaves wear permitted-but-not-enabled and behavior enabled-but-not-permitted, so nothing collects. Cloud upload, vendor sync, research and syni never qualify; they govern what happens to data once gathered. - Session buffers grew without bound.
getSessionHsiWindows()andgetSessionWearSamples()retained every entry for the session's life (24h by default) and cleared only on the nextstartSession(). Both are now capped ring buffers — seemaxSessionHsiWindows/maxSessionWearSamples. isSessionRunningignored the runtime. It returned the Dart module flag; the runtime's ownis_runningwas never read anywhere in the SDK. A session ending natively left hosts reporting "collecting" indefinitely. It now prefers the runtime, falling back to the Dart flag when the bridge is absent.- Configuration errors named a field but not a fix.
validate()threw bare assertions like "appId must not be empty". The messages now say what to set, what a good value looks like, and why — including that passinguserId:toinitialize()does not populateconfig.subjectId, which is the most common way to hit the error. Failures are also logged before the throw, since hosts commonly catch and render a short toast that discards the detail. - One failing module killed the whole session, and orphaned a native one.
ModuleManager.startAll()awaitedmodule.start()unguarded, so the first throw ended the loop and every later module was skipped —initializeAll()andstopAll()were already per-module resilient. Modules start wear-first, so on an iOS build without the HealthKit entitlement nothing started rather than everything-but-biosignals. Separately,startSession()opened the native session before starting modules, so a throw escaped leaving a session the host did not know about; the next call then gotSessionActive, returned null, and fell through to the Dart-only path with acore_<ts>handle for a session the runtime never opened.startAll()is now resilient and reportsmoduleId -> error, andstartSession()rolls the native session back on failure. runtimeDiagnostics()['lastQuality']always reported0.0; it read a native symbol the runtime has never exported outside the edge variant. Removed.
Added #
runtimeDiagnostics()['missingSymbols']— optional native symbols the loaded runtime does not export. ~20 lookups previously failed silently, so a runtime one release behind disabled recovery scores, readiness, breathing, backfill, priority resolution, data deletion, research enrolment, sync-space management and cloud HSI fetch with nothing in the logs. Each miss now logs once, naming the symbol.runtimeDiagnostics(probeAll: true)andprobedSymbols. Optional bindings resolve lazily, so a diagnostics screen readingmissingSymbolsbefore anything used those features reports an empty list and looks healthy while having checked nothing.probeAllforces a full audit. Note the list covers only the guarded bindings: most lab ABI calls are bound eagerly and throw on first access when absent — checkisLabAvailablebefore using them.HSIState.parseError/hasParseError— a parse failure previously returned empty axes, indistinguishable from a window the engine had not populated.buildRuntimeConfigMap(internal) — one source of truth for the JSON handed tosynheart_core_new, replacing three drifting copies, and unit-testable without a native runtime.
Changed #
- BREAKING (direct
CoreRuntimeBridgeusers only):CoreRuntimeBridge.flushUploads()returnsFuture<Map<String, dynamic>?>. Addawait.Synheart.ingestioncallers are unaffected. Synheart.deleteCloudData()now throwsUnsupportedError. It never deleted anything — it logged twice and returned — so a host could have wired it to a "delete my cloud data" control and shipped a promise the SDK did not keep. UserequestDataDeletion()andwipeLocalData().getSyncStatus()reflects real engine/config state instead of a hardcodedfalse.WearModule(useSynheartWear: false)now yields no sources instead of a mock.- Error messages and doc comments no longer cite "native runtime 5.4.0+"; that
scheme does not match the runtime's actual
0.xversioning. They name the remedy (synheart install runtime) instead.
Deprecated #
The native runtime removed bundle-secret configuration as a security fix, and these were never forwarded to it. All are scheduled for removal in 0.12.0.
SynheartConfig.capabilityToken,SynheartConfig.capabilitySecretCloudConfig.apiKey,ConsentConfig.appApiKeySynheart.deleteCloudData(),Synheart.getSyncStatus()Synheart.runtimeBaselineSummary(duplicate ofruntimeBaselinesJson)WearModule(useSynheartWear:)
Removed #
MockWearSourceHandler. It synthesised heart rate, HRV, RR intervals and sleep stages fromRandom(). Those samples were indistinguishable from real ones downstream and fed the runtime's longitudinal baselines, so a host passinguseSynheartWear: falsesilently corrupted the user's on-device reference ranges with invented numbers.WearModule(sources:)already accepts an injected handler, which is the supported way to supply a fake.HsiWindowArtifact,TombstoneArtifact,CapabilityTokenFetcher— no producers, consumers, or implementations.ios/Runner/(host-app scaffolding in a plugin package) andios/synheart_ffi_symbols.txt(an ld64 keep-list referenced by nothing and 68 symbols stale — obsolete since the runtime became a dynamic framework).- A no-op
loadCapabilityToken('{}', secret)call that could never load a capability.
Example app #
Rebuilt as a lean reference: 6,555 lines across 17 files and three competing entry points, down to ~1,600 across 8.
- Consent now uses the canonical runtime editable-form flow
(
consentGetEditableFormTyped→consentSubmitFormTyped→consentEffectiveStateTyped). The old app drove the legacy Dart-side helpers thatSDK_CONSENT_FLOW.md§7-G lists as superseded. - No longer reaches into
package:synheart_core/src/. - Local-only by default — runs with no credentials.
- Never fabricates biosignals, and distinguishes sources that feed the runtime (wear, behavior) from ones that only collect locally (phone context).
example.dart, the snippet pub.dev renders, previously omitted the requiredappId/subjectIdand could not run.
0.10.1 - 2026-07-31 #
Added #
Synheart.pushRrBatch(...)— push a batch of RR intervals anchored to a single timestamp, instead of one call per interval.- Buffered runtime logging (
synheart_core_init_logging_buffered+ log drain) so runtime diagnostics survive a crash. Falls back gracefully when the vendored runtime does not export the symbols.
0.10.0 - 2026-07-29 #
Changed #
- Reorganized and updated the SDK documentation to match the current public API, configuration, native runtime layout, platform requirements, and examples.
0.9.0 - 2026-07-21 #
Added #
Synheart.recordStudyConsent(...)— record a durable study-consent record via the core runtime (newsynheart_core_record_study_consentFFI). Includes a matchingstudyConsentPathendpoint constant.
0.8.2 - 2026-07-07 #
Fixed #
- Sync the
synheartCoreVersionconstant with the package version; 0.8.1 inadvertently shipped it as0.8.0.
0.8.1 - 2026-07-07 #
Changed #
- Widen the
synheart_wearconstraint to>=0.4.0 <0.6.0so the 0.5.x line resolves.synheart_wear0.5.0 is additive and backward compatible (adds an optional per-request auth-header signer for the cloud providers), so the re-exported workout and provider surface is unchanged and no code updates are required.
0.8.0 - 2026-06-29 #
Added #
Synheart.rebindSubjectId(subject)— rebind the subject when the signed-in identity changes: the native runtime atomically re-points consent and the cloud connector, then the SDK syncs its subject and re-mints cloud consent if needed — without a full dispose/reinit.
Changed #
- The native runtime is now the source of truth for the canonical subject id.
Synheart.subjectIdprefers the runtime's value (captured after init) over the configured one, andconsentTokenSubjectStalecompares against it. - Subject FFI degrades softly: on a runtime build that predates the new symbols, subject sync/rebind fall back to the configured value instead of failing init.
0.7.3 - 2026-06-28 #
Changed #
- Bump
synheart_authto^0.1.7. This adds a 30-second timeout to Android Play Integrity device attestation (and pulls in the matching native-SDK timeouts). A stalled attestation — e.g. anIntegrityServicebind that never resolves on a device without Play Store — now fails fast and falls through to the existing unsigned-capabilities path instead of leaving device registration hung.
0.7.2 - 2026-06-28 #
Fixed #
- Cloud consent token is kept scoped to the current subject. A consent token
is issued for a specific
subjectId; reusing one issued for a previous subject (e.g. a different signed-in account, or a token minted before the account was known) would attribute uploads to the wrong subject.ensureCloudConsentReadynow treats a token whoseuser_idclaim differs from the currentsubjectIdas not-ready and reissues it for the current subject, and on (re)init the SDK self-heals by reissuing under the current subject so uploads resume immediately rather than waiting for the next consent change. Best-effort and idempotent — no behavior change for a stable, matching subject. AddsSynheart.consentTokenSubjectStale().
0.7.1 - 2026-06-25 #
Fixed #
syncNownow runs on a background isolate. It previously executed the blocking native sync (a network round-trip) inline on the calling isolate, so a slow or stalled sync froze that isolate until it completed or timed out. Routed through the sameIsolate.runoffload the other network-bound calls use; the publicSynheart.syncNow()already returned aFuture, so this is not an API change.
0.7.0 - 2026-06-17 #
Added #
HSIAxes.stress— typed accessor for the engine's multimodal stress reading (engine v0.10.0; HSI 1.3axes.affective[].stress). Hosts get a named field instead of digging throughrawJson. Resolves tonullon the legacy 1.2 path that never carried it. Parity with the Kotlin/Swift bindings.EdgeIngest— canonical phone-side consumer of the Synheart edge wire contract (watch → phone). Pure Dart (no Flutter import), unit-tests underdart test/flutter test. Parseshr_sample/bio_sample/hsi_artifact/ session events and, for artifacts, dedupes byartifact_id, verifiespayload_hash_sha256== sha256(payload_json), validates the innerhsi_versionagainst the supported set, and produces theartifact_ackbody. Public surface:- Sealed
EdgeEventfamily (HrEvent | BioEvent | ArtifactEvent | SessionEventWrap) plus the typed models (HrSample,BioSample,HsiArtifact,Accel) and theEdgeOutcomeresult family. - Reactive
eventsbroadcastStream<EdgeEvent>, emitting in lock-step with the optionalEdgeIngestListenercallbacks (parity with the KotlinSharedFlowand Swifteventspublisher). - ACK helpers
drainPendingAcks()/buildAckBody(...)/drainAckBody(). dispose()— Flutter-only lifecycle that closes the broadcastStreamController(idempotent). The Kotlin/Swift hot streams have no equivalent. Hosts usingEdgeIngest.eventsmust calldispose()when done.- The Kotlin-only
onUnsupportedHsiVersion/onHashMismatchhooks are folded into theEdgeOutcomereturn value (ArtifactHashMismatch) and logging, plus the optional poison-pill / dead-letter hookEdgeIngestListener.onPoisonPill(artifactId, expected, actual, attempts). - Delivery hardening (the watch outbox is delete-on-ACK):
- Duplicate re-ack — a duplicate
artifact_idis not re-surfaced (ArtifactDuplicate) but is re-queued for ACK, so a lost ACK no longer makes the watch resend forever. - Bounded dedupe set — the seen-artifact set is a bounded LRU
(
seenLruCapacity), keeping memory flat over a long-lived process. - Poison-pill dead-letter — an artifact that fails hash verification
poisonPillThreshold(3) times for the same id is dead-lettered (ArtifactDeadLettered, theonPoisonPillhook, and ack-to-discard) so a deterministically-corrupt artifact stops blocking the outbox.
- Duplicate re-ack — a duplicate
- Sealed
Notes #
- Requires the Synheart Runtime. This package is a thin Flutter wrapper; the
on-device logic lives in the Synheart Runtime native binary, which is installed
separately via the Synheart CLI (
synheart install runtime). See the README's "Native Runtime Setup".
0.6.3 - 2026-06-07 #
Added #
Synheart.requestStudyDataDeletion({bool dryRun}): request erasure of the data the participant contributed to their study for this app — the deletion the consent copy promises alongside withdrawal. No identifiers are passed; the participant and app come from the device's signed cloud credential.dryRunreturns an inventory preview without deleting; a real request is accepted asynchronously and carries arequest_id. Idempotent.
0.6.2 - 2026-06-07 #
Added #
- Research-study enrolment API:
Synheart.enrolResearchStudy(...),Synheart.validateResearchStudyCodes(...), andSynheart.withdrawResearchStudy(). Enrolment rides the device's signed cloud credential — no tokens are handled by the app. Withdrawal is idempotent. DeviceAuthConfig.packageName: the app's package / bundle id, passed through so device registration can be bound to the app. Optional (defaults to empty).
0.6.1 - 2026-05-24 #
Added #
- Consent surface for Syni.
ConsentForm.syni, the parameterSynheart.grantConsent(..., syni: bool), andConsentSnapshot.syniare now exposed end-to-end so hosts can track and persist the user's consent for on-device or cloud LLM processing alongside the other channels. Backward compatible —synidefaults tofalseeverywhere so existing callers compile unchanged. - Canonical channel iteration. New
ConsentTypeMetaextension onConsentTypeexposeswireKey,displayName,valueOn(typed accessor forConsentEffectiveState), andvalueOnForm(typed accessor forConsentForm). PlusConsentEffectiveState.toChannelMap()/ConsentForm.toChannelMap()andConsentForm.fromChannelMap(...). Hosts can now iterateConsentType.valuesrather than hardcoding each channel — adding a new channel becomes a single change in the SDK enum plus its metadata; consumers pick it up automatically. Synheart.consentChanges— public broadcast stream ofConsentSnapshots. Emits on any grant or revoke so hosts that render consent in multiple places (e.g. a profile counter alongside a consent screen) can stay in sync without pollingconsentEffectiveStateTypedor re-dispatching reads.Synheart.syni.bindPersona(SyniPersona)(passthrough topackage:syni's newSyniAgent.bindPersona). Cloud chat needs only a persona plus a configured cloud client; hosts that pickSyniExecutionMode.cloudOnlycan call this once instead of runninginstall()purely to attach a persona. Requiressyni: ^0.3.2.
Changed #
synidependency bumped to^0.3.2(carriesbindPersona+ the fix that letscloudOnlychat run without a local install).ConsentEffectiveState.hasAnyGrantnow includessyni— previously the flag was tracked through the rest of the state but missing from this convenience getter.
Fixed #
consentSubmitFormTypednow propagates the form'ssynivalue to the runtime via the per-type grant/revoke FFI when the runtime's form parser doesn't carry the key directly. Without this a host settingsynion the typed form would see the other channels update butsynistay at its previous value.
0.6.0 - 2026-05-23 #
Added #
Synheart.configure()now wires Syni's hybrid router to the cloud — it builds theSyniCloudConfigand authenticates Syni cloud-chat requests as device-attested cloud requests.Synheart.synicloud chat needs no extra host-app setup.DeviceAuthProvider.signUrl— device-attests an absolute URL (no base-URL prefixing);signRequestnow delegates to it.
Changed #
- Bumped the
synidependency to^0.3.0, whoseSyniCloudConfigreplaced the staticauthTokenwith the request-awareauthHeaders. Consumers building aSyniCloudConfigdirectly need to update to the new shape; consumers usingSynheart.configure()are unaffected. - The SDK-side default
SyniCloudConfig.authHeadersclosure lazily device-attests each request — works whether device auth resolves through the DartDeviceAuthProvideror the native runtime.
0.5.3 - 2026-05-21 #
Fixed #
- iOS: the native runtime can now start on the ONNX-backed tiers.
The podspec force-loads ONNX Runtime into the host app binary. The
runtime framework resolves
OrtGetApiBaseat load time, andonnxruntime-cships ONNX as a static archive — nothing in the host app references it directly, so the linker dead-stripped the whole archive and the runtime crashed on first use. Consumers no longer need any per-app linker configuration; depending onsynheart_coreis enough. iOS build configuration only — no Dart API change.
0.5.2 - 2026-05-20 #
Changed #
- Bumped the
synheart_behaviordependency to^0.4.0. The behavior SDK is now an event producer — a session summary may arrive with nobehavioralMetrics.BehaviorSessionResults.fromSummaryalready treats those metrics as optional and defaults each to 0 when absent.
0.5.1 - 2026-05-19 #
Changed #
- Scrubbed internal references from the public package surface — dartdoc comments, section header dividers, and barrel-file comments no longer cite internal identifiers or internal documentation paths. Comments and doc strings only; no code or API changes.
0.5.0 - 2026-05-19 #
Added #
- Cross-device baseline sync.
Synheart.syncCreateSpace,syncJoinSpace,syncGeneratePairing,syncStatus— pair two devices and replicate baselines in either direction with end-to-end encryption. - Offline export / import.
Synheart.baselineExportOffline(passphrase)produces an encrypted.srm.synheartbundle (6-word passphrase, never sent to any server).baselineImportOffline(passphrase, bytes)returns{imported, skipped, errors}. Lets users move baselines across devices without the cloud. BaselineLocalHydratorfacade —wireLocalHydrator(...)is the new entry point for hydrating baselines into the SDK from device-local sources.
Fixed #
- iOS native runtime loading. Replaced the relative-path framework
open with
DynamicLibrary.process(), which resolves symbols from the auto-loaded embedded framework. The previous form silently failed and surfaced only as a generic "Native runtime not loaded" warning. - Runtime initialization failures are now visible. When the native runtime rejects a configuration on iOS, the host now sees the actual reason instead of a silent fallback.
Changed (breaking) #
wireCloud(...)removed. Migrate towireLocalHydrator(...). The separate baseline cloud uploader is retired; baselines now ride the cross-device sync path.- iOS install model. Podspec moves to a vendored dynamic framework
installed via the synheart CLI (
synheart install runtime), rather than a static library bundled with the package. Consumer Podfile gets aprepare_commandsymlink pointing at the CLI-installed vendor dir.
Other #
- Logging hygiene: consent change events emit one structured line instead of an 8-line block; native bridge startup is silent on the happy path and warns only on real failures; baseline scoring no longer dumps the full engine input JSON.
Requires #
- The matching Synheart native runtime release (v0.10.0).
0.4.0 - 2026-05-16 #
Added #
Synheart.syni— gated Syni client surface with install lifecycle + chat + chatStream, delegating topackage:syni'sSyniAgent.SyniContextBuilder— projects this SDK's HSI (live state + stored session history) into the runtime's conditioning contract. HSI-version-agnostic; iterates whatever axes the runtime emitted.- Trivial-message context skip — for greetings / acks, ship only the persona prefix instead of full HSI + history (~30–50% prefill reduction on short messages).
SyniSpecPersonare-exported frompackage:syniso consumers canSyniSpecPersona.load('focus.coach.v1')for canonical prompts.Synheart.{closeOrphanSession, sweepOrphanSessions}— host-side orphan cleanup. Sweep on app start closesstate='active'sessions older than 6h that the runtime never finalized (force-kill, OS reclaim, sudden reboot).Synheart.configureSyniCloud(...)— injects cloud client config;Synheart.syni.hasCloud+ per-callSyniExecutionModeroutes between local and cloud.- Cold-start restore —
SyniAgent.restoreInstallIfReadychecks disk before download flow; consumers don't re-prompt for an install when the model is already cached.
Changed #
synidependency switched frompath: ../syni-flutterto^0.1.0(now published on pub.dev).SessionSummaryArtifactparser rewritten to match the runtime's actual wire format (nestedheaderblock,started_at_ms/ended_at_mskeys, nullable per-axis aggregates, structuredSessionAggregateskeyed by axis name).SessionRecordnow readsstarted_at_msfrom the FFI (was missing — surfaced as 1970-01-01 timestamps in downstream digests).stateandendedAtUtcexposed for the sweep's filter.
0.3.0 - 2026-05-14 #
Added #
Synheart.labReenqueueSession(String sessionJson)— replay a previously-finalized lab session payload through the cloud connector. Useful when the initial upload was dropped on a 4xx (typically a cloud schema mismatch — the runtime removes those rows from the in-memory upload queue). Host reads the persisted JSON from app-side storage and passes it back.Synheart.isLabReenqueueAvailable— feature-detect whether the linked runtime binary exports the symbol (engine v0.8.1+).LabReenqueueResultenum — mirrors the FFI return codes (queued,researchNotAllowed,cloudNotConfigured,parseError,invalidArgument,unsupported).CoreRuntimeBridge.labReenqueueSessioninstance method + companionisLabReenqueueAvailablegetter; new_LabReenqueueC/_LabReenqueueDarttypedefs inffi_bindings.dart. Lookup is optional so older runtime binaries (pre-v0.8.1) keep loading without errors.- Customer-facing data deletion API (
requestDataDeletion,getDataDeletion,listDataDeletions) andDataDeletion*models for GDPR Article 17.
Runtime compatibility #
- Requires the Synheart native runtime v0.8.1+ for
labReenqueueSessionto actually invoke the FFI. Older binaries returnLabReenqueueResult.unsupported.
0.2.0 - 2026-05-09 #
Added #
- HSI 1.3 envelope parsing in
HSIStateandHSIPayload. Producers emit the closed 5-axis domain set (physiological,kinematic,digital,cognitive,affective) with deterministic UUIDv5hsi_id. The SDK now parses the new shape and exposes the digital readings (focus_quality,interruption_pressure,interaction_mode) alongside the existing physiological / cognitive / affective fields. The 1.2 wire shape is still accepted as a fallback; consumers should not rely on that path long-term.
Fixed #
BehaviorModule._convertSynheartEventnow forwardsBehaviorEventType.app_switchto the runtime instead of dropping it via the default-arm. The runtime needsapp_switchto detect notification responses (an app switch shortly after a notification) and to anchor session boundaries between foreground events of different apps. Without this forward, the digital readings on the HSI 1.3 envelope (axes.digital[]—focus_quality,interruption_pressure,interaction_mode) were silent on iOS and Android.
0.1.1 - 2026-05-08 #
Changed #
- Bumped
synheart_authdep to^0.1.2. Picks up the Maven Centralai.synheart:synheart-auth:0.1.1upgrade (clock-skew auto-apply, register/rotate race fix, HTTP timeouts, audit-log PII redaction).
0.1.0 - 2026-05-08 #
First public open-source release on pub.dev. The SDK is now a thin FFI shell over the native Synheart Runtime — storage, crypto, sync, consent, the artifact pipeline, the cloud connector, and SRM live in the runtime, and this package exposes them through a Dart surface.
The native runtime is installed via the
Synheart CLI
(synheart install runtime); see the README.
This release consolidates the OSS-launch refactors plus the
breaking change to processVendorEvent.
Breaking #
Synheart.processVendorEvent(...)andWearModule.processVendorEvent(...)now returnFuture<CanonicalWearableEvent?>instead ofFuture<void>. The previous void return discarded the canonical event the vendor payload was mapped to. Mirrors the Swift and Kotlin counterparts.- Removed
CloudConfig.tenantId. Drop the argument —app_idis the only identifier the SDK sends. - Removed
CloudConfig.hmacSecret. Request signing uses the device key;authProvideris now optional. - Removed
InvalidTenantError. ConsentFormshape is flat (profile_id,biosignals,phone_context,behavior,consent_tier,allow_cloud,allow_research,allow_vendor_sync) to mirror the runtime. Hosts that previously builtcategories[] → channels[]structures must migrate to the flat form.- Consent type strings are snake_case at the runtime boundary
(
phone_context/cloud_upload/vendor_sync). Flutter still accepts camelCase on its public API. - Removed
ConsentCategoryandConsentChanneltypes — channel-level truth is owned by the runtime.
Added #
Synheart.rawRamenEvents— broadcastStream<RamenEvent>re-exported fromsynheart_wear, surfacing every RAMEN event with its capability-flavoreddeliveryHint.app_id/user_idfrom the activestartVendorSyncconfig are stamped onto each event soRamenEventDispatchercan drive REST pulls without extra plumbing.- Offline-first consent FFI surface:
consentConfigureCloud,consentGetEditableForm,consentSubmitForm,consentEffectiveState,consentStatus,consentNeedsTokenRefresh,consentClearStored. Local choice is persisted immediately; cloud sync is best-effort. - Typed consent form accessors:
consentGetEditableFormTyped()andconsentSubmitFormTyped({ required ConsentForm form, … }). - Consent coverage for
vendorSyncandresearchthroughhasConsent()/getConsentStatusMap()and the granular grant API. - Durable
data_dirfor the native runtime viapath_provider. Fixes SRM snapshots and the artifact SQLite landing instd::env::temp_dir()(cleaned up by the OS), which broke baseline persistence across app restarts.
Changed #
setStreamCallbackauto-route now skipsdelivery_hint == "ping"events. Their inlinepayload_jsonis empty by design (Garmin / Oura / Fitbit only send a notification), so apps must subscribe torawRamenEventsand useRamenEventDispatcherto fetch the full record before re-feeding the canonical pipeline.- README rewritten to match the actual public API.
- Dependencies on sibling Synheart SDKs are now hosted on pub.dev
(
synheart_wear ^0.4.0,synheart_session ^0.2.0,synheart_behavior ^0.3.0,synheart_auth ^0.1.1) instead of git refs.