forge-openclaw-plugin 0.2.83 → 0.2.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -13,14 +13,14 @@
13
13
  />
14
14
  <link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
15
15
  <link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
16
- <script type="module" crossorigin src="/forge/assets/index-DEoJdpz5.js"></script>
16
+ <script type="module" crossorigin src="/forge/assets/index-BNvUaA6y.js"></script>
17
17
  <link rel="modulepreload" crossorigin href="/forge/assets/vendor-BVU0cZC9.js">
18
18
  <link rel="modulepreload" crossorigin href="/forge/assets/board-DKxKOwax.js">
19
19
  <link rel="modulepreload" crossorigin href="/forge/assets/ui-3Wd4pVaA.js">
20
20
  <link rel="modulepreload" crossorigin href="/forge/assets/motion-CM4AfIqo.js">
21
21
  <link rel="modulepreload" crossorigin href="/forge/assets/table-BUeQ9wzR.js">
22
22
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-B-Lq_OG3.css">
23
- <link rel="stylesheet" crossorigin href="/forge/assets/index-Chc9CWm5.css">
23
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-NqIbz_lv.css">
24
24
  </head>
25
25
  <body class="bg-canvas text-ink antialiased">
26
26
  <div id="root"></div>
@@ -41,7 +41,12 @@ const generatedHealthEventTemplateSchema = z.object({
41
41
  enabled: z.boolean().default(false),
42
42
  workoutType: z.string().trim().min(1).default("workout"),
43
43
  title: z.string().trim().default(""),
44
- durationMinutes: z.number().int().positive().max(24 * 60).default(45),
44
+ durationMinutes: z
45
+ .number()
46
+ .int()
47
+ .positive()
48
+ .max(24 * 60)
49
+ .default(45),
45
50
  xpReward: z.number().int().min(0).max(500).default(0),
46
51
  tags: z.array(z.string().trim()).default([]),
47
52
  links: z.array(healthLinkSchema).default([]),
@@ -110,7 +115,12 @@ const companionSourceStatesSchema = z.object({
110
115
  export const createCompanionPairingSessionSchema = z.object({
111
116
  label: z.string().trim().default("Forge Companion"),
112
117
  userId: z.string().trim().nullable().optional(),
113
- expiresInMinutes: z.coerce.number().int().min(5).max(24 * 60).default(30),
118
+ expiresInMinutes: z.coerce
119
+ .number()
120
+ .int()
121
+ .min(5)
122
+ .max(24 * 60)
123
+ .default(30),
114
124
  transportMode: z
115
125
  .enum(["iroh", "manual-http"])
116
126
  .default("iroh")
@@ -221,7 +231,11 @@ export const mobileHealthSyncSchema = z.object({
221
231
  endedAt: z.string().datetime(),
222
232
  sourceTimezone: z.string().trim().min(1).default("UTC"),
223
233
  localDateKey: z.string().trim().min(1),
224
- providerRecordType: z.string().trim().min(1).default("healthkit_sleep_sample"),
234
+ providerRecordType: z
235
+ .string()
236
+ .trim()
237
+ .min(1)
238
+ .default("healthkit_sleep_sample"),
225
239
  rawStage: z.string().trim().min(1),
226
240
  rawValue: z.number().int().nullable().optional(),
227
241
  payload: z.record(z.string(), z.unknown()).default({}),
@@ -294,14 +308,13 @@ export const mobileHealthSyncSessionStartSchema = mobileHealthSyncSchema
294
308
  sourceStates: true
295
309
  })
296
310
  .extend({
297
- schemaVersion: z
298
- .string()
299
- .trim()
300
- .default(HEALTH_MOBILE_SYNC_SCHEMA_VERSION),
311
+ schemaVersion: z.string().trim().default(HEALTH_MOBILE_SYNC_SCHEMA_VERSION),
301
312
  requestedFamilies: z
302
313
  .array(mobileHealthSyncFamilySchema)
303
314
  .default(defaultMobileHealthSyncFamilies),
304
- expectedCounts: z.record(z.string(), z.number().int().nonnegative()).default({}),
315
+ expectedCounts: z
316
+ .record(z.string(), z.number().int().nonnegative())
317
+ .default({}),
305
318
  metadata: z.record(z.string(), z.unknown()).default({})
306
319
  });
307
320
  const workoutTimeSeriesChunkPayloadSchema = z.object({
@@ -355,7 +368,9 @@ export const mobileHealthSyncChunkSchema = z.object({
355
368
  });
356
369
  export const mobileHealthSyncSessionCompleteSchema = z.object({
357
370
  finalCursor: z.record(z.string(), z.unknown()).default({}),
358
- expectedCounts: z.record(z.string(), z.number().int().nonnegative()).default({})
371
+ expectedCounts: z
372
+ .record(z.string(), z.number().int().nonnegative())
373
+ .default({})
359
374
  });
360
375
  export const verifyCompanionPairingSchema = z.object({
361
376
  sessionId: z.string().trim().min(1),
@@ -535,9 +550,9 @@ function upsertPairingSourceState(pairing, source, patch) {
535
550
  ...patch.metadata
536
551
  }
537
552
  : safeJsonParse(current.metadata_json, {});
538
- const nextDesiredEnabled = patch.desiredEnabled ?? (current.desired_enabled === 1);
539
- const nextAppliedEnabled = patch.appliedEnabled ?? (current.applied_enabled === 1);
540
- const nextSyncEligible = patch.syncEligible ?? (current.sync_eligible === 1);
553
+ const nextDesiredEnabled = patch.desiredEnabled ?? current.desired_enabled === 1;
554
+ const nextAppliedEnabled = patch.appliedEnabled ?? current.applied_enabled === 1;
555
+ const nextSyncEligible = patch.syncEligible ?? current.sync_eligible === 1;
541
556
  const nextUpdatedAt = nowIso();
542
557
  getDatabase()
543
558
  .prepare(`UPDATE companion_pairing_source_states
@@ -681,9 +696,7 @@ function computeSleepDerivedMetrics(input) {
681
696
  })
682
697
  .reduce((total, stage) => total + stage.seconds, 0);
683
698
  const restorativeShare = input.asleepSeconds > 0 ? restorativeSeconds / input.asleepSeconds : 0;
684
- const sleepDebtHours = input.asleepSeconds > 0
685
- ? Math.max(0, 8 - input.asleepSeconds / 3600)
686
- : 8;
699
+ const sleepDebtHours = input.asleepSeconds > 0 ? Math.max(0, 8 - input.asleepSeconds / 3600) : 8;
687
700
  return {
688
701
  durationHours: round(input.asleepSeconds / 3600, 2),
689
702
  efficiency: round(efficiency, 3),
@@ -888,10 +901,13 @@ function mapSleepRawLog(row) {
888
901
  createdAt: row.created_at
889
902
  };
890
903
  }
891
- function mapWorkoutSession(row) {
904
+ function mapWorkoutSession(row, options = {}) {
892
905
  const provenance = safeJsonParse(row.provenance_json, {});
893
906
  const derived = safeJsonParse(row.derived_json, {});
894
- const analytics = getStoredWorkoutAnalytics(row);
907
+ const includeAnalytics = options.includeAnalytics ?? true;
908
+ const analytics = includeAnalytics
909
+ ? getStoredWorkoutAnalytics(row)
910
+ : undefined;
895
911
  const presentation = buildWorkoutSessionPresentation({
896
912
  source: row.source,
897
913
  sourceType: row.source_type,
@@ -937,7 +953,7 @@ function mapWorkoutSession(row) {
937
953
  annotations: safeJsonParse(row.annotations_json, {}),
938
954
  provenance,
939
955
  derived,
940
- analytics,
956
+ ...(analytics ? { analytics } : {}),
941
957
  generatedFromHabitId: row.generated_from_habit_id,
942
958
  generatedFromCheckInId: row.generated_from_check_in_id,
943
959
  reconciliationStatus: row.reconciliation_status,
@@ -1113,7 +1129,10 @@ function inferHistoricalRawStage(payload) {
1113
1129
  return "asleep_unspecified";
1114
1130
  }
1115
1131
  function inferHistoricalSleepBucket(stage) {
1116
- const normalized = stage.trim().toLowerCase().replace(/[\s-]+/g, "_");
1132
+ const normalized = stage
1133
+ .trim()
1134
+ .toLowerCase()
1135
+ .replace(/[\s-]+/g, "_");
1117
1136
  if (normalized.includes("bed")) {
1118
1137
  return "in_bed";
1119
1138
  }
@@ -1135,7 +1154,7 @@ export function listSleepSessions(userIds) {
1135
1154
  return listSleepRows(userIds).map(mapSleepSession);
1136
1155
  }
1137
1156
  export function listWorkoutSessions(userIds) {
1138
- return listWorkoutRows(userIds).map(mapWorkoutSession);
1157
+ return listWorkoutRows(userIds).map((row) => mapWorkoutSession(row));
1139
1158
  }
1140
1159
  export function getSleepSessionById(sleepId) {
1141
1160
  ensureLegacyAppleSleepHistoryRepaired();
@@ -1155,22 +1174,24 @@ function sleepHasReflection(session) {
1155
1174
  tags.length > 0);
1156
1175
  }
1157
1176
  function sleepEfficiency(session) {
1158
- return typeof session.derived.efficiency === "number"
1177
+ return typeof session.derived.efficiency ===
1178
+ "number"
1159
1179
  ? session.derived.efficiency
1160
1180
  : session.timeInBedSeconds > 0
1161
1181
  ? session.asleepSeconds / session.timeInBedSeconds
1162
1182
  : 0;
1163
1183
  }
1164
1184
  function sleepRestorativeShare(session) {
1165
- return typeof session.derived.restorativeShare ===
1166
- "number"
1185
+ return typeof session.derived
1186
+ .restorativeShare === "number"
1167
1187
  ? session.derived.restorativeShare
1168
1188
  : 0;
1169
1189
  }
1170
1190
  function sleepRecoveryState(session) {
1171
1191
  return typeof session.derived.recoveryState ===
1172
1192
  "string"
1173
- ? (session.derived.recoveryState || null)
1193
+ ? session.derived.recoveryState ||
1194
+ null
1174
1195
  : null;
1175
1196
  }
1176
1197
  function sleepQualitativeState(session) {
@@ -1211,7 +1232,9 @@ function sleepStageShare(session) {
1211
1232
  return session.stageBreakdown.map((stage) => ({
1212
1233
  stage: stage.stage,
1213
1234
  seconds: stage.seconds,
1214
- percentage: session.asleepSeconds > 0 ? round(stage.seconds / session.asleepSeconds, 3) : 0
1235
+ percentage: session.asleepSeconds > 0
1236
+ ? round(stage.seconds / session.asleepSeconds, 3)
1237
+ : 0
1215
1238
  }));
1216
1239
  }
1217
1240
  function buildSleepSurfaceNight(session, baselineAverageSleepSeconds) {
@@ -1239,8 +1262,8 @@ function buildSleepSurfaceNight(session, baselineAverageSleepSeconds) {
1239
1262
  hasRawSegments: session.rawSegmentCount > 0,
1240
1263
  qualitySummary: typeof session.annotations.qualitySummary ===
1241
1264
  "string"
1242
- ? (session.annotations.qualitySummary ||
1243
- null)
1265
+ ? session.annotations
1266
+ .qualitySummary || null
1244
1267
  : null,
1245
1268
  stageBreakdown: sleepStageShare(session)
1246
1269
  };
@@ -1289,7 +1312,10 @@ function pickDisplaySleepSessions(sessions) {
1289
1312
  return [...byDateKey.values()].sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));
1290
1313
  }
1291
1314
  function normalizeTimelineStage(stage, bucket) {
1292
- const normalized = stage.trim().toLowerCase().replace(/[\s-]+/g, "_");
1315
+ const normalized = stage
1316
+ .trim()
1317
+ .toLowerCase()
1318
+ .replace(/[\s-]+/g, "_");
1293
1319
  if (bucket === "in_bed" || normalized.includes("bed")) {
1294
1320
  return "in_bed";
1295
1321
  }
@@ -1560,8 +1586,7 @@ export function revokeCompanionPairingSession(pairingSessionId, activity) {
1560
1586
  }
1561
1587
  export function revokeAllCompanionPairingSessions(input, activity) {
1562
1588
  const parsed = revokeAllCompanionPairingSessionsSchema.parse(input ?? {});
1563
- const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined)
1564
- .filter((row) => parsed.includeRevoked || row.status !== "revoked");
1589
+ const rows = listPairingRows(parsed.userIds.length > 0 ? parsed.userIds : undefined).filter((row) => parsed.includeRevoked || row.status !== "revoked");
1565
1590
  const sessions = revokePairingRows(rows, {
1566
1591
  actor: activity?.actor ?? null,
1567
1592
  source: activity?.source ?? "ui",
@@ -1794,7 +1819,8 @@ function normalizeSleepSegmentInput(input) {
1794
1819
  return {
1795
1820
  ...input,
1796
1821
  sourceTimezone,
1797
- localDateKey: input.localDateKey || localDateKeyForTimezone(input.endedAt, sourceTimezone)
1822
+ localDateKey: input.localDateKey ||
1823
+ localDateKeyForTimezone(input.endedAt, sourceTimezone)
1798
1824
  };
1799
1825
  }
1800
1826
  function normalizeSleepRawRecordInput(input) {
@@ -1802,7 +1828,8 @@ function normalizeSleepRawRecordInput(input) {
1802
1828
  return {
1803
1829
  ...input,
1804
1830
  sourceTimezone,
1805
- localDateKey: input.localDateKey || localDateKeyForTimezone(input.endedAt, sourceTimezone)
1831
+ localDateKey: input.localDateKey ||
1832
+ localDateKeyForTimezone(input.endedAt, sourceTimezone)
1806
1833
  };
1807
1834
  }
1808
1835
  function listNormalizedSleepNights(payload) {
@@ -2018,8 +2045,7 @@ function upsertVitalDaySummary(userId, input) {
2018
2045
  }
2019
2046
  function summarizeUserHealthDay(userId, dateKeyValue) {
2020
2047
  const sleeps = listSleepRows([userId]).filter((row) => sleepSessionDateKey(row) === dateKeyValue ||
2021
- localDateKeyForTimezone(row.started_at, resolveTimeZone(row.source_timezone)) ===
2022
- dateKeyValue);
2048
+ localDateKeyForTimezone(row.started_at, resolveTimeZone(row.source_timezone)) === dateKeyValue);
2023
2049
  const workouts = listWorkoutRows([userId]).filter((row) => dayKey(row.started_at) === dateKeyValue);
2024
2050
  const totalSleepSeconds = sleeps.reduce((sum, row) => sum + row.asleep_seconds, 0);
2025
2051
  const totalWorkoutSeconds = workouts.reduce((sum, row) => sum + row.duration_seconds, 0);
@@ -2147,12 +2173,16 @@ function insertOrUpdateWorkoutSession(pairing, input) {
2147
2173
  (typeof existingProvenance.sourceProductType === "string"
2148
2174
  ? existingProvenance.sourceProductType
2149
2175
  : null),
2150
- activity: input.activity ?? existingDerived.activity ?? existingProvenance.activity,
2176
+ activity: input.activity ??
2177
+ existingDerived.activity ??
2178
+ existingProvenance.activity,
2151
2179
  details: input.details ?? existingDerived.details ?? existingProvenance.details
2152
2180
  });
2153
2181
  const mergedLinks = mergeHealthLinks(existingLinks, input.links, annotations.links);
2154
2182
  const mergedTags = mergeStringLists(existingTags, annotations.tags);
2155
- const nextSubjectiveEffort = matchedGenerated.subjective_effort ?? annotations.subjectiveEffort ?? null;
2183
+ const nextSubjectiveEffort = matchedGenerated.subjective_effort ??
2184
+ annotations.subjectiveEffort ??
2185
+ null;
2156
2186
  const nextMoodBefore = matchedGenerated.mood_before || annotations.moodBefore;
2157
2187
  const nextMoodAfter = matchedGenerated.mood_after || annotations.moodAfter;
2158
2188
  const nextMeaningText = matchedGenerated.meaning_text || annotations.meaningText;
@@ -2201,7 +2231,8 @@ function insertOrUpdateWorkoutSession(pairing, input) {
2201
2231
  }), matchedGenerated.generated_from_habit_id ? "merged" : "standalone", now, matchedGenerated.id);
2202
2232
  persistWorkoutEvidenceForInput(matchedGenerated.id, pairing.user_id, input);
2203
2233
  return {
2204
- mode: matchedGenerated.generated_from_habit_id || matchedGenerated.source !== "apple_health"
2234
+ mode: matchedGenerated.generated_from_habit_id ||
2235
+ matchedGenerated.source !== "apple_health"
2205
2236
  ? "merged"
2206
2237
  : "updated",
2207
2238
  id: matchedGenerated.id
@@ -2364,13 +2395,14 @@ function backfillHistoricalSleepEvidence() {
2364
2395
  )
2365
2396
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2366
2397
  .run(sourceRecordId, rawLog.import_run_id, rawLog.pairing_session_id, session.id, rawLog.user_id, "apple_health", "historical_import_interval", rawLog.external_uid ?? rawLog.id, session.source_device, sourceTimezone, localDateKey, rawLog.started_at ?? session.started_at, rawLog.ended_at ?? session.ended_at, inferHistoricalRawStage(payload), null, "historical_import", JSON.stringify(payload), JSON.stringify({
2367
- ...(safeJsonParse(rawLog.metadata_json, {})),
2398
+ ...safeJsonParse(rawLog.metadata_json, {}),
2368
2399
  migratedFromRawLogId: rawLog.id
2369
2400
  }), rawLog.created_at);
2370
2401
  }
2371
2402
  }
2372
2403
  if (existingSegmentCount.count === 0 && rawLogs.length > 0) {
2373
- const authoritativeLogs = selectAuthoritativeSleepRows(rawLogs.filter((row) => typeof row.started_at === "string" && typeof row.ended_at === "string"));
2404
+ const authoritativeLogs = selectAuthoritativeSleepRows(rawLogs.filter((row) => typeof row.started_at === "string" &&
2405
+ typeof row.ended_at === "string"));
2374
2406
  for (const rawLog of authoritativeLogs) {
2375
2407
  const payload = safeJsonParse(rawLog.payload_json, {});
2376
2408
  const inferredStage = inferHistoricalRawStage(payload);
@@ -2499,7 +2531,9 @@ function ensureLegacyAppleSleepHistoryRepaired() {
2499
2531
  for (const cluster of clusters) {
2500
2532
  const rowsForNight = selectAuthoritativeSleepRows(cluster);
2501
2533
  const startedAt = rowsForNight[0].started_at;
2502
- const endedAt = rowsForNight.reduce((latest, row) => Date.parse(row.ended_at) > Date.parse(latest) ? row.ended_at : latest, rowsForNight[0].ended_at);
2534
+ const endedAt = rowsForNight.reduce((latest, row) => Date.parse(row.ended_at) > Date.parse(latest)
2535
+ ? row.ended_at
2536
+ : latest, rowsForNight[0].ended_at);
2503
2537
  const sourceTimezone = inferHistoricalSleepTimeZone(cluster.map((row) => row.source_timezone));
2504
2538
  const localDateKey = localDateKeyForTimezone(endedAt, sourceTimezone);
2505
2539
  const asleepSeconds = unionDurationSeconds(rowsForNight.map((row) => ({
@@ -2897,7 +2931,7 @@ function mobileSyncSessionPairing(session) {
2897
2931
  function mobileSyncSessionMetadata(session) {
2898
2932
  return safeJsonParse(session.source_metadata_json, {});
2899
2933
  }
2900
- function applyWorkoutSummaryChunkImmediately(session, workouts) {
2934
+ function applyWorkoutChunkImmediately(session, family, workouts) {
2901
2935
  if (workouts.length === 0) {
2902
2936
  return;
2903
2937
  }
@@ -2951,6 +2985,7 @@ function applyWorkoutSummaryChunkImmediately(session, workouts) {
2951
2985
  .run(device.sourceDevice, JSON.stringify({
2952
2986
  progressiveChunk: true,
2953
2987
  syncSessionId: session.id,
2988
+ family,
2954
2989
  workouts: workouts.length
2955
2990
  }), workouts.length, createdCount, updatedCount, mergedCount, now, now, runId);
2956
2991
  });
@@ -2999,24 +3034,34 @@ export function startMobileHealthSyncSession(payload) {
2999
3034
  if (existing &&
3000
3035
  existing.pairing_session_id === pairing.id &&
3001
3036
  existing.status === "running") {
3002
- const receivedChunkIds = getDatabase()
3003
- .prepare(`SELECT chunk_id
3004
- FROM health_mobile_sync_chunks
3005
- WHERE sync_session_id = ?
3006
- ORDER BY sequence ASC`)
3007
- .all(resumeSyncSessionId)
3008
- .map((row) => row.chunk_id);
3009
- return {
3010
- syncSessionId: resumeSyncSessionId,
3011
- schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
3012
- chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
3013
- chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
3014
- chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
3015
- acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
3016
- supportsCompression: true,
3017
- acceptedFamilies: safeJsonParse(existing.requested_families_json, parsed.requestedFamilies),
3018
- receivedChunkIds
3019
- };
3037
+ const existingFamilies = safeJsonParse(existing.requested_families_json, []);
3038
+ const canResume = existing.schema_version === HEALTH_MOBILE_SYNC_SCHEMA_VERSION &&
3039
+ parsed.requestedFamilies.every((family) => existingFamilies.includes(family));
3040
+ if (canResume) {
3041
+ const receivedChunkIds = getDatabase()
3042
+ .prepare(`SELECT chunk_id
3043
+ FROM health_mobile_sync_chunks
3044
+ WHERE sync_session_id = ?
3045
+ ORDER BY sequence ASC`)
3046
+ .all(resumeSyncSessionId)
3047
+ .map((row) => row.chunk_id);
3048
+ return {
3049
+ syncSessionId: resumeSyncSessionId,
3050
+ schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
3051
+ chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
3052
+ chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
3053
+ chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
3054
+ acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
3055
+ supportsCompression: true,
3056
+ acceptedFamilies: existingFamilies,
3057
+ receivedChunkIds
3058
+ };
3059
+ }
3060
+ getDatabase()
3061
+ .prepare(`UPDATE health_mobile_sync_sessions
3062
+ SET status = 'aborted', aborted_at = ?, updated_at = ?
3063
+ WHERE id = ?`)
3064
+ .run(nowIso(), nowIso(), existing.id);
3020
3065
  }
3021
3066
  }
3022
3067
  const now = nowIso();
@@ -3082,7 +3127,8 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
3082
3127
  actualBytes: actualByteCount
3083
3128
  });
3084
3129
  }
3085
- if (wirePayload.mode !== "legacy_payload_object" && parsed.byteCount !== actualByteCount) {
3130
+ if (wirePayload.mode !== "legacy_payload_object" &&
3131
+ parsed.byteCount !== actualByteCount) {
3086
3132
  console.warn("[healthkit-sync] chunk byte count mismatch", {
3087
3133
  syncSessionId,
3088
3134
  chunkId: parsed.chunkId,
@@ -3159,8 +3205,9 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
3159
3205
  serverChecksum,
3160
3206
  mode: wirePayload.mode
3161
3207
  }), now, now, now, now);
3162
- if (parsed.family === "workout_summaries") {
3163
- applyWorkoutSummaryChunkImmediately(session, wirePayload.payload.workouts ?? []);
3208
+ if (parsed.family === "workout_summaries" ||
3209
+ parsed.family === "workout_archive") {
3210
+ applyWorkoutChunkImmediately(session, parsed.family, wirePayload.payload.workouts ?? []);
3164
3211
  }
3165
3212
  const progress = updateMobileSyncSessionProgress(syncSessionId);
3166
3213
  return {
@@ -3478,7 +3525,8 @@ export function ingestMobileHealthSync(payload) {
3478
3525
  for (const sleep of normalizedSleepNights) {
3479
3526
  const targetSessionId = sleepSessionsByLocalDate
3480
3527
  .get(sleep.localDateKey)
3481
- ?.find((session) => session.startedAt === sleep.startedAt && session.endedAt === sleep.endedAt)?.id ?? null;
3528
+ ?.find((session) => session.startedAt === sleep.startedAt &&
3529
+ session.endedAt === sleep.endedAt)?.id ?? null;
3482
3530
  if (targetSessionId) {
3483
3531
  replaceHistoricalSleepSessionsForDate(pairing.user_id, sleep.localDateKey, targetSessionId);
3484
3532
  }
@@ -3566,7 +3614,9 @@ export function ingestMobileHealthSync(payload) {
3566
3614
  screenTimeDaySummaries: parsed.screenTime.daySummaries.length,
3567
3615
  screenTimeHourlySegments: parsed.screenTime.hourlySegments.length,
3568
3616
  createdCount: createdCount + movementSync.createdCount,
3569
- updatedCount: updatedCount + movementSync.updatedCount + screenTimeSync.updatedCount,
3617
+ updatedCount: updatedCount +
3618
+ movementSync.updatedCount +
3619
+ screenTimeSync.updatedCount,
3570
3620
  mergedCount
3571
3621
  }
3572
3622
  });
@@ -3582,8 +3632,12 @@ export function ingestMobileHealthSync(payload) {
3582
3632
  workouts: parsed.workouts.length,
3583
3633
  vitalsDaySummaries: parsed.vitals.daySummaries.length,
3584
3634
  vitalsMetricEntries: vitalMetricEntries,
3585
- createdCount: createdCount + movementSync.createdCount + screenTimeSync.createdCount,
3586
- updatedCount: updatedCount + movementSync.updatedCount + screenTimeSync.updatedCount,
3635
+ createdCount: createdCount +
3636
+ movementSync.createdCount +
3637
+ screenTimeSync.createdCount,
3638
+ updatedCount: updatedCount +
3639
+ movementSync.updatedCount +
3640
+ screenTimeSync.updatedCount,
3587
3641
  mergedCount,
3588
3642
  movementStays: parsed.movement.stays.length,
3589
3643
  movementTrips: parsed.movement.trips.length,
@@ -3620,7 +3674,7 @@ export function getCompanionOverview(userIds) {
3620
3674
  WHERE user_id IN (${userIds.map(() => "?").join(",")})`
3621
3675
  : `SELECT COUNT(*) AS count FROM health_sleep_raw_logs`)
3622
3676
  .get(...(userIds ?? []));
3623
- const workouts = listWorkoutRows(userIds).map(mapWorkoutSession);
3677
+ const workouts = listWorkoutRows(userIds).map((row) => mapWorkoutSession(row));
3624
3678
  const vitalsRows = listDailySummaryRows("vitals", userIds);
3625
3679
  const vitalsMetricEntries = vitalsRows.reduce((sum, row) => {
3626
3680
  const metrics = safeJsonParse(row.metrics_json, {});
@@ -3677,10 +3731,12 @@ export function getCompanionOverview(userIds) {
3677
3731
  return (session.links.length > 0 ||
3678
3732
  (typeof annotations.qualitySummary === "string" &&
3679
3733
  annotations.qualitySummary.length > 0) ||
3680
- (typeof annotations.notes === "string" && annotations.notes.length > 0) ||
3734
+ (typeof annotations.notes === "string" &&
3735
+ annotations.notes.length > 0) ||
3681
3736
  tags.length > 0);
3682
3737
  }).length,
3683
- linkedWorkouts: workouts.filter((session) => session.links.length > 0).length,
3738
+ linkedWorkouts: workouts.filter((session) => session.links.length > 0)
3739
+ .length,
3684
3740
  habitGeneratedWorkouts: workouts.filter((session) => session.sourceType === "habit_generated").length,
3685
3741
  reconciledWorkouts: workouts.filter((session) => session.reconciliationStatus === "merged").length,
3686
3742
  vitalsDaySummaries: vitalsRows.length,
@@ -3740,9 +3796,9 @@ export function getSleepViewData(userIds) {
3740
3796
  ? session.asleepSeconds / session.timeInBedSeconds
3741
3797
  : 0)), 2),
3742
3798
  averageRestorativeShare: round(average(weekly.map((session) => sleepRestorativeShare(session))), 2),
3743
- reflectiveNightCount: weekly.filter((session) => sleepHasReflection(session))
3799
+ reflectiveNightCount: weekly.filter((session) => sleepHasReflection(session)).length,
3800
+ linkedNightCount: weekly.filter((session) => session.links.length > 0)
3744
3801
  .length,
3745
- linkedNightCount: weekly.filter((session) => session.links.length > 0).length,
3746
3802
  averageBedtimeConsistencyMinutes: Math.round(average(weekly
3747
3803
  .map((session) => session.bedtimeConsistencyMinutes)
3748
3804
  .filter((value) => value !== null))),
@@ -3771,8 +3827,10 @@ export function getSleepViewData(userIds) {
3771
3827
  .map((session) => ({
3772
3828
  id: session.id,
3773
3829
  dateKey: session.localDateKey,
3774
- onsetHour: getTimeZoneParts(session.startedAt, session.sourceTimezone).hour,
3775
- wakeHour: getTimeZoneParts(session.endedAt, session.sourceTimezone).hour,
3830
+ onsetHour: getTimeZoneParts(session.startedAt, session.sourceTimezone)
3831
+ .hour,
3832
+ wakeHour: getTimeZoneParts(session.endedAt, session.sourceTimezone)
3833
+ .hour,
3776
3834
  sleepHours: Number((session.asleepSeconds / 3600).toFixed(2))
3777
3835
  }))
3778
3836
  .reverse(),
@@ -3844,9 +3902,17 @@ function buildFitnessVitalsTrend(rows) {
3844
3902
  }));
3845
3903
  }
3846
3904
  export function getFitnessViewData(userIds) {
3847
- const workouts = listWorkoutRows(userIds).map(mapWorkoutSession);
3905
+ const workoutRows = listWorkoutRows(userIds);
3906
+ const recent = workoutRows
3907
+ .slice(0, 40)
3908
+ .map((row) => mapWorkoutSession(row, { includeAnalytics: true }));
3909
+ const browserSessions = workoutRows
3910
+ .slice(0, 2000)
3911
+ .map((row, index) => mapWorkoutSession(row, { includeAnalytics: index < 40 }));
3912
+ const analysisSessions = workoutRows
3913
+ .slice(0, 500)
3914
+ .map((row) => mapWorkoutSession(row, { includeAnalytics: true }));
3848
3915
  const vitalsTrend = buildFitnessVitalsTrend(listDailySummaryRows("vitals", userIds));
3849
- const recent = workouts.slice(0, 40);
3850
3916
  const weekly = recent.filter((session) => Date.now() - Date.parse(session.startedAt) <= 7 * 24 * 60 * 60 * 1000);
3851
3917
  const weeklyVolumeSeconds = weekly.reduce((sum, session) => sum + session.durationSeconds, 0);
3852
3918
  const exerciseMinutes = weekly.reduce((sum, session) => sum + (session.exerciseMinutes ?? session.durationSeconds / 60), 0);
@@ -3914,14 +3980,14 @@ export function getFitnessViewData(userIds) {
3914
3980
  averageEffort: round(average(recent
3915
3981
  .map((session) => session.subjectiveEffort)
3916
3982
  .filter((value) => value !== null)), 1),
3917
- linkedSessionCount: recent.filter((session) => session.links.length > 0).length,
3983
+ linkedSessionCount: recent.filter((session) => session.links.length > 0)
3984
+ .length,
3918
3985
  plannedSessionCount: recent.filter((session) => session.plannedContext.trim().length > 0).length,
3919
3986
  importedSessionCount: recent.filter((session) => session.source === "apple_health").length,
3920
3987
  habitGeneratedSessionCount: recent.filter((session) => session.sourceType === "habit_generated").length,
3921
3988
  reconciledSessionCount: recent.filter((session) => session.reconciliationStatus === "merged").length,
3922
3989
  topWorkoutType: orderedWorkoutTypes[0]?.[0] ?? null,
3923
- topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])
3924
- ?.workoutTypeLabel ?? null,
3990
+ topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])?.workoutTypeLabel ?? null,
3925
3991
  streakDays: Array.from(new Set(weekly.map((session) => dayKey(session.startedAt)))).length,
3926
3992
  averageHeartRateCoverage: heartRateCoverageCount > 0
3927
3993
  ? Number((heartRateCoverageSum / heartRateCoverageCount).toFixed(3))
@@ -3942,17 +4008,16 @@ export function getFitnessViewData(userIds) {
3942
4008
  energyKcal: Math.round(session.totalEnergyKcal ?? session.activeEnergyKcal ?? 0),
3943
4009
  zoneDurations: session.analytics
3944
4010
  ?.zoneDurations ?? [],
3945
- trainingLoad: (session.analytics
3946
- ?.load?.trimp ?? null),
3947
- heartRateCoverage: (session.analytics?.dataQuality?.sampleCoverage ?? 0)
4011
+ trainingLoad: session.analytics?.load?.trimp ?? null,
4012
+ heartRateCoverage: session.analytics?.dataQuality?.sampleCoverage ?? 0
3948
4013
  }))
3949
4014
  .reverse(),
3950
4015
  typeBreakdown: orderedWorkoutTypes.map(([workoutType, metrics]) => ({
3951
4016
  workoutType,
3952
- workoutTypeLabel: recent.find((session) => session.workoutType === workoutType)?.workoutTypeLabel ??
3953
- workoutType,
3954
- activityFamily: recent.find((session) => session.workoutType === workoutType)?.activityFamily ??
3955
- "other",
4017
+ workoutTypeLabel: recent.find((session) => session.workoutType === workoutType)
4018
+ ?.workoutTypeLabel ?? workoutType,
4019
+ activityFamily: recent.find((session) => session.workoutType === workoutType)
4020
+ ?.activityFamily ?? "other",
3956
4021
  activityFamilyLabel: recent.find((session) => session.workoutType === workoutType)
3957
4022
  ?.activityFamilyLabel ?? "Other",
3958
4023
  sessionCount: metrics.sessionCount,
@@ -3960,8 +4025,8 @@ export function getFitnessViewData(userIds) {
3960
4025
  energyKcal: metrics.energyKcal
3961
4026
  })),
3962
4027
  vitalsTrend,
3963
- analysisSessions: workouts,
3964
- sessions: recent
4028
+ analysisSessions,
4029
+ sessions: browserSessions
3965
4030
  };
3966
4031
  }
3967
4032
  function averageNullable(values) {
@@ -3970,7 +4035,9 @@ function averageNullable(values) {
3970
4035
  }
3971
4036
  function sumNullable(values) {
3972
4037
  const present = values.filter((value) => value != null);
3973
- return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : null;
4038
+ return present.length > 0
4039
+ ? present.reduce((sum, value) => sum + value, 0)
4040
+ : null;
3974
4041
  }
3975
4042
  function maxNullable(values) {
3976
4043
  const present = values.filter((value) => value != null);
@@ -4047,7 +4114,9 @@ export function getVitalsViewData(userIds) {
4047
4114
  }))
4048
4115
  .filter((value) => value != null);
4049
4116
  const baselineValues = recentValues.slice(Math.max(0, recentValues.length - 8), recentValues.length - 1);
4050
- const baselineValue = baselineValues.length > 0 ? average(baselineValues) : recentValues.at(-2) ?? null;
4117
+ const baselineValue = baselineValues.length > 0
4118
+ ? average(baselineValues)
4119
+ : (recentValues.at(-2) ?? null);
4051
4120
  const latestValue = latestDay
4052
4121
  ? vitalMetricPrimaryValue({
4053
4122
  aggregation: bucket.aggregation,
@@ -4063,9 +4132,13 @@ export function getVitalsViewData(userIds) {
4063
4132
  category: bucket.category,
4064
4133
  unit: bucket.displayUnit,
4065
4134
  aggregation: bucket.aggregation,
4066
- latestValue: latestValue == null ? null : round(latestValue, bucket.aggregation === "cumulative" ? 0 : 1),
4135
+ latestValue: latestValue == null
4136
+ ? null
4137
+ : round(latestValue, bucket.aggregation === "cumulative" ? 0 : 1),
4067
4138
  latestDateKey: latestDay?.dateKey ?? null,
4068
- baselineValue: baselineValue == null ? null : round(baselineValue, bucket.aggregation === "cumulative" ? 0 : 1),
4139
+ baselineValue: baselineValue == null
4140
+ ? null
4141
+ : round(baselineValue, bucket.aggregation === "cumulative" ? 0 : 1),
4069
4142
  deltaValue: latestValue != null && baselineValue != null
4070
4143
  ? round(latestValue - baselineValue, bucket.aggregation === "cumulative" ? 0 : 1)
4071
4144
  : null,
@@ -4079,7 +4152,9 @@ export function getVitalsViewData(userIds) {
4079
4152
  }
4080
4153
  return left.category.localeCompare(right.category);
4081
4154
  });
4082
- const categoryBreakdown = [...new Set(metrics.map((metric) => metric.category))]
4155
+ const categoryBreakdown = [
4156
+ ...new Set(metrics.map((metric) => metric.category))
4157
+ ]
4083
4158
  .map((category) => {
4084
4159
  const categoryMetrics = metrics.filter((metric) => metric.category === category);
4085
4160
  return {
@@ -4094,8 +4169,7 @@ export function getVitalsViewData(userIds) {
4094
4169
  trackedDays: dayCount,
4095
4170
  metricCount: metrics.length,
4096
4171
  latestDateKey: rows[0]?.date_key ?? null,
4097
- latestMetricCount: metrics.filter((metric) => metric.latestDateKey === (rows[0]?.date_key ?? null))
4098
- .length,
4172
+ latestMetricCount: metrics.filter((metric) => metric.latestDateKey === (rows[0]?.date_key ?? null)).length,
4099
4173
  categoryBreakdown
4100
4174
  },
4101
4175
  metrics
@@ -4147,7 +4221,8 @@ export function createSleepSession(input, activity) {
4147
4221
  provenance_json, derived_json, created_at, updated_at
4148
4222
  )
4149
4223
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
4150
- .run(id, externalUid, null, userId, parsed.source, parsed.sourceType, parsed.sourceDevice, sourceTimezone, parsed.localDateKey ?? localDateKeyForTimezone(parsed.endedAt, sourceTimezone), parsed.startedAt, parsed.endedAt, timeInBedSeconds, asleepSeconds, awakeSeconds, sleepScore, timingMetrics.regularityScore, timingMetrics.bedtimeConsistencyMinutes, timingMetrics.wakeConsistencyMinutes, parsed.rawSegmentCount, JSON.stringify(parsed.stageBreakdown), JSON.stringify(parsed.recoveryMetrics), JSON.stringify(parsed.sourceMetrics), JSON.stringify(parsed.links), JSON.stringify(annotations), JSON.stringify({
4224
+ .run(id, externalUid, null, userId, parsed.source, parsed.sourceType, parsed.sourceDevice, sourceTimezone, parsed.localDateKey ??
4225
+ localDateKeyForTimezone(parsed.endedAt, sourceTimezone), parsed.startedAt, parsed.endedAt, timeInBedSeconds, asleepSeconds, awakeSeconds, sleepScore, timingMetrics.regularityScore, timingMetrics.bedtimeConsistencyMinutes, timingMetrics.wakeConsistencyMinutes, parsed.rawSegmentCount, JSON.stringify(parsed.stageBreakdown), JSON.stringify(parsed.recoveryMetrics), JSON.stringify(parsed.sourceMetrics), JSON.stringify(parsed.links), JSON.stringify(annotations), JSON.stringify({
4151
4226
  manualEntry: true,
4152
4227
  entryMode: "local",
4153
4228
  source: parsed.source,
@@ -4157,7 +4232,8 @@ export function createSleepSession(input, activity) {
4157
4232
  createdAt: now,
4158
4233
  ...parsed.provenance
4159
4234
  }), JSON.stringify(derived), now, now);
4160
- summarizeUserHealthDay(userId, parsed.localDateKey ?? localDateKeyForTimezone(parsed.endedAt, sourceTimezone));
4235
+ summarizeUserHealthDay(userId, parsed.localDateKey ??
4236
+ localDateKeyForTimezone(parsed.endedAt, sourceTimezone));
4161
4237
  recordActivityEvent({
4162
4238
  entityType: "sleep_session",
4163
4239
  entityId: id,
@@ -4374,9 +4450,7 @@ export function updateWorkoutSession(workoutId, patch, activity) {
4374
4450
  ? durationSecondsBetween(startedAt, endedAt)
4375
4451
  : current.duration_seconds;
4376
4452
  const currentAnnotations = safeJsonParse(current.annotations_json, {});
4377
- const tags = parsed.tags ??
4378
- safeJsonParse(current.tags_json, []) ??
4379
- [];
4453
+ const tags = parsed.tags ?? safeJsonParse(current.tags_json, []) ?? [];
4380
4454
  const links = parsed.links ??
4381
4455
  safeJsonParse(current.links_json, []);
4382
4456
  const annotations = {
@@ -4512,7 +4586,9 @@ export function updateWorkoutMetadata(workoutId, patch, activity) {
4512
4586
  ...(parsed.subjectiveEffort !== undefined
4513
4587
  ? { subjectiveEffort: parsed.subjectiveEffort }
4514
4588
  : {}),
4515
- ...(parsed.moodBefore !== undefined ? { moodBefore: parsed.moodBefore } : {}),
4589
+ ...(parsed.moodBefore !== undefined
4590
+ ? { moodBefore: parsed.moodBefore }
4591
+ : {}),
4516
4592
  ...(parsed.moodAfter !== undefined ? { moodAfter: parsed.moodAfter } : {}),
4517
4593
  ...(parsed.meaningText !== undefined
4518
4594
  ? { meaningText: parsed.meaningText }