forge-openclaw-plugin 0.2.92 → 0.2.94

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.
Files changed (28) hide show
  1. package/dist/assets/{board-DKxKOwax.js → board-D1HbyD4u.js} +1 -1
  2. package/dist/assets/{index-BNvUaA6y.js → index-DWZd3qT-.js} +44 -44
  3. package/dist/assets/index-PA_Ih223.css +1 -0
  4. package/dist/assets/{motion-CM4AfIqo.js → motion-D2OqILg_.js} +1 -1
  5. package/dist/assets/{table-BUeQ9wzR.js → table-YWWjPjC_.js} +1 -1
  6. package/dist/assets/{ui-3Wd4pVaA.js → ui-DikPZj8S.js} +1 -1
  7. package/dist/assets/vendor-BS9OPVNh.js +2181 -0
  8. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  9. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  11. package/dist/index.html +7 -7
  12. package/dist/openclaw/parity.js +1 -0
  13. package/dist/openclaw/routes.js +5 -0
  14. package/dist/openclaw/tools.js +7 -0
  15. package/dist/server/server/migrations/064_health_workout_time_series_identity.sql +161 -0
  16. package/dist/server/server/src/app.js +72 -2
  17. package/dist/server/server/src/health-workout-analytics.js +8 -2
  18. package/dist/server/server/src/health.js +337 -2
  19. package/dist/server/server/src/openapi.js +59 -0
  20. package/dist/server/src/lib/api.js +6 -0
  21. package/openclaw.plugin.json +2 -1
  22. package/package.json +1 -1
  23. package/server/migrations/064_health_workout_time_series_identity.sql +161 -0
  24. package/skills/forge-openclaw/SKILL.md +25 -7
  25. package/skills/forge-openclaw/entity_conversation_playbooks.md +88 -12
  26. package/skills/forge-openclaw/psyche_entity_playbooks.md +35 -0
  27. package/dist/assets/index-NqIbz_lv.css +0 -1
  28. package/dist/assets/vendor-BVU0cZC9.js +0 -2171
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-BNvUaA6y.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-BVU0cZC9.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/board-DKxKOwax.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-3Wd4pVaA.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-CM4AfIqo.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/table-BUeQ9wzR.js">
16
+ <script type="module" crossorigin src="/forge/assets/index-DWZd3qT-.js"></script>
17
+ <link rel="modulepreload" crossorigin href="/forge/assets/vendor-BS9OPVNh.js">
18
+ <link rel="modulepreload" crossorigin href="/forge/assets/board-D1HbyD4u.js">
19
+ <link rel="modulepreload" crossorigin href="/forge/assets/ui-DikPZj8S.js">
20
+ <link rel="modulepreload" crossorigin href="/forge/assets/motion-D2OqILg_.js">
21
+ <link rel="modulepreload" crossorigin href="/forge/assets/table-YWWjPjC_.js">
22
22
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-B-Lq_OG3.css">
23
- <link rel="stylesheet" crossorigin href="/forge/assets/index-NqIbz_lv.css">
23
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-PA_Ih223.css">
24
24
  </head>
25
25
  <body class="bg-canvas text-ink antialiased">
26
26
  <div id="root"></div>
@@ -30,6 +30,7 @@ export const FORGE_SUPPORTED_PLUGIN_API_ROUTES = [
30
30
  { method: "GET", path: "/api/v1/health/sleep", purpose: "health" },
31
31
  { method: "PATCH", path: "/api/v1/health/sleep/:id", purpose: "health" },
32
32
  { method: "GET", path: "/api/v1/health/fitness", purpose: "health" },
33
+ { method: "GET", path: "/api/v1/health/training-load", purpose: "health" },
33
34
  { method: "PATCH", path: "/api/v1/health/workouts/:id", purpose: "health" },
34
35
  { method: "GET", path: "/api/v1/movement/day", purpose: "movement" },
35
36
  { method: "GET", path: "/api/v1/movement/month", purpose: "movement" },
@@ -176,6 +176,11 @@ export const FORGE_PLUGIN_ROUTE_GROUPS = [
176
176
  upstreamPath: "/api/v1/health/fitness",
177
177
  target: (_match, url) => passthroughSearch("/api/v1/health/fitness", url)
178
178
  }),
179
+ exact("/forge/v1/health/training-load", {
180
+ method: "GET",
181
+ upstreamPath: "/api/v1/health/training-load",
182
+ target: (_match, url) => passthroughSearch("/api/v1/health/training-load", url)
183
+ }),
179
184
  {
180
185
  path: "/forge/v1/movement",
181
186
  match: "prefix",
@@ -752,6 +752,13 @@ export function registerForgePluginTools(api, config) {
752
752
  parameters: scopedReadSchema,
753
753
  path: (params) => withUserIds("/api/v1/health/fitness", params.userIds)
754
754
  });
755
+ registerReadTool(api, config, {
756
+ name: "forge_get_training_load_overview",
757
+ label: "Forge Training Load Overview",
758
+ description: "Read the cardiovascular training-load surface with acute/chronic load, HR zone distribution, weekly intensity targets, and data-quality flags.",
759
+ parameters: scopedReadSchema,
760
+ path: (params) => withUserIds("/api/v1/health/training-load", params.userIds)
761
+ });
755
762
  api.registerTool({
756
763
  name: "forge_update_sleep_session",
757
764
  label: "Forge Update Sleep Session",
@@ -0,0 +1,161 @@
1
+ CREATE TEMP TABLE health_workout_time_series_dedupe AS
2
+ SELECT
3
+ workout_id,
4
+ metric_key,
5
+ source_sample_uid,
6
+ MIN(rowid) AS survivor_rowid,
7
+ (
8
+ SELECT latest.rowid
9
+ FROM health_workout_time_series AS latest
10
+ WHERE latest.workout_id = grouped.workout_id
11
+ AND latest.metric_key = grouped.metric_key
12
+ AND latest.source_sample_uid = grouped.source_sample_uid
13
+ ORDER BY latest.updated_at DESC, latest.created_at DESC, latest.rowid DESC
14
+ LIMIT 1
15
+ ) AS latest_rowid,
16
+ (
17
+ SELECT latest.series_index
18
+ FROM health_workout_time_series AS latest
19
+ WHERE latest.workout_id = grouped.workout_id
20
+ AND latest.metric_key = grouped.metric_key
21
+ AND latest.source_sample_uid = grouped.source_sample_uid
22
+ ORDER BY latest.updated_at DESC, latest.created_at DESC, latest.rowid DESC
23
+ LIMIT 1
24
+ ) AS latest_series_index
25
+ FROM health_workout_time_series AS grouped
26
+ WHERE source_sample_uid IS NOT NULL
27
+ AND source_sample_uid != ''
28
+ GROUP BY workout_id, metric_key, source_sample_uid
29
+ HAVING COUNT(*) > 1;
30
+
31
+ UPDATE health_workout_time_series
32
+ SET
33
+ label = (
34
+ SELECT latest.label
35
+ FROM health_workout_time_series AS latest
36
+ JOIN health_workout_time_series_dedupe AS dedupe
37
+ ON dedupe.latest_rowid = latest.rowid
38
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
39
+ ),
40
+ category = (
41
+ SELECT latest.category
42
+ FROM health_workout_time_series AS latest
43
+ JOIN health_workout_time_series_dedupe AS dedupe
44
+ ON dedupe.latest_rowid = latest.rowid
45
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
46
+ ),
47
+ unit = (
48
+ SELECT latest.unit
49
+ FROM health_workout_time_series AS latest
50
+ JOIN health_workout_time_series_dedupe AS dedupe
51
+ ON dedupe.latest_rowid = latest.rowid
52
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
53
+ ),
54
+ value = (
55
+ SELECT latest.value
56
+ FROM health_workout_time_series AS latest
57
+ JOIN health_workout_time_series_dedupe AS dedupe
58
+ ON dedupe.latest_rowid = latest.rowid
59
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
60
+ ),
61
+ started_at = (
62
+ SELECT latest.started_at
63
+ FROM health_workout_time_series AS latest
64
+ JOIN health_workout_time_series_dedupe AS dedupe
65
+ ON dedupe.latest_rowid = latest.rowid
66
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
67
+ ),
68
+ ended_at = (
69
+ SELECT latest.ended_at
70
+ FROM health_workout_time_series AS latest
71
+ JOIN health_workout_time_series_dedupe AS dedupe
72
+ ON dedupe.latest_rowid = latest.rowid
73
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
74
+ ),
75
+ source_device = (
76
+ SELECT latest.source_device
77
+ FROM health_workout_time_series AS latest
78
+ JOIN health_workout_time_series_dedupe AS dedupe
79
+ ON dedupe.latest_rowid = latest.rowid
80
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
81
+ ),
82
+ source_bundle_identifier = (
83
+ SELECT latest.source_bundle_identifier
84
+ FROM health_workout_time_series AS latest
85
+ JOIN health_workout_time_series_dedupe AS dedupe
86
+ ON dedupe.latest_rowid = latest.rowid
87
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
88
+ ),
89
+ source_product_type = (
90
+ SELECT latest.source_product_type
91
+ FROM health_workout_time_series AS latest
92
+ JOIN health_workout_time_series_dedupe AS dedupe
93
+ ON dedupe.latest_rowid = latest.rowid
94
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
95
+ ),
96
+ capture_method = (
97
+ SELECT latest.capture_method
98
+ FROM health_workout_time_series AS latest
99
+ JOIN health_workout_time_series_dedupe AS dedupe
100
+ ON dedupe.latest_rowid = latest.rowid
101
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
102
+ ),
103
+ quality_flags_json = (
104
+ SELECT latest.quality_flags_json
105
+ FROM health_workout_time_series AS latest
106
+ JOIN health_workout_time_series_dedupe AS dedupe
107
+ ON dedupe.latest_rowid = latest.rowid
108
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
109
+ ),
110
+ metadata_json = (
111
+ SELECT latest.metadata_json
112
+ FROM health_workout_time_series AS latest
113
+ JOIN health_workout_time_series_dedupe AS dedupe
114
+ ON dedupe.latest_rowid = latest.rowid
115
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
116
+ ),
117
+ provenance_json = (
118
+ SELECT latest.provenance_json
119
+ FROM health_workout_time_series AS latest
120
+ JOIN health_workout_time_series_dedupe AS dedupe
121
+ ON dedupe.latest_rowid = latest.rowid
122
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
123
+ ),
124
+ updated_at = (
125
+ SELECT latest.updated_at
126
+ FROM health_workout_time_series AS latest
127
+ JOIN health_workout_time_series_dedupe AS dedupe
128
+ ON dedupe.latest_rowid = latest.rowid
129
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
130
+ )
131
+ WHERE rowid IN (
132
+ SELECT survivor_rowid
133
+ FROM health_workout_time_series_dedupe
134
+ );
135
+
136
+ DELETE FROM health_workout_time_series
137
+ WHERE rowid IN (
138
+ SELECT duplicate.rowid
139
+ FROM health_workout_time_series AS duplicate
140
+ JOIN health_workout_time_series_dedupe AS dedupe
141
+ ON dedupe.workout_id = duplicate.workout_id
142
+ AND dedupe.metric_key = duplicate.metric_key
143
+ AND dedupe.source_sample_uid = duplicate.source_sample_uid
144
+ WHERE duplicate.rowid != dedupe.survivor_rowid
145
+ );
146
+
147
+ UPDATE health_workout_time_series
148
+ SET series_index = (
149
+ SELECT dedupe.latest_series_index
150
+ FROM health_workout_time_series_dedupe AS dedupe
151
+ WHERE dedupe.survivor_rowid = health_workout_time_series.rowid
152
+ )
153
+ WHERE rowid IN (
154
+ SELECT survivor_rowid
155
+ FROM health_workout_time_series_dedupe
156
+ );
157
+
158
+ DROP TABLE health_workout_time_series_dedupe;
159
+
160
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_health_workout_time_series_sample_identity
161
+ ON health_workout_time_series(workout_id, metric_key, source_sample_uid);
@@ -66,7 +66,7 @@ import { registerWebRoutes } from "./web.js";
66
66
  import { createManagerRuntime } from "./managers/runtime.js";
67
67
  import { isManagerError } from "./managers/type-guards.js";
68
68
  import { buildCompanionPairingTransport, getCompanionIrohStatus, stopCompanionIroh } from "./services/companion-iroh.js";
69
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getHealthZoneProfileForUser, getMobileHealthSyncSessionStatus, getWorkoutSessionById, getWorkoutSessionDetailById, heartbeatCompanionPairing, heartbeatCompanionPairingSchema, healthZoneProfilePatchSchema, abortMobileHealthSyncSession, completeMobileHealthSyncSession, ingestMobileHealthSync, ingestMobileHealthSyncChunk, mobileHealthSyncChunkSchema, mobileHealthSyncSessionCompleteSchema, mobileHealthSyncSessionStartSchema, mobileHealthSyncSchema, patchHealthZoneProfileForUser, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, startMobileHealthSyncSession, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
69
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getTrainingLoadViewData, getVitalsViewData, getHealthZoneProfileForUser, getMobileHealthSyncSessionStatus, getWorkoutSessionById, getWorkoutSessionDetailById, heartbeatCompanionPairing, heartbeatCompanionPairingSchema, healthZoneProfilePatchSchema, abortMobileHealthSyncSession, completeMobileHealthSyncSession, ingestMobileHealthSync, ingestMobileHealthSyncChunk, mobileHealthSyncChunkSchema, mobileHealthSyncSessionCompleteSchema, mobileHealthSyncSessionStartSchema, mobileHealthSyncSchema, patchHealthZoneProfileForUser, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, startMobileHealthSyncSession, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
70
70
  import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
71
71
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
72
72
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
@@ -2246,6 +2246,8 @@ function buildPreferredMutationPath(entityType) {
2246
2246
  return "Read-only surface. Use batch CRUD for sleep_session records or the review enrichment route for reflective notes.";
2247
2247
  case "sports_overview":
2248
2248
  return "Read-only surface. Use batch CRUD for workout_session records or the review enrichment route for reflective notes.";
2249
+ case "training_load":
2250
+ return "Read-only surface. Use it for cardiovascular load, HR zones, acute/chronic stress, VO2max context, and target analysis; use batch CRUD for underlying workout_session records.";
2249
2251
  default:
2250
2252
  return "Read-only surface.";
2251
2253
  }
@@ -2317,6 +2319,8 @@ function buildPreferredReadPath(entityType) {
2317
2319
  return "/api/v1/health/sleep";
2318
2320
  case "sports_overview":
2319
2321
  return "/api/v1/health/fitness";
2322
+ case "training_load":
2323
+ return "/api/v1/health/training-load";
2320
2324
  default:
2321
2325
  return null;
2322
2326
  }
@@ -3037,6 +3041,19 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
3037
3041
  "Read this surface before suggesting workout reflections or recovery follow-up."
3038
3042
  ],
3039
3043
  fieldGuide: []
3044
+ }),
3045
+ enrichOnboardingEntityGuide({
3046
+ entityType: "training_load",
3047
+ purpose: "The read-model cardiovascular training-load workspace for acute/chronic load, HR zone distribution, intensity targets, and VO2max context.",
3048
+ minimumCreateFields: [],
3049
+ relationshipRules: [
3050
+ "Use this surface for review and interpretation.",
3051
+ "Create, update, delete, or search the underlying workout_session records through batch CRUD by default."
3052
+ ],
3053
+ searchHints: [
3054
+ "Read this surface before advising on high-intensity balance, recovery load, or cardiovascular training targets."
3055
+ ],
3056
+ fieldGuide: []
3040
3057
  })
3041
3058
  ];
3042
3059
  const AGENT_ONBOARDING_CONVERSATION_RULES = [
@@ -3060,6 +3077,8 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
3060
3077
  "For direct update or review requests, the next question should usually narrow the saved object, timeframe, or route family instead of reopening the whole meaning-making arc.",
3061
3078
  "For updates, start with the smallest thing that now feels wrong, newly true, or newly visible rather than restarting the whole story.",
3062
3079
  "For review requests, ask what practical question the user wants the read to answer before you ask for more scope.",
3080
+ "Treat userId, owner, and human/bot assignees as accountability and scope, not as opening form fields. Ask whose record or owner scope matters only when it changes visibility, review results, collaboration, automation behavior, or later filtering.",
3081
+ "For read and overview requests, ask for human or bot user scope only when the answer would meaningfully differ across owners; otherwise keep the next question focused on the user's practical question.",
3063
3082
  "The opening question should help the user understand what they are actually trying to save, decide, review, or change, not make them perform the schema out loud.",
3064
3083
  "If the user already named the exact correction in usable language, confirm only the missing scope, timing, or route-selecting detail that still matters, then act.",
3065
3084
  "Keep API and architecture nouns out of user-facing questions unless the user asks about implementation. Do not ask the user about surfaces, route families, CRUD, payloads, mutation paths, or read paths; ask about the human object such as a wiki page, note, trigger report, behavior pattern, belief, mode, movement timeline, energy model, weekday pattern, flow, run, or node result.",
@@ -3080,7 +3099,7 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
3080
3099
  "Once the route family is clear, say it plainly enough that another agent could follow the same path without guessing.",
3081
3100
  "For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved.",
3082
3101
  "For action workflows such as task_run, work_adjustment, questionnaire_run, preference_judgment, preference_signal, and self_observation, keep the question focused on the missing action detail and do not downgrade the request into generic batch CRUD.",
3083
- "For read-model-only health surfaces such as sleep_overview and sports_overview, use the dedicated overview reads first when the user wants review, pattern interpretation, recovery context, or training-load context. Move to sleep_session or workout_session writes only after one specific stored session needs enrichment.",
3102
+ "For read-model-only health surfaces such as sleep_overview, sports_overview, and training_load, use the dedicated overview reads first when the user wants review, pattern interpretation, recovery context, training-load context, or cardiovascular target analysis. Move to sleep_session or workout_session writes only after one specific stored session needs enrichment.",
3084
3103
  "For normal stored Preferences and questionnaire records, use batch CRUD by default; switch to dedicated action routes only for judgments, signals, run answers, clone/draft/publish lifecycle, or visual comparison gameplay.",
3085
3104
  "When the user wants to remember a book, article, paper, source, concept, person, conversation, project reference, recurring explanation, or personal manual, consider wiki_page before note or self_observation.",
3086
3105
  "For meaning-bearing updates, especially in Psyche, briefly say what feels newly true before you ask for the one structural detail that still changes the save."
@@ -3373,6 +3392,18 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3373
3392
  "Move to a workout_session write only when one specific workout needs reflective context, tags, notes, or links."
3374
3393
  ]
3375
3394
  },
3395
+ {
3396
+ focus: "training_load",
3397
+ openingQuestion: "What training decision should the load picture help with right now?",
3398
+ coachingGoal: "Review cardiovascular load, HR zones, acute/chronic stress, high-intensity pressure, VO2max context, and target distribution before suggesting training changes.",
3399
+ askSequence: [
3400
+ "Ask what decision the user wants from the load surface only if it is not already clear.",
3401
+ "Use the dedicated training-load overview before asking the user to reconstruct workout metrics from memory.",
3402
+ "Separate data-quality limits from true physiological interpretation.",
3403
+ "Translate the load pattern into a concrete next training constraint or target.",
3404
+ "Move to a workout_session write only when one specific workout needs reflective context, tags, notes, or links."
3405
+ ]
3406
+ },
3376
3407
  {
3377
3408
  focus: "preference_catalog",
3378
3409
  openingQuestion: "What decision or taste question should this catalog help with?",
@@ -4228,6 +4259,19 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
4228
4259
  ],
4229
4260
  example: '{"userIds":["user_operator"]}'
4230
4261
  },
4262
+ {
4263
+ toolName: "forge_get_training_load_overview",
4264
+ summary: "Read the cardiovascular training-load surface with acute/chronic load, HR zone distribution, weekly intensity targets, and data-quality flags.",
4265
+ whenToUse: "Use when the operator wants to analyze training stress, zone balance, VO2max context, combat-sport load, or what to optimize next.",
4266
+ inputShape: "{ userIds?: string[] }",
4267
+ requiredFields: [],
4268
+ notes: [
4269
+ "The API path is /api/v1/health/training-load and the UI route is /training-load.",
4270
+ "This is a read-model-only surface. Workout records remain ordinary workout_session entities for batch CRUD.",
4271
+ "Forge uses HRR zone analytics and TRIMP-like internal load from stored workout evidence."
4272
+ ],
4273
+ example: '{"userIds":["user_operator"]}'
4274
+ },
4231
4275
  {
4232
4276
  toolName: "forge_update_sleep_session",
4233
4277
  summary: "Patch one sleep session with reflective notes, tags, or linked Forge context.",
@@ -5011,6 +5055,8 @@ function buildAgentOnboardingPayload(request) {
5011
5055
  sleep_overview: "/api/v1/health/sleep",
5012
5056
  sportsOverview: "/api/v1/health/fitness",
5013
5057
  sports_overview: "/api/v1/health/fitness",
5058
+ trainingLoad: "/api/v1/health/training-load",
5059
+ training_load: "/api/v1/health/training-load",
5014
5060
  selfObservation: "/api/v1/psyche/self-observation/calendar",
5015
5061
  self_observation: "/api/v1/psyche/self-observation/calendar",
5016
5062
  calendarOverview: "/api/v1/calendar/overview",
@@ -5120,6 +5166,7 @@ function buildAgentOnboardingPayload(request) {
5120
5166
  weeklyReview: "/api/v1/reviews/weekly",
5121
5167
  sleepOverview: "/api/v1/health/sleep",
5122
5168
  sportsOverview: "/api/v1/health/fitness",
5169
+ trainingLoad: "/api/v1/health/training-load",
5123
5170
  lifeForce: "/api/v1/life-force",
5124
5171
  lifeForceProfile: "/api/v1/life-force/profile",
5125
5172
  lifeForceWeekdayTemplate: "/api/v1/life-force/templates/:weekday",
@@ -5171,6 +5218,7 @@ function buildAgentOnboardingPayload(request) {
5171
5218
  "forge_get_psyche_overview",
5172
5219
  "forge_get_sleep_overview",
5173
5220
  "forge_get_sports_overview",
5221
+ "forge_get_training_load_overview",
5174
5222
  "forge_get_xp_metrics",
5175
5223
  "forge_get_weekly_review"
5176
5224
  ],
@@ -5201,6 +5249,7 @@ function buildAgentOnboardingPayload(request) {
5201
5249
  healthWorkflow: [
5202
5250
  "forge_get_sleep_overview",
5203
5251
  "forge_get_sports_overview",
5252
+ "forge_get_training_load_overview",
5204
5253
  "forge_update_sleep_session",
5205
5254
  "forge_update_workout_session"
5206
5255
  ],
@@ -5268,8 +5317,12 @@ function buildAgentOnboardingPayload(request) {
5268
5317
  movementTripDetail: '{"routeKey":"tripDetail","pathParams":{"id":"trip_123"}}',
5269
5318
  movementSettings: '{"routeKey":"settings","query":{"userIds":["user_operator"]}}',
5270
5319
  movementSettingsUpdate: '{"routeKey":"settingsUpdate","body":{"trackingEnabled":true,"publishMode":"draft_review","retentionMode":"aggregates_only"}}',
5320
+ movementPlaceCreate: '{"routeKey":"placeCreate","body":{"label":"Home","centerLat":46.2044,"centerLon":6.1432,"radiusMeters":120,"userId":"user_operator","note":"Primary home boundary for future time-in-place reads."}}',
5321
+ movementPlaceUpdate: '{"routeKey":"placeUpdate","pathParams":{"id":"place_home"},"body":{"label":"Home office","radiusMeters":90,"note":"Tighten the boundary so clinic visits do not count as home."}}',
5271
5322
  movementMissingStayPreflight: '{"routeKey":"userBoxPreflight","body":{"kind":"stay","startedAt":"2026-05-06T13:00:00.000Z","endedAt":"2026-05-06T15:00:00.000Z","placeLabel":"Home","userId":"user_operator"}}',
5272
5323
  movementMissingStayCreate: '{"routeKey":"userBoxCreate","body":{"kind":"stay","startedAt":"2026-05-06T13:00:00.000Z","endedAt":"2026-05-06T15:00:00.000Z","placeLabel":"Home","userId":"user_operator","note":"Manual correction after reviewing the timeline."}}',
5324
+ movementUserBoxUpdate: '{"routeKey":"userBoxUpdate","pathParams":{"id":"box_manual_123"},"body":{"endedAt":"2026-05-06T15:30:00.000Z","note":"Extended after checking the timeline detail."}}',
5325
+ movementUserBoxDelete: '{"routeKey":"userBoxDelete","pathParams":{"id":"box_manual_123"}}',
5273
5326
  lifeForceOverview: '{"routeKey":"overview"}',
5274
5327
  lifeForceProfile: '{"routeKey":"profile","body":{"baselineDailyAp":24,"recoveryNotes":"Clinic-admin days need a lower expected afternoon load."}}',
5275
5328
  lifeForceWeekdayTemplate: '{"routeKey":"weekdayTemplate","pathParams":{"weekday":"monday"},"body":{"points":[{"hour":13,"freeAp":-4}]}}',
@@ -5280,6 +5333,8 @@ function buildAgentOnboardingPayload(request) {
5280
5333
  workbenchUpdateFlow: '{"routeKey":"updateFlow","pathParams":{"id":"flow_research_digest"},"body":{"description":"Keep the same input contract but add a stronger evidence-check node."}}',
5281
5334
  workbenchDeleteFlow: '{"routeKey":"deleteFlow","pathParams":{"id":"flow_research_digest"}}',
5282
5335
  workbenchRunDetail: '{"routeKey":"runDetail","pathParams":{"id":"flow_research_digest","runId":"run_123"}}',
5336
+ workbenchRunNodes: '{"routeKey":"runNodes","pathParams":{"id":"flow_research_digest","runId":"run_123"}}',
5337
+ workbenchNodeResult: '{"routeKey":"nodeResult","pathParams":{"id":"flow_research_digest","runId":"run_123","nodeId":"node_summary"}}',
5283
5338
  workbenchPublishedOutput: '{"routeKey":"publishedOutput","pathParams":{"id":"flow_research_digest"}}',
5284
5339
  workbenchLatestNodeOutput: '{"routeKey":"latestNodeOutput","pathParams":{"id":"flow_research_digest","nodeId":"node_summary"}}',
5285
5340
  workbenchRunFlow: '{"routeKey":"runFlow","pathParams":{"id":"flow_research_digest"},"body":{"input":{"topic":"question flow quality"}}}',
@@ -6202,6 +6257,16 @@ function compactFitness(fitness) {
6202
6257
  detailRoute: "/api/v1/health/fitness"
6203
6258
  };
6204
6259
  }
6260
+ function compactTrainingLoad(trainingLoad) {
6261
+ return {
6262
+ summary: trainingLoad.summary,
6263
+ intensityDistribution: trainingLoad.recentIntensityDistribution,
6264
+ weeklyLoad: trainingLoad.weeklyLoad.slice(-8),
6265
+ topActivities: trainingLoad.activityBreakdown.slice(0, 6),
6266
+ targetModel: trainingLoad.targetModel,
6267
+ detailRoute: "/api/v1/health/training-load"
6268
+ };
6269
+ }
6205
6270
  function compactVitals(vitals) {
6206
6271
  return {
6207
6272
  summary: vitals.summary,
@@ -6482,6 +6547,7 @@ function buildOperatorOverview(request) {
6482
6547
  const yesterday = compactDailyContext(getTodayContext(addDays(now, -1), { userIds }));
6483
6548
  const sleep = compactSleep(getSleepViewData(userIds));
6484
6549
  const fitness = compactFitness(getFitnessViewData(userIds));
6550
+ const trainingLoad = compactTrainingLoad(getTrainingLoadViewData(userIds));
6485
6551
  const vitals = compactVitals(getVitalsViewData(userIds));
6486
6552
  const lifeForce = compactLifeForce(buildLifeForcePayload(now, userIds));
6487
6553
  const psyche = canReadPsyche ? compactPsyche(getPsycheOverview(userIds)) : null;
@@ -6563,6 +6629,7 @@ function buildOperatorOverview(request) {
6563
6629
  notes,
6564
6630
  sleep,
6565
6631
  fitness,
6632
+ trainingLoad,
6566
6633
  vitals,
6567
6634
  lifeForce,
6568
6635
  domains: listDomains().map((domain) => ({
@@ -7241,6 +7308,9 @@ export async function buildServer(options = {}) {
7241
7308
  app.get("/api/v1/health/fitness", async (request) => ({
7242
7309
  fitness: getFitnessViewData(resolveScopedUserIds(request.query))
7243
7310
  }));
7311
+ app.get("/api/v1/health/training-load", async (request) => ({
7312
+ trainingLoad: getTrainingLoadViewData(resolveScopedUserIds(request.query))
7313
+ }));
7244
7314
  app.get("/api/v1/health/vitals", async (request) => ({
7245
7315
  vitals: getVitalsViewData(resolveScopedUserIds(request.query))
7246
7316
  }));
@@ -365,9 +365,15 @@ export function upsertWorkoutTimeSeries(input) {
365
365
  quality_flags_json, metadata_json, provenance_json, created_at, updated_at
366
366
  )
367
367
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
368
- ON CONFLICT(workout_id, metric_key, source_sample_uid, series_index)
369
- DO UPDATE SET value = excluded.value, started_at = excluded.started_at,
368
+ ON CONFLICT(workout_id, metric_key, source_sample_uid)
369
+ DO UPDATE SET series_index = excluded.series_index,
370
+ value = excluded.value, started_at = excluded.started_at,
370
371
  ended_at = excluded.ended_at, source_device = excluded.source_device,
372
+ label = excluded.label,
373
+ category = excluded.category,
374
+ unit = excluded.unit,
375
+ source_bundle_identifier = excluded.source_bundle_identifier,
376
+ source_product_type = excluded.source_product_type,
371
377
  capture_method = excluded.capture_method,
372
378
  quality_flags_json = excluded.quality_flags_json,
373
379
  metadata_json = excluded.metadata_json,