forge-openclaw-plugin 0.2.47 → 0.2.49
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/README.md +6 -5
- package/dist/assets/index-2_tuemtU.css +1 -0
- package/dist/assets/index-BAmEvOXb.js +91 -0
- package/dist/assets/index-BAmEvOXb.js.map +1 -0
- package/dist/index.html +2 -2
- package/dist/openclaw/api-client.js +15 -1
- package/dist/openclaw/session-registry.js +17 -0
- package/dist/openclaw/tools.js +1 -1
- package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/dist/server/server/src/app.js +42 -12
- package/dist/server/server/src/health-workout-adapters.js +465 -0
- package/dist/server/server/src/health.js +134 -9
- package/dist/server/server/src/openapi.js +33 -0
- package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
- package/dist/server/server/src/repositories/habits.js +62 -25
- package/dist/server/server/src/repositories/model-settings.js +5 -0
- package/dist/server/server/src/repositories/settings.js +101 -13
- package/dist/server/server/src/repositories/users.js +23 -0
- package/dist/server/server/src/types.js +22 -6
- package/dist/server/server/src/watch-mobile.js +33 -21
- package/dist/server/src/lib/date-keys.js +21 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/skills/forge-openclaw/SKILL.md +3 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +45 -8
- package/skills/forge-openclaw/psyche_entity_playbooks.md +14 -0
- package/dist/assets/index-BejDHw1R.js +0 -91
- package/dist/assets/index-BejDHw1R.js.map +0 -1
- package/dist/assets/index-DtEvFzXp.css +0 -1
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getDatabase, runInTransaction } from "./db.js";
|
|
4
4
|
import { HttpError } from "./errors.js";
|
|
5
|
+
import { buildWorkoutSessionPersistenceSeed, buildWorkoutSessionPresentation, workoutActivityDescriptorSchema, workoutDetailsSchema } from "./health-workout-adapters.js";
|
|
5
6
|
import { getMovementMobileBootstrap, ingestMovementSync, movementSyncPayloadSchema } from "./movement.js";
|
|
6
7
|
import { ingestScreenTimeSync, screenTimeSyncPayloadSchema } from "./screen-time.js";
|
|
7
8
|
import { recordActivityEvent } from "./repositories/activity-events.js";
|
|
@@ -235,6 +236,11 @@ export const mobileHealthSyncSchema = z.object({
|
|
|
235
236
|
averageHeartRate: z.number().nonnegative().nullable().optional(),
|
|
236
237
|
maxHeartRate: z.number().nonnegative().nullable().optional(),
|
|
237
238
|
sourceDevice: z.string().trim().default("Apple Health"),
|
|
239
|
+
sourceSystem: z.string().trim().min(1).default("apple_health"),
|
|
240
|
+
sourceBundleIdentifier: z.string().trim().optional(),
|
|
241
|
+
sourceProductType: z.string().trim().optional(),
|
|
242
|
+
activity: workoutActivityDescriptorSchema.optional(),
|
|
243
|
+
details: workoutDetailsSchema.optional(),
|
|
238
244
|
links: z.array(healthLinkSchema).default([]),
|
|
239
245
|
annotations: workoutAnnotationSchema.partial().default({})
|
|
240
246
|
}))
|
|
@@ -766,6 +772,15 @@ function mapSleepRawLog(row) {
|
|
|
766
772
|
};
|
|
767
773
|
}
|
|
768
774
|
function mapWorkoutSession(row) {
|
|
775
|
+
const provenance = safeJsonParse(row.provenance_json, {});
|
|
776
|
+
const derived = safeJsonParse(row.derived_json, {});
|
|
777
|
+
const presentation = buildWorkoutSessionPresentation({
|
|
778
|
+
source: row.source,
|
|
779
|
+
sourceType: row.source_type,
|
|
780
|
+
workoutType: row.workout_type,
|
|
781
|
+
provenance,
|
|
782
|
+
derived
|
|
783
|
+
});
|
|
769
784
|
return {
|
|
770
785
|
id: row.id,
|
|
771
786
|
externalUid: row.external_uid,
|
|
@@ -773,7 +788,15 @@ function mapWorkoutSession(row) {
|
|
|
773
788
|
userId: row.user_id,
|
|
774
789
|
source: row.source,
|
|
775
790
|
sourceType: row.source_type,
|
|
776
|
-
|
|
791
|
+
sourceSystem: presentation.sourceSystem,
|
|
792
|
+
sourceBundleIdentifier: presentation.sourceBundleIdentifier,
|
|
793
|
+
sourceProductType: presentation.sourceProductType,
|
|
794
|
+
workoutType: presentation.workoutType,
|
|
795
|
+
workoutTypeLabel: presentation.workoutTypeLabel,
|
|
796
|
+
activityFamily: presentation.activityFamily,
|
|
797
|
+
activityFamilyLabel: presentation.activityFamilyLabel,
|
|
798
|
+
activity: presentation.activity,
|
|
799
|
+
details: presentation.details,
|
|
777
800
|
sourceDevice: row.source_device,
|
|
778
801
|
startedAt: row.started_at,
|
|
779
802
|
endedAt: row.ended_at,
|
|
@@ -794,8 +817,8 @@ function mapWorkoutSession(row) {
|
|
|
794
817
|
links: safeJsonParse(row.links_json, []),
|
|
795
818
|
tags: safeJsonParse(row.tags_json, []),
|
|
796
819
|
annotations: safeJsonParse(row.annotations_json, {}),
|
|
797
|
-
provenance
|
|
798
|
-
derived
|
|
820
|
+
provenance,
|
|
821
|
+
derived,
|
|
799
822
|
generatedFromHabitId: row.generated_from_habit_id,
|
|
800
823
|
generatedFromCheckInId: row.generated_from_check_in_id,
|
|
801
824
|
reconciliationStatus: row.reconciliation_status,
|
|
@@ -1904,10 +1927,20 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
1904
1927
|
.get(pairing.user_id, input.externalUid);
|
|
1905
1928
|
const annotations = workoutAnnotationSchema.parse(input.annotations ?? {});
|
|
1906
1929
|
const now = nowIso();
|
|
1930
|
+
const basePersistenceSeed = buildWorkoutSessionPersistenceSeed({
|
|
1931
|
+
source: "apple_health",
|
|
1932
|
+
sourceType: "healthkit",
|
|
1933
|
+
workoutType: input.workoutType,
|
|
1934
|
+
sourceSystem: input.sourceSystem,
|
|
1935
|
+
sourceBundleIdentifier: input.sourceBundleIdentifier,
|
|
1936
|
+
sourceProductType: input.sourceProductType,
|
|
1937
|
+
activity: input.activity,
|
|
1938
|
+
details: input.details
|
|
1939
|
+
});
|
|
1907
1940
|
const matchedGenerated = existing ??
|
|
1908
1941
|
findMatchingGeneratedWorkout({
|
|
1909
1942
|
userId: pairing.user_id,
|
|
1910
|
-
workoutType:
|
|
1943
|
+
workoutType: basePersistenceSeed.activity.canonicalKey,
|
|
1911
1944
|
startedAt: input.startedAt,
|
|
1912
1945
|
endedAt: input.endedAt
|
|
1913
1946
|
});
|
|
@@ -1915,6 +1948,26 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
1915
1948
|
const existingLinks = safeJsonParse(matchedGenerated.links_json, []);
|
|
1916
1949
|
const existingTags = safeJsonParse(matchedGenerated.tags_json, []);
|
|
1917
1950
|
const existingAnnotations = safeJsonParse(matchedGenerated.annotations_json, {});
|
|
1951
|
+
const existingProvenance = safeJsonParse(matchedGenerated.provenance_json, {});
|
|
1952
|
+
const existingDerived = safeJsonParse(matchedGenerated.derived_json, {});
|
|
1953
|
+
const persistenceSeed = buildWorkoutSessionPersistenceSeed({
|
|
1954
|
+
source: "apple_health",
|
|
1955
|
+
sourceType: matchedGenerated.generated_from_habit_id
|
|
1956
|
+
? "reconciled"
|
|
1957
|
+
: "healthkit",
|
|
1958
|
+
workoutType: input.workoutType,
|
|
1959
|
+
sourceSystem: input.sourceSystem,
|
|
1960
|
+
sourceBundleIdentifier: input.sourceBundleIdentifier ??
|
|
1961
|
+
(typeof existingProvenance.sourceBundleIdentifier === "string"
|
|
1962
|
+
? existingProvenance.sourceBundleIdentifier
|
|
1963
|
+
: null),
|
|
1964
|
+
sourceProductType: input.sourceProductType ??
|
|
1965
|
+
(typeof existingProvenance.sourceProductType === "string"
|
|
1966
|
+
? existingProvenance.sourceProductType
|
|
1967
|
+
: null),
|
|
1968
|
+
activity: input.activity ?? existingDerived.activity ?? existingProvenance.activity,
|
|
1969
|
+
details: input.details ?? existingDerived.details ?? existingProvenance.details
|
|
1970
|
+
});
|
|
1918
1971
|
const mergedLinks = mergeHealthLinks(existingLinks, input.links, annotations.links);
|
|
1919
1972
|
const mergedTags = mergeStringLists(existingTags, annotations.tags);
|
|
1920
1973
|
const nextSubjectiveEffort = matchedGenerated.subjective_effort ?? annotations.subjectiveEffort ?? null;
|
|
@@ -1945,12 +1998,21 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
1945
1998
|
reconciliation_status = ?, updated_at = ?
|
|
1946
1999
|
WHERE id = ?`)
|
|
1947
2000
|
.run(input.externalUid, pairing.id, matchedGenerated.generated_from_habit_id ? "reconciled" : "healthkit", input.sourceDevice, input.startedAt, input.endedAt, Math.max(0, Math.round((Date.parse(input.endedAt) - Date.parse(input.startedAt)) / 1000)), input.activeEnergyKcal ?? null, input.totalEnergyKcal ?? null, input.distanceMeters ?? null, input.stepCount ?? null, input.exerciseMinutes ?? null, input.averageHeartRate ?? null, input.maxHeartRate ?? null, nextSubjectiveEffort, nextMoodBefore, nextMoodAfter, nextMeaningText, nextPlannedContext, nextSocialContext, JSON.stringify(mergedLinks), JSON.stringify(mergedTags), JSON.stringify(nextAnnotations), JSON.stringify({
|
|
2001
|
+
...existingProvenance,
|
|
1948
2002
|
importedVia: "ios_companion",
|
|
1949
2003
|
pairingSessionId: pairing.id,
|
|
2004
|
+
sourceSystem: persistenceSeed.sourceSystem,
|
|
2005
|
+
sourceBundleIdentifier: persistenceSeed.sourceBundleIdentifier,
|
|
2006
|
+
sourceProductType: persistenceSeed.sourceProductType,
|
|
2007
|
+
activity: persistenceSeed.activity,
|
|
2008
|
+
details: persistenceSeed.details,
|
|
1950
2009
|
mergedWithGenerated: matchedGenerated.generated_from_habit_id !== null,
|
|
1951
2010
|
priorSource: matchedGenerated.source,
|
|
1952
2011
|
updatedAt: now
|
|
1953
2012
|
}), JSON.stringify({
|
|
2013
|
+
...existingDerived,
|
|
2014
|
+
activity: persistenceSeed.activity,
|
|
2015
|
+
details: persistenceSeed.details,
|
|
1954
2016
|
paceMetersPerMinute: input.distanceMeters && input.exerciseMinutes
|
|
1955
2017
|
? Number((input.distanceMeters / input.exerciseMinutes).toFixed(2))
|
|
1956
2018
|
: null
|
|
@@ -1972,11 +2034,18 @@ function insertOrUpdateWorkoutSession(pairing, input) {
|
|
|
1972
2034
|
provenance_json, derived_json, reconciliation_status, created_at, updated_at
|
|
1973
2035
|
)
|
|
1974
2036
|
VALUES (?, ?, ?, ?, 'apple_health', 'healthkit', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'standalone', ?, ?)`)
|
|
1975
|
-
.run(id, input.externalUid, pairing.id, pairing.user_id,
|
|
2037
|
+
.run(id, input.externalUid, pairing.id, pairing.user_id, basePersistenceSeed.activity.canonicalKey, input.sourceDevice, input.startedAt, input.endedAt, Math.max(0, Math.round((Date.parse(input.endedAt) - Date.parse(input.startedAt)) / 1000)), input.activeEnergyKcal ?? null, input.totalEnergyKcal ?? null, input.distanceMeters ?? null, input.stepCount ?? null, input.exerciseMinutes ?? null, input.averageHeartRate ?? null, input.maxHeartRate ?? null, annotations.subjectiveEffort, annotations.moodBefore, annotations.moodAfter, annotations.meaningText, annotations.plannedContext, annotations.socialContext, JSON.stringify(input.links), JSON.stringify(annotations.tags), JSON.stringify(annotations), JSON.stringify({
|
|
1976
2038
|
importedVia: "ios_companion",
|
|
1977
2039
|
pairingSessionId: pairing.id,
|
|
2040
|
+
sourceSystem: basePersistenceSeed.sourceSystem,
|
|
2041
|
+
sourceBundleIdentifier: basePersistenceSeed.sourceBundleIdentifier,
|
|
2042
|
+
sourceProductType: basePersistenceSeed.sourceProductType,
|
|
2043
|
+
activity: basePersistenceSeed.activity,
|
|
2044
|
+
details: basePersistenceSeed.details,
|
|
1978
2045
|
createdAt: now
|
|
1979
2046
|
}), JSON.stringify({
|
|
2047
|
+
activity: basePersistenceSeed.activity,
|
|
2048
|
+
details: basePersistenceSeed.details,
|
|
1980
2049
|
paceMetersPerMinute: input.distanceMeters && input.exerciseMinutes
|
|
1981
2050
|
? Number((input.distanceMeters / input.exerciseMinutes).toFixed(2))
|
|
1982
2051
|
: null
|
|
@@ -2814,6 +2883,8 @@ export function getFitnessViewData(userIds) {
|
|
|
2814
2883
|
habitGeneratedSessionCount: recent.filter((session) => session.sourceType === "habit_generated").length,
|
|
2815
2884
|
reconciledSessionCount: recent.filter((session) => session.reconciliationStatus === "merged").length,
|
|
2816
2885
|
topWorkoutType: orderedWorkoutTypes[0]?.[0] ?? null,
|
|
2886
|
+
topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])
|
|
2887
|
+
?.workoutTypeLabel ?? null,
|
|
2817
2888
|
streakDays: Array.from(new Set(weekly.map((session) => dayKey(session.startedAt)))).length
|
|
2818
2889
|
},
|
|
2819
2890
|
weeklyTrend: weekly
|
|
@@ -2821,12 +2892,21 @@ export function getFitnessViewData(userIds) {
|
|
|
2821
2892
|
id: session.id,
|
|
2822
2893
|
dateKey: dayKey(session.startedAt),
|
|
2823
2894
|
workoutType: session.workoutType,
|
|
2895
|
+
workoutTypeLabel: session.workoutTypeLabel,
|
|
2896
|
+
activityFamily: session.activityFamily,
|
|
2897
|
+
activityFamilyLabel: session.activityFamilyLabel,
|
|
2824
2898
|
durationMinutes: Math.round(session.durationSeconds / 60),
|
|
2825
2899
|
energyKcal: Math.round(session.totalEnergyKcal ?? session.activeEnergyKcal ?? 0)
|
|
2826
2900
|
}))
|
|
2827
2901
|
.reverse(),
|
|
2828
2902
|
typeBreakdown: orderedWorkoutTypes.map(([workoutType, metrics]) => ({
|
|
2829
2903
|
workoutType,
|
|
2904
|
+
workoutTypeLabel: recent.find((session) => session.workoutType === workoutType)?.workoutTypeLabel ??
|
|
2905
|
+
workoutType,
|
|
2906
|
+
activityFamily: recent.find((session) => session.workoutType === workoutType)?.activityFamily ??
|
|
2907
|
+
"other",
|
|
2908
|
+
activityFamilyLabel: recent.find((session) => session.workoutType === workoutType)
|
|
2909
|
+
?.activityFamilyLabel ?? "Other",
|
|
2830
2910
|
sessionCount: metrics.sessionCount,
|
|
2831
2911
|
totalMinutes: metrics.totalMinutes,
|
|
2832
2912
|
energyKcal: metrics.energyKcal
|
|
@@ -3180,6 +3260,11 @@ export function createWorkoutSession(input, activity) {
|
|
|
3180
3260
|
tags: parsed.tags,
|
|
3181
3261
|
links: parsed.links
|
|
3182
3262
|
};
|
|
3263
|
+
const persistenceSeed = buildWorkoutSessionPersistenceSeed({
|
|
3264
|
+
source: parsed.source,
|
|
3265
|
+
sourceType: parsed.sourceType,
|
|
3266
|
+
workoutType: parsed.workoutType
|
|
3267
|
+
});
|
|
3183
3268
|
getDatabase()
|
|
3184
3269
|
.prepare(`INSERT INTO health_workout_sessions (
|
|
3185
3270
|
id, external_uid, pairing_session_id, user_id, source, source_type, workout_type, source_device,
|
|
@@ -3189,16 +3274,21 @@ export function createWorkoutSession(input, activity) {
|
|
|
3189
3274
|
provenance_json, derived_json, reconciliation_status, created_at, updated_at
|
|
3190
3275
|
)
|
|
3191
3276
|
VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'standalone', ?, ?)`)
|
|
3192
|
-
.run(id, externalUid, userId, parsed.source, parsed.sourceType,
|
|
3277
|
+
.run(id, externalUid, userId, parsed.source, parsed.sourceType, persistenceSeed.activity.canonicalKey, parsed.sourceDevice, parsed.startedAt, parsed.endedAt, durationSeconds, parsed.activeEnergyKcal ?? null, parsed.totalEnergyKcal ?? null, parsed.distanceMeters ?? null, parsed.stepCount ?? null, parsed.exerciseMinutes ?? null, parsed.averageHeartRate ?? null, parsed.maxHeartRate ?? null, parsed.subjectiveEffort ?? null, parsed.moodBefore, parsed.moodAfter, parsed.meaningText, parsed.plannedContext, parsed.socialContext, JSON.stringify(parsed.links), JSON.stringify(parsed.tags), JSON.stringify(annotations), JSON.stringify({
|
|
3193
3278
|
manualEntry: true,
|
|
3194
3279
|
entryMode: "local",
|
|
3195
3280
|
source: parsed.source,
|
|
3196
3281
|
sourceType: parsed.sourceType,
|
|
3282
|
+
sourceSystem: persistenceSeed.sourceSystem,
|
|
3283
|
+
activity: persistenceSeed.activity,
|
|
3284
|
+
details: persistenceSeed.details,
|
|
3197
3285
|
sourceDevice: parsed.sourceDevice,
|
|
3198
3286
|
actor: activity?.actor ?? null,
|
|
3199
3287
|
createdAt: now,
|
|
3200
3288
|
...parsed.provenance
|
|
3201
3289
|
}), JSON.stringify({
|
|
3290
|
+
activity: persistenceSeed.activity,
|
|
3291
|
+
details: persistenceSeed.details,
|
|
3202
3292
|
paceMetersPerMinute: parsed.distanceMeters && parsed.exerciseMinutes
|
|
3203
3293
|
? Number((parsed.distanceMeters / parsed.exerciseMinutes).toFixed(2))
|
|
3204
3294
|
: null
|
|
@@ -3268,8 +3358,25 @@ export function updateWorkoutSession(workoutId, patch, activity) {
|
|
|
3268
3358
|
links
|
|
3269
3359
|
};
|
|
3270
3360
|
const currentProvenance = safeJsonParse(current.provenance_json, {});
|
|
3361
|
+
const currentDerived = safeJsonParse(current.derived_json, {});
|
|
3271
3362
|
const nextExerciseMinutes = parsed.exerciseMinutes ?? current.exercise_minutes;
|
|
3272
3363
|
const nextDistanceMeters = parsed.distanceMeters ?? current.distance_meters;
|
|
3364
|
+
const persistenceSeed = buildWorkoutSessionPersistenceSeed({
|
|
3365
|
+
source: parsed.source ?? current.source,
|
|
3366
|
+
sourceType: parsed.sourceType ?? current.source_type,
|
|
3367
|
+
workoutType: parsed.workoutType ?? current.workout_type,
|
|
3368
|
+
sourceSystem: typeof currentProvenance.sourceSystem === "string"
|
|
3369
|
+
? currentProvenance.sourceSystem
|
|
3370
|
+
: null,
|
|
3371
|
+
sourceBundleIdentifier: typeof currentProvenance.sourceBundleIdentifier === "string"
|
|
3372
|
+
? currentProvenance.sourceBundleIdentifier
|
|
3373
|
+
: null,
|
|
3374
|
+
sourceProductType: typeof currentProvenance.sourceProductType === "string"
|
|
3375
|
+
? currentProvenance.sourceProductType
|
|
3376
|
+
: null,
|
|
3377
|
+
activity: currentDerived.activity ?? currentProvenance.activity,
|
|
3378
|
+
details: currentDerived.details ?? currentProvenance.details
|
|
3379
|
+
});
|
|
3273
3380
|
getDatabase()
|
|
3274
3381
|
.prepare(`UPDATE health_workout_sessions
|
|
3275
3382
|
SET external_uid = ?, source = ?, source_type = ?, workout_type = ?, source_device = ?,
|
|
@@ -3279,12 +3386,20 @@ export function updateWorkoutSession(workoutId, patch, activity) {
|
|
|
3279
3386
|
social_context = ?, links_json = ?, tags_json = ?, annotations_json = ?, provenance_json = ?,
|
|
3280
3387
|
derived_json = ?, updated_at = ?
|
|
3281
3388
|
WHERE id = ?`)
|
|
3282
|
-
.run(parsed.externalUid ?? current.external_uid, parsed.source ?? current.source, parsed.sourceType ?? current.source_type,
|
|
3389
|
+
.run(parsed.externalUid ?? current.external_uid, parsed.source ?? current.source, parsed.sourceType ?? current.source_type, persistenceSeed.activity.canonicalKey, parsed.sourceDevice ?? current.source_device, startedAt, endedAt, durationSeconds, parsed.activeEnergyKcal ?? current.active_energy_kcal, parsed.totalEnergyKcal ?? current.total_energy_kcal, nextDistanceMeters, parsed.stepCount ?? current.step_count, nextExerciseMinutes, parsed.averageHeartRate ?? current.average_heart_rate, parsed.maxHeartRate ?? current.max_heart_rate, annotations.subjectiveEffort, annotations.moodBefore, annotations.moodAfter, annotations.meaningText, annotations.plannedContext, annotations.socialContext, JSON.stringify(links), JSON.stringify(tags), JSON.stringify(annotations), JSON.stringify({
|
|
3283
3390
|
...currentProvenance,
|
|
3284
3391
|
...(parsed.provenance ?? {}),
|
|
3392
|
+
sourceSystem: persistenceSeed.sourceSystem,
|
|
3393
|
+
sourceBundleIdentifier: persistenceSeed.sourceBundleIdentifier,
|
|
3394
|
+
sourceProductType: persistenceSeed.sourceProductType,
|
|
3395
|
+
activity: persistenceSeed.activity,
|
|
3396
|
+
details: persistenceSeed.details,
|
|
3285
3397
|
updatedAt: now,
|
|
3286
3398
|
updatedByActor: activity?.actor ?? null
|
|
3287
3399
|
}), JSON.stringify({
|
|
3400
|
+
...currentDerived,
|
|
3401
|
+
activity: persistenceSeed.activity,
|
|
3402
|
+
details: persistenceSeed.details,
|
|
3288
3403
|
paceMetersPerMinute: nextDistanceMeters && nextExerciseMinutes
|
|
3289
3404
|
? Number((nextDistanceMeters / nextExerciseMinutes).toFixed(2))
|
|
3290
3405
|
: null
|
|
@@ -3441,6 +3556,11 @@ export function createGeneratedWorkoutFromHabit(args) {
|
|
|
3441
3556
|
}
|
|
3442
3557
|
const now = nowIso();
|
|
3443
3558
|
const id = `workout_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
3559
|
+
const persistenceSeed = buildWorkoutSessionPersistenceSeed({
|
|
3560
|
+
source: "forge_habit",
|
|
3561
|
+
sourceType: "habit_generated",
|
|
3562
|
+
workoutType: template.workoutType
|
|
3563
|
+
});
|
|
3444
3564
|
getDatabase()
|
|
3445
3565
|
.prepare(`INSERT INTO health_workout_sessions (
|
|
3446
3566
|
id, external_uid, pairing_session_id, user_id, source, source_type, workout_type, source_device,
|
|
@@ -3448,7 +3568,7 @@ export function createGeneratedWorkoutFromHabit(args) {
|
|
|
3448
3568
|
derived_json, generated_from_habit_id, generated_from_check_in_id, reconciliation_status, created_at, updated_at
|
|
3449
3569
|
)
|
|
3450
3570
|
VALUES (?, ?, NULL, ?, 'forge_habit', 'habit_generated', ?, 'Habit automation', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'awaiting_import_match', ?, ?)`)
|
|
3451
|
-
.run(id, `habit_${args.checkInId}`, args.userId,
|
|
3571
|
+
.run(id, `habit_${args.checkInId}`, args.userId, persistenceSeed.activity.canonicalKey, startedAt, endedAt, template.durationMinutes * 60, JSON.stringify([
|
|
3452
3572
|
...(template.links ?? []),
|
|
3453
3573
|
...(args.linkedEntities ?? [])
|
|
3454
3574
|
]), JSON.stringify(template.tags), JSON.stringify({
|
|
@@ -3457,8 +3577,13 @@ export function createGeneratedWorkoutFromHabit(args) {
|
|
|
3457
3577
|
}), JSON.stringify({
|
|
3458
3578
|
generatedFrom: "habit_completion",
|
|
3459
3579
|
habitId: args.habitId,
|
|
3460
|
-
checkInId: args.checkInId
|
|
3580
|
+
checkInId: args.checkInId,
|
|
3581
|
+
sourceSystem: persistenceSeed.sourceSystem,
|
|
3582
|
+
activity: persistenceSeed.activity,
|
|
3583
|
+
details: persistenceSeed.details
|
|
3461
3584
|
}), JSON.stringify({
|
|
3585
|
+
activity: persistenceSeed.activity,
|
|
3586
|
+
details: persistenceSeed.details,
|
|
3462
3587
|
xpReward: template.xpReward
|
|
3463
3588
|
}), args.habitId, args.checkInId, now, now);
|
|
3464
3589
|
recordActivityEvent({
|
|
@@ -2137,6 +2137,11 @@ export function buildOpenApiDocument() {
|
|
|
2137
2137
|
"id",
|
|
2138
2138
|
"label",
|
|
2139
2139
|
"agentType",
|
|
2140
|
+
"identityKey",
|
|
2141
|
+
"provider",
|
|
2142
|
+
"machineKey",
|
|
2143
|
+
"personaKey",
|
|
2144
|
+
"linkedUsers",
|
|
2140
2145
|
"trustLevel",
|
|
2141
2146
|
"autonomyMode",
|
|
2142
2147
|
"approvalMode",
|
|
@@ -2150,6 +2155,20 @@ export function buildOpenApiDocument() {
|
|
|
2150
2155
|
id: { type: "string" },
|
|
2151
2156
|
label: { type: "string" },
|
|
2152
2157
|
agentType: { type: "string" },
|
|
2158
|
+
identityKey: nullable({ type: "string" }),
|
|
2159
|
+
provider: nullable({ type: "string", enum: ["openclaw", "hermes", "codex"] }),
|
|
2160
|
+
machineKey: nullable({ type: "string" }),
|
|
2161
|
+
personaKey: nullable({ type: "string" }),
|
|
2162
|
+
linkedUsers: arrayOf({
|
|
2163
|
+
type: "object",
|
|
2164
|
+
additionalProperties: false,
|
|
2165
|
+
required: ["userId", "role", "user"],
|
|
2166
|
+
properties: {
|
|
2167
|
+
userId: { type: "string" },
|
|
2168
|
+
role: { type: "string" },
|
|
2169
|
+
user: nullable({ $ref: "#/components/schemas/UserSummary" })
|
|
2170
|
+
}
|
|
2171
|
+
}),
|
|
2153
2172
|
trustLevel: {
|
|
2154
2173
|
type: "string",
|
|
2155
2174
|
enum: ["standard", "trusted", "autonomous"]
|
|
@@ -4055,6 +4074,12 @@ export function buildOpenApiDocument() {
|
|
|
4055
4074
|
"userId",
|
|
4056
4075
|
"source",
|
|
4057
4076
|
"sourceType",
|
|
4077
|
+
"sourceSystem",
|
|
4078
|
+
"workoutTypeLabel",
|
|
4079
|
+
"activityFamily",
|
|
4080
|
+
"activityFamilyLabel",
|
|
4081
|
+
"activity",
|
|
4082
|
+
"details",
|
|
4058
4083
|
"workoutType",
|
|
4059
4084
|
"sourceDevice",
|
|
4060
4085
|
"startedAt",
|
|
@@ -4091,7 +4116,15 @@ export function buildOpenApiDocument() {
|
|
|
4091
4116
|
userId: { type: "string" },
|
|
4092
4117
|
source: { type: "string" },
|
|
4093
4118
|
sourceType: { type: "string" },
|
|
4119
|
+
sourceSystem: { type: "string" },
|
|
4120
|
+
sourceBundleIdentifier: nullable({ type: "string" }),
|
|
4121
|
+
sourceProductType: nullable({ type: "string" }),
|
|
4094
4122
|
workoutType: { type: "string" },
|
|
4123
|
+
workoutTypeLabel: { type: "string" },
|
|
4124
|
+
activityFamily: { type: "string" },
|
|
4125
|
+
activityFamilyLabel: { type: "string" },
|
|
4126
|
+
activity: { type: "object", additionalProperties: true },
|
|
4127
|
+
details: { type: "object", additionalProperties: true },
|
|
4095
4128
|
sourceDevice: { type: "string" },
|
|
4096
4129
|
startedAt: { type: "string", format: "date-time" },
|
|
4097
4130
|
endedAt: { type: "string", format: "date-time" },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
3
|
import { recordActivityEvent } from "./activity-events.js";
|
|
4
4
|
import { listAgentActions } from "./collaboration.js";
|
|
5
|
+
import { ensureBotUser, getUserById } from "./users.js";
|
|
5
6
|
import { agentActionSchema, agentRuntimeEventLevelSchema, agentRuntimeReconnectPlanSchema, agentRuntimeSessionEventSchema, agentRuntimeSessionSchema, createAgentRuntimeSessionEventSchema, createAgentRuntimeSessionSchema, disconnectAgentRuntimeSessionSchema, heartbeatAgentRuntimeSessionSchema, reconnectAgentRuntimeSessionSchema } from "../types.js";
|
|
6
7
|
function parseMetadata(raw) {
|
|
7
8
|
try {
|
|
@@ -211,7 +212,95 @@ function ensureCurrentSessionInstance(row, externalSessionId) {
|
|
|
211
212
|
}
|
|
212
213
|
return true;
|
|
213
214
|
}
|
|
214
|
-
function
|
|
215
|
+
function normalizeIdentityPart(value) {
|
|
216
|
+
return (value
|
|
217
|
+
?.trim()
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-z0-9._:]+/g, "_")
|
|
220
|
+
.replace(/^_+|_+$/g, "") ?? "");
|
|
221
|
+
}
|
|
222
|
+
function shortHash(value) {
|
|
223
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 12);
|
|
224
|
+
}
|
|
225
|
+
function canonicalRuntimeAgentLabel(provider) {
|
|
226
|
+
if (provider === "openclaw") {
|
|
227
|
+
return "Forge OpenClaw";
|
|
228
|
+
}
|
|
229
|
+
if (provider === "hermes") {
|
|
230
|
+
return "Forge Hermes";
|
|
231
|
+
}
|
|
232
|
+
return "Forge Codex";
|
|
233
|
+
}
|
|
234
|
+
function canonicalRuntimeDescription(provider) {
|
|
235
|
+
return `${canonicalRuntimeAgentLabel(provider)} runtime agent with stable Forge identity and linked Kanban user.`;
|
|
236
|
+
}
|
|
237
|
+
function canonicalAgentUserSpec(provider) {
|
|
238
|
+
if (provider === "openclaw") {
|
|
239
|
+
return {
|
|
240
|
+
id: "user_agent_openclaw",
|
|
241
|
+
handle: "openclaw",
|
|
242
|
+
displayName: "OpenClaw",
|
|
243
|
+
description: "OpenClaw runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
244
|
+
accentColor: "#38bdf8"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (provider === "hermes") {
|
|
248
|
+
return {
|
|
249
|
+
id: "user_agent_hermes",
|
|
250
|
+
handle: "hermes",
|
|
251
|
+
displayName: "Hermes",
|
|
252
|
+
description: "Hermes runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
253
|
+
accentColor: "#a78bfa"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
id: "user_agent_codex",
|
|
258
|
+
handle: "codex",
|
|
259
|
+
displayName: "Codex",
|
|
260
|
+
description: "Codex runtime actor linked to Forge agent identity and Kanban ownership.",
|
|
261
|
+
accentColor: "#22c55e"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function deriveMachineKey(input) {
|
|
265
|
+
const explicit = normalizeIdentityPart(input.machineKey);
|
|
266
|
+
if (explicit) {
|
|
267
|
+
return explicit;
|
|
268
|
+
}
|
|
269
|
+
const source = [
|
|
270
|
+
normalizeText(input.dataRoot) ?? "",
|
|
271
|
+
normalizeText(input.baseUrl) ?? "local"
|
|
272
|
+
].join("|");
|
|
273
|
+
return `machine_${shortHash(source)}`;
|
|
274
|
+
}
|
|
275
|
+
function derivePersonaKey(input) {
|
|
276
|
+
return (normalizeIdentityPart(input.personaKey) ||
|
|
277
|
+
normalizeIdentityPart(input.agentType) ||
|
|
278
|
+
"default");
|
|
279
|
+
}
|
|
280
|
+
function deriveAgentIdentityKey(input) {
|
|
281
|
+
const explicit = normalizeIdentityPart(input.agentIdentityKey);
|
|
282
|
+
if (explicit) {
|
|
283
|
+
return explicit;
|
|
284
|
+
}
|
|
285
|
+
return `runtime:${input.provider}:${deriveMachineKey(input)}:${derivePersonaKey(input)}`;
|
|
286
|
+
}
|
|
287
|
+
function linkAgentIdentityUsers(agentId, provider, linkedUserIds, now) {
|
|
288
|
+
const primaryUser = ensureBotUser(canonicalAgentUserSpec(provider));
|
|
289
|
+
const normalizedUserIds = Array.from(new Set([primaryUser.id, ...linkedUserIds.map((id) => id.trim()).filter(Boolean)]));
|
|
290
|
+
for (const userId of normalizedUserIds) {
|
|
291
|
+
if (!getUserById(userId)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
getDatabase()
|
|
295
|
+
.prepare(`INSERT INTO agent_identity_users (agent_id, user_id, role, created_at, updated_at)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?)
|
|
297
|
+
ON CONFLICT(agent_id, user_id) DO UPDATE SET
|
|
298
|
+
role = excluded.role,
|
|
299
|
+
updated_at = excluded.updated_at`)
|
|
300
|
+
.run(agentId, userId, userId === primaryUser.id ? "primary" : "linked", now, now);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now) {
|
|
215
304
|
if (!parsed.metadata?.singleton) {
|
|
216
305
|
return;
|
|
217
306
|
}
|
|
@@ -224,11 +313,11 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
|
|
|
224
313
|
created_at, updated_at
|
|
225
314
|
FROM agent_runtime_sessions
|
|
226
315
|
WHERE provider = ?
|
|
227
|
-
AND
|
|
316
|
+
AND agent_id = ?
|
|
228
317
|
AND coalesce(base_url, '') = coalesce(?, '')
|
|
229
318
|
AND coalesce(data_root, '') = coalesce(?, '')
|
|
230
319
|
AND id <> ?`)
|
|
231
|
-
.all(parsed.provider,
|
|
320
|
+
.all(parsed.provider, agentId, normalizeText(parsed.baseUrl), normalizeText(parsed.dataRoot), sessionId);
|
|
232
321
|
for (const row of rows) {
|
|
233
322
|
if (row.status === "disconnected" && row.ended_at) {
|
|
234
323
|
continue;
|
|
@@ -251,29 +340,45 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
|
|
|
251
340
|
}
|
|
252
341
|
}
|
|
253
342
|
function upsertRuntimeAgentIdentity(input) {
|
|
343
|
+
const identityKey = deriveAgentIdentityKey(input);
|
|
344
|
+
const machineKey = deriveMachineKey(input);
|
|
345
|
+
const personaKey = derivePersonaKey(input);
|
|
346
|
+
const label = canonicalRuntimeAgentLabel(input.provider);
|
|
254
347
|
const existing = getDatabase()
|
|
255
348
|
.prepare(`SELECT id
|
|
256
349
|
FROM agent_identities
|
|
257
|
-
WHERE
|
|
350
|
+
WHERE identity_key = ?
|
|
351
|
+
OR (
|
|
352
|
+
(identity_key IS NULL OR machine_key IS NULL OR machine_key = 'legacy' OR identity_key LIKE 'runtime:%:legacy:%')
|
|
353
|
+
AND (
|
|
354
|
+
provider = ?
|
|
355
|
+
OR lower(agent_type) = lower(?)
|
|
356
|
+
OR lower(label) = lower(?)
|
|
357
|
+
)
|
|
358
|
+
)
|
|
258
359
|
LIMIT 1`)
|
|
259
|
-
.get(input.
|
|
360
|
+
.get(identityKey, input.provider, input.provider, label);
|
|
260
361
|
const now = new Date().toISOString();
|
|
261
|
-
const description =
|
|
362
|
+
const description = canonicalRuntimeDescription(input.provider);
|
|
262
363
|
if (existing) {
|
|
263
364
|
getDatabase()
|
|
264
365
|
.prepare(`UPDATE agent_identities
|
|
265
|
-
SET agent_type = ?,
|
|
366
|
+
SET label = ?, agent_type = ?, identity_key = ?, provider = ?,
|
|
367
|
+
machine_key = ?, persona_key = ?, description = ?, updated_at = ?
|
|
266
368
|
WHERE id = ?`)
|
|
267
|
-
.run(input.agentType || input.provider, now, existing.id);
|
|
369
|
+
.run(label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, existing.id);
|
|
370
|
+
linkAgentIdentityUsers(existing.id, input.provider, input.linkedUserIds, now);
|
|
268
371
|
return existing.id;
|
|
269
372
|
}
|
|
270
373
|
const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
271
374
|
getDatabase()
|
|
272
375
|
.prepare(`INSERT INTO agent_identities (
|
|
273
|
-
id, label, agent_type,
|
|
376
|
+
id, label, agent_type, identity_key, provider, machine_key, persona_key,
|
|
377
|
+
trust_level, autonomy_mode, approval_mode,
|
|
274
378
|
description, created_at, updated_at
|
|
275
|
-
) VALUES (?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
|
|
276
|
-
.run(agentId,
|
|
379
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
|
|
380
|
+
.run(agentId, label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, now);
|
|
381
|
+
linkAgentIdentityUsers(agentId, input.provider, input.linkedUserIds, now);
|
|
277
382
|
return agentId;
|
|
278
383
|
}
|
|
279
384
|
function insertSessionEvent(sessionId, input, now = new Date().toISOString()) {
|
|
@@ -321,6 +426,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
321
426
|
return runInTransaction(() => {
|
|
322
427
|
const now = new Date().toISOString();
|
|
323
428
|
const agentId = upsertRuntimeAgentIdentity(parsed);
|
|
429
|
+
const agentLabel = canonicalRuntimeAgentLabel(parsed.provider);
|
|
324
430
|
const existing = getSessionRowByCompositeKey(parsed.provider, parsed.sessionKey);
|
|
325
431
|
const sessionId = existing?.id ?? `ags_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
326
432
|
if (existing) {
|
|
@@ -332,7 +438,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
332
438
|
last_error = ?, last_seen_at = ?, last_heartbeat_at = ?, started_at = ?,
|
|
333
439
|
ended_at = NULL, metadata_json = ?, updated_at = ?
|
|
334
440
|
WHERE id = ?`)
|
|
335
|
-
.run(agentId,
|
|
441
|
+
.run(agentId, agentLabel, parsed.agentType || parsed.provider, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, sessionId);
|
|
336
442
|
insertSessionEvent(sessionId, {
|
|
337
443
|
eventType: "session_registered",
|
|
338
444
|
title: "Session re-registered",
|
|
@@ -349,7 +455,7 @@ export function registerAgentRuntimeSession(input) {
|
|
|
349
455
|
last_seen_at, last_heartbeat_at, started_at, ended_at, metadata_json,
|
|
350
456
|
created_at, updated_at
|
|
351
457
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)`)
|
|
352
|
-
.run(sessionId, agentId,
|
|
458
|
+
.run(sessionId, agentId, agentLabel, parsed.agentType || parsed.provider, parsed.provider, parsed.sessionKey, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, now);
|
|
353
459
|
insertSessionEvent(sessionId, {
|
|
354
460
|
eventType: "session_registered",
|
|
355
461
|
title: "Session registered",
|
|
@@ -357,12 +463,12 @@ export function registerAgentRuntimeSession(input) {
|
|
|
357
463
|
metadata: parsed.metadata
|
|
358
464
|
}, now);
|
|
359
465
|
}
|
|
360
|
-
disconnectSupersededSingletonSessions(parsed, sessionId, now);
|
|
466
|
+
disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now);
|
|
361
467
|
recordActivityEvent({
|
|
362
468
|
entityType: "session",
|
|
363
469
|
entityId: sessionId,
|
|
364
470
|
eventType: "agent_session_registered",
|
|
365
|
-
title: `Agent session registered: ${
|
|
471
|
+
title: `Agent session registered: ${agentLabel}`,
|
|
366
472
|
description: `${parsed.provider} registered a live agent session.`,
|
|
367
473
|
actor: parsed.actorLabel,
|
|
368
474
|
source: "agent",
|