incremnt 0.8.3 → 0.8.5
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/package.json +1 -1
- package/src/ask-coach.js +412 -62
- package/src/contract.js +12 -1
- package/src/format.js +33 -0
- package/src/openrouter.js +6 -5
- package/src/program-schedule-action.js +107 -0
- package/src/prompt-changelog.js +18 -0
- package/src/queries.js +185 -2
- package/src/remote.js +7 -0
- package/src/sync-service.js +194 -55
package/src/sync-service.js
CHANGED
|
@@ -25,9 +25,11 @@ import { sanitizeHistory, detectSystemPromptLeak, stripXMLTagBlocks } from './pr
|
|
|
25
25
|
import { enrichScoreSnapshots } from './score-context.js';
|
|
26
26
|
import { extractAskProgramDraft } from './program-draft.js';
|
|
27
27
|
import { extractPlanChangeset } from './plan-changeset.js';
|
|
28
|
+
import { extractProgramScheduleAction } from './program-schedule-action.js';
|
|
28
29
|
|
|
29
30
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
30
31
|
const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
32
|
+
const COACH_OBSERVATION_DAILY_REFRESH_COOLDOWN_MS = 20 * 60 * 60 * 1000;
|
|
31
33
|
const WEEKLY_CHECKIN_COMPLETION_USER_TURNS = 3;
|
|
32
34
|
const WEEKLY_CHECKIN_RECAP_LOCAL_HOUR = 16;
|
|
33
35
|
const DEFAULT_RATE_LIMIT_RULES = {
|
|
@@ -70,6 +72,8 @@ const DEFAULT_RATE_LIMIT_RULES = {
|
|
|
70
72
|
'program-share-list': 60,
|
|
71
73
|
'program-share-public': 120,
|
|
72
74
|
'program-share-revoke': 30,
|
|
75
|
+
'workout-share-create': 30,
|
|
76
|
+
'workout-share-public': 120,
|
|
73
77
|
'mobile-sync-bootstrap': 60,
|
|
74
78
|
'mobile-sync-pull': 120,
|
|
75
79
|
'mobile-sync-push': 60,
|
|
@@ -282,6 +286,7 @@ export function sanitizeAskStructuredResponseForStorage(structured) {
|
|
|
282
286
|
const limitations = askStorageStringArray(structured.limitations, { maxItems: ASK_STRUCTURED_MAX_ITEMS, maxLength: 240 });
|
|
283
287
|
const answerVerification = sanitizeAskAnswerVerificationReceipt(structured.answerVerification);
|
|
284
288
|
const programDraft = sanitizeAskProgramDraftForStorage(structured.programDraft);
|
|
289
|
+
const programScheduleAction = sanitizeAskProgramDraftForStorage(structured.programScheduleAction);
|
|
285
290
|
|
|
286
291
|
return {
|
|
287
292
|
...(answer ? { answer } : {}),
|
|
@@ -291,7 +296,8 @@ export function sanitizeAskStructuredResponseForStorage(structured) {
|
|
|
291
296
|
followUpSuggestions,
|
|
292
297
|
limitations,
|
|
293
298
|
...(answerVerification ? { answerVerification } : {}),
|
|
294
|
-
programDraft
|
|
299
|
+
programDraft,
|
|
300
|
+
programScheduleAction
|
|
295
301
|
};
|
|
296
302
|
}
|
|
297
303
|
|
|
@@ -645,6 +651,7 @@ export function buildAskInteractionLogPayload({
|
|
|
645
651
|
followUpSuggestionCount: Array.isArray(structured?.followUpSuggestions) ? structured.followUpSuggestions.length : undefined,
|
|
646
652
|
limitationCount: Array.isArray(structured?.limitations) ? structured.limitations.length : undefined,
|
|
647
653
|
hasProgramDraft: structured?.programDraft != null ? true : undefined,
|
|
654
|
+
hasProgramScheduleAction: structured?.programScheduleAction != null ? true : undefined,
|
|
648
655
|
askVerificationStatus: answerVerification.status,
|
|
649
656
|
askVerificationRetryCount: typeof answerVerification.retryCount === 'number' ? answerVerification.retryCount : undefined,
|
|
650
657
|
askVerificationDegraded: answerVerification.degraded === true ? true : undefined,
|
|
@@ -947,6 +954,38 @@ function coachObservationGenerationMetadata(result, sourceTrigger) {
|
|
|
947
954
|
};
|
|
948
955
|
}
|
|
949
956
|
|
|
957
|
+
function skippedCoachObservationGenerationMetadata(skippedReason, sourceTrigger) {
|
|
958
|
+
return {
|
|
959
|
+
attempted: false,
|
|
960
|
+
generated: 0,
|
|
961
|
+
persisted: 0,
|
|
962
|
+
skippedReason,
|
|
963
|
+
sourceTrigger: sourceTrigger ?? null
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function recentDailyRefreshObservation(observations, now = new Date()) {
|
|
968
|
+
const threshold = now.getTime() - COACH_OBSERVATION_DAILY_REFRESH_COOLDOWN_MS;
|
|
969
|
+
return (Array.isArray(observations) ? observations : []).some((observation) => {
|
|
970
|
+
if (observation?.sourceTrigger !== 'daily_refresh') return false;
|
|
971
|
+
const generatedAt = new Date(observation.generatedAt ?? observation.windowEnd ?? 0).getTime();
|
|
972
|
+
return Number.isFinite(generatedAt) && generatedAt >= threshold;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function coachObservationGenerationSkipReason({
|
|
977
|
+
account,
|
|
978
|
+
uploadResult,
|
|
979
|
+
sourceTrigger,
|
|
980
|
+
listCurrentCoachObservationsForAccount
|
|
981
|
+
}) {
|
|
982
|
+
if (sourceTrigger !== 'daily_refresh') return null;
|
|
983
|
+
if (Number(uploadResult?.inserted ?? 0) <= 0) return 'no_score_snapshot_change';
|
|
984
|
+
if (typeof listCurrentCoachObservationsForAccount !== 'function') return null;
|
|
985
|
+
const observations = await listCurrentCoachObservationsForAccount(account, { limit: 20 });
|
|
986
|
+
return recentDailyRefreshObservation(observations) ? 'recent_daily_refresh' : null;
|
|
987
|
+
}
|
|
988
|
+
|
|
950
989
|
function normalizeAskCoachObservationFollowUp(value) {
|
|
951
990
|
if (!value || typeof value !== 'object') return null;
|
|
952
991
|
const id = String(value.id ?? '').trim();
|
|
@@ -1177,6 +1216,20 @@ function routeRequest(url, method) {
|
|
|
1177
1216
|
}
|
|
1178
1217
|
}
|
|
1179
1218
|
|
|
1219
|
+
if (pathname === '/cli/workout-share') {
|
|
1220
|
+
return { command: 'workout-share-create', options: {} };
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
{
|
|
1224
|
+
const workoutSharePublicMatch = pathname.match(/^\/workout-share\/([^/]+)$/);
|
|
1225
|
+
if (workoutSharePublicMatch) {
|
|
1226
|
+
return {
|
|
1227
|
+
command: 'workout-share-public',
|
|
1228
|
+
options: { token: decodeURIComponent(workoutSharePublicMatch[1]) }
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1180
1233
|
{
|
|
1181
1234
|
const programShareRevokeMatch = pathname.match(/^\/cli\/program-share\/([^/]+)\/revoke$/);
|
|
1182
1235
|
if (programShareRevokeMatch) {
|
|
@@ -1211,6 +1264,16 @@ function routeRequest(url, method) {
|
|
|
1211
1264
|
};
|
|
1212
1265
|
}
|
|
1213
1266
|
|
|
1267
|
+
if (pathname === '/cli/programs/history') {
|
|
1268
|
+
return {
|
|
1269
|
+
command: 'program-history',
|
|
1270
|
+
options: {
|
|
1271
|
+
'program-id': url.searchParams.get('program-id') ?? undefined,
|
|
1272
|
+
limit: url.searchParams.get('limit') ?? undefined
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1214
1277
|
const programShowMatch = pathname.match(/^\/cli\/programs\/([^/]+)$/);
|
|
1215
1278
|
if (programShowMatch) {
|
|
1216
1279
|
return { command: 'program-detail', options: { id: programShowMatch[1] } };
|
|
@@ -2316,6 +2379,8 @@ export function createSyncServiceRequestHandler({
|
|
|
2316
2379
|
listProgramSharesForAccount = null,
|
|
2317
2380
|
readPublicProgramShare = null,
|
|
2318
2381
|
revokeProgramShareForAccount = null,
|
|
2382
|
+
createWorkoutShareForAccount = null,
|
|
2383
|
+
readPublicWorkoutShare = null,
|
|
2319
2384
|
updateAnalysisConsentForAccount = null,
|
|
2320
2385
|
updateDisplayNameForAccount = null,
|
|
2321
2386
|
saveAskConversationForAccount = null,
|
|
@@ -3004,6 +3069,45 @@ export function createSyncServiceRequestHandler({
|
|
|
3004
3069
|
}
|
|
3005
3070
|
}
|
|
3006
3071
|
|
|
3072
|
+
if (route.command === 'workout-share-public') {
|
|
3073
|
+
if (request.method !== 'GET') {
|
|
3074
|
+
methodNotAllowed(response, 'Use GET for /workout-share/:token.');
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
if (!readPublicWorkoutShare) {
|
|
3078
|
+
methodNotAllowed(response, 'Workout sharing is not enabled for this service mode.');
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
try {
|
|
3082
|
+
const shared = await readPublicWorkoutShare(route.options.token);
|
|
3083
|
+
if (shared.status === 'not_found') {
|
|
3084
|
+
notFound(response, 'Workout share not found.');
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
if (shared.status === 'revoked' || shared.status === 'expired') {
|
|
3088
|
+
json(response, 410, { error: 'Workout share is no longer available.' });
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
json(response, 200, {
|
|
3092
|
+
ok: true,
|
|
3093
|
+
token: route.options.token,
|
|
3094
|
+
version: shared.share.version,
|
|
3095
|
+
workoutName: shared.share.workoutName,
|
|
3096
|
+
workoutPayload: shared.share.workoutPayload,
|
|
3097
|
+
createdAt: shared.share.createdAt,
|
|
3098
|
+
expiresAt: shared.share.expiresAt
|
|
3099
|
+
});
|
|
3100
|
+
return;
|
|
3101
|
+
} catch (error) {
|
|
3102
|
+
if (error?.message === 'Invalid workout share token.') {
|
|
3103
|
+
badRequest(response, error.message);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
internalError(response, error, onError);
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3007
3111
|
if (route.command === 'google-mobile') {
|
|
3008
3112
|
if (request.method !== 'POST') {
|
|
3009
3113
|
methodNotAllowed(response, 'Use POST for /auth/google/mobile.');
|
|
@@ -3322,18 +3426,33 @@ export function createSyncServiceRequestHandler({
|
|
|
3322
3426
|
const result = await insertScoreSnapshotsForAccount(account, body.snapshots);
|
|
3323
3427
|
if (generateCoachObservationsForAccount) {
|
|
3324
3428
|
const sourceTrigger = coachObservationSourceTriggerForScoreSnapshots(body.snapshots);
|
|
3429
|
+
let skipReason = null;
|
|
3325
3430
|
try {
|
|
3326
|
-
|
|
3327
|
-
|
|
3431
|
+
skipReason = await coachObservationGenerationSkipReason({
|
|
3432
|
+
account,
|
|
3433
|
+
uploadResult: result,
|
|
3434
|
+
sourceTrigger,
|
|
3435
|
+
listCurrentCoachObservationsForAccount
|
|
3436
|
+
});
|
|
3328
3437
|
} catch (error) {
|
|
3329
|
-
console.error('Coach observation generation
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
sourceTrigger
|
|
3336
|
-
|
|
3438
|
+
console.error('Coach observation generation skip check failed:', error.message);
|
|
3439
|
+
}
|
|
3440
|
+
if (skipReason) {
|
|
3441
|
+
result.coachObservations = skippedCoachObservationGenerationMetadata(skipReason, sourceTrigger);
|
|
3442
|
+
} else {
|
|
3443
|
+
try {
|
|
3444
|
+
const generationResult = await generateCoachObservationsForAccount(account, { sourceTrigger });
|
|
3445
|
+
result.coachObservations = coachObservationGenerationMetadata(generationResult, sourceTrigger);
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
console.error('Coach observation generation after score upload failed:', error.message);
|
|
3448
|
+
result.coachObservations = {
|
|
3449
|
+
attempted: true,
|
|
3450
|
+
generated: 0,
|
|
3451
|
+
persisted: 0,
|
|
3452
|
+
skippedReason: 'generation_failed',
|
|
3453
|
+
sourceTrigger
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3337
3456
|
}
|
|
3338
3457
|
}
|
|
3339
3458
|
json(response, 200, result);
|
|
@@ -3604,6 +3723,53 @@ export function createSyncServiceRequestHandler({
|
|
|
3604
3723
|
}
|
|
3605
3724
|
}
|
|
3606
3725
|
|
|
3726
|
+
if (route.command === 'workout-share-create') {
|
|
3727
|
+
if (request.method !== 'POST') {
|
|
3728
|
+
methodNotAllowed(response, 'Use POST for /cli/workout-share.');
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
if (!createWorkoutShareForAccount) {
|
|
3732
|
+
methodNotAllowed(response, 'Workout sharing is not enabled for this service mode.');
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
const account = connectedWriteAuthenticator
|
|
3736
|
+
? await connectedWriteAuthenticator(requestToken)
|
|
3737
|
+
: null;
|
|
3738
|
+
if (!account) {
|
|
3739
|
+
if (await rejectInsufficientScopeForReadToken(response, request, requestToken, readAuthenticator)) {
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
unauthorized(response, request);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
try {
|
|
3746
|
+
const body = await readJsonBody(request);
|
|
3747
|
+
const workoutPayload = body.workoutPayload ?? body;
|
|
3748
|
+
const share = await createWorkoutShareForAccount(account, workoutPayload);
|
|
3749
|
+
json(response, 201, {
|
|
3750
|
+
ok: true,
|
|
3751
|
+
shareId: share.id,
|
|
3752
|
+
tokenHint: share.tokenHint,
|
|
3753
|
+
token: share.token,
|
|
3754
|
+
workoutName: share.workoutName,
|
|
3755
|
+
createdAt: share.createdAt,
|
|
3756
|
+
expiresAt: share.expiresAt,
|
|
3757
|
+
revokedAt: share.revokedAt,
|
|
3758
|
+
version: share.version,
|
|
3759
|
+
link: `${resolvedPublicOrigin}/workout-share/${share.token}`,
|
|
3760
|
+
deepLink: `incremnt://workout-share/${share.token}?base=${encodeURIComponent(resolvedPublicOrigin)}`
|
|
3761
|
+
});
|
|
3762
|
+
return;
|
|
3763
|
+
} catch (error) {
|
|
3764
|
+
if (error?.message === 'Workout share payload is malformed.') {
|
|
3765
|
+
badRequest(response, error.message);
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
internalError(response, error, onError);
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3607
3773
|
if (route.command === 'program-share-list') {
|
|
3608
3774
|
if (request.method !== 'GET') {
|
|
3609
3775
|
methodNotAllowed(response, 'Use GET for /cli/programs/:programId/shares.');
|
|
@@ -4024,45 +4190,8 @@ export function createSyncServiceRequestHandler({
|
|
|
4024
4190
|
const limit = parseLimit(route.options.limit, { defaultValue: 5, max: 20 });
|
|
4025
4191
|
let refresh = null;
|
|
4026
4192
|
if (route.options.refresh === 'morning_open') {
|
|
4027
|
-
const writeAccount = writeAuthenticator
|
|
4028
|
-
? await writeAuthenticator(requestToken)
|
|
4029
|
-
: requestToken === token
|
|
4030
|
-
? account
|
|
4031
|
-
: null;
|
|
4032
|
-
if (!writeAccount) {
|
|
4033
|
-
if (await rejectInsufficientScopeForReadToken(response, request, requestToken, readAuthenticator)) {
|
|
4034
|
-
return;
|
|
4035
|
-
}
|
|
4036
|
-
unauthorized(response, request);
|
|
4037
|
-
return;
|
|
4038
|
-
}
|
|
4039
4193
|
const sourceTrigger = 'daily_refresh';
|
|
4040
|
-
|
|
4041
|
-
try {
|
|
4042
|
-
const generationResult = await generateCoachObservationsForAccount(writeAccount, {
|
|
4043
|
-
sourceTrigger,
|
|
4044
|
-
refresh: route.options.refresh
|
|
4045
|
-
});
|
|
4046
|
-
refresh = coachObservationGenerationMetadata(generationResult, sourceTrigger);
|
|
4047
|
-
} catch (error) {
|
|
4048
|
-
console.error('Coach observation generation on morning open failed:', error.message);
|
|
4049
|
-
refresh = {
|
|
4050
|
-
attempted: true,
|
|
4051
|
-
generated: 0,
|
|
4052
|
-
persisted: 0,
|
|
4053
|
-
skippedReason: 'generation_failed',
|
|
4054
|
-
sourceTrigger
|
|
4055
|
-
};
|
|
4056
|
-
}
|
|
4057
|
-
} else {
|
|
4058
|
-
refresh = {
|
|
4059
|
-
attempted: false,
|
|
4060
|
-
generated: 0,
|
|
4061
|
-
persisted: 0,
|
|
4062
|
-
skippedReason: 'generation_unavailable',
|
|
4063
|
-
sourceTrigger
|
|
4064
|
-
};
|
|
4065
|
-
}
|
|
4194
|
+
refresh = skippedCoachObservationGenerationMetadata('morning_open_read_only', sourceTrigger);
|
|
4066
4195
|
}
|
|
4067
4196
|
const observations = await listCurrentCoachObservationsForAccount(account, {
|
|
4068
4197
|
limit
|
|
@@ -5264,7 +5393,12 @@ export function createSyncServiceRequestHandler({
|
|
|
5264
5393
|
canonicalizeExerciseName: canonicalExerciseName
|
|
5265
5394
|
});
|
|
5266
5395
|
const changesetParsed = extractPlanChangeset(parsed.answerText);
|
|
5396
|
+
const scheduleActionParsed = extractProgramScheduleAction(changesetParsed.answerText, {
|
|
5397
|
+
expectedStartDate: routedContext.metadata?.programScheduleActionStartDate ?? null,
|
|
5398
|
+
requireTrailing: true
|
|
5399
|
+
});
|
|
5267
5400
|
const requestedPlanAdjustment = requestedCoachObservation?.intent === 'plan_adjustment';
|
|
5401
|
+
const requestedProgramScheduleAction = routedContext.metadata?.intent?.requestedAction === 'schedule_program_action';
|
|
5268
5402
|
const draft = missingRequestedCoachObservation && requestedCoachObservation?.intent === 'successor_plan'
|
|
5269
5403
|
? undefined
|
|
5270
5404
|
: parsed.programDraft;
|
|
@@ -5273,8 +5407,11 @@ export function createSyncServiceRequestHandler({
|
|
|
5273
5407
|
const changeset = !requestedPlanAdjustment || missingRequestedCoachObservation
|
|
5274
5408
|
? undefined
|
|
5275
5409
|
: changesetParsed.planChangeset;
|
|
5276
|
-
const
|
|
5277
|
-
|
|
5410
|
+
const programScheduleAction = requestedProgramScheduleAction
|
|
5411
|
+
? scheduleActionParsed.programScheduleAction ?? undefined
|
|
5412
|
+
: undefined;
|
|
5413
|
+
const answer = stripXMLTagBlocks(scheduleActionParsed.answerText);
|
|
5414
|
+
return { askResult: result, programDraft: draft, planChangeset: changeset, programScheduleAction, assistantAnswer: answer };
|
|
5278
5415
|
};
|
|
5279
5416
|
|
|
5280
5417
|
let attempt = await generateAttempt(ctx);
|
|
@@ -5384,7 +5521,8 @@ export function createSyncServiceRequestHandler({
|
|
|
5384
5521
|
...attempt,
|
|
5385
5522
|
assistantAnswer: safeAskVerificationFallback(),
|
|
5386
5523
|
programDraft: undefined,
|
|
5387
|
-
planChangeset: undefined
|
|
5524
|
+
planChangeset: undefined,
|
|
5525
|
+
programScheduleAction: undefined
|
|
5388
5526
|
};
|
|
5389
5527
|
}
|
|
5390
5528
|
}
|
|
@@ -5410,7 +5548,7 @@ export function createSyncServiceRequestHandler({
|
|
|
5410
5548
|
});
|
|
5411
5549
|
}
|
|
5412
5550
|
|
|
5413
|
-
const { askResult, assistantAnswer, programDraft, planChangeset } = attempt;
|
|
5551
|
+
const { askResult, assistantAnswer, programDraft, planChangeset, programScheduleAction } = attempt;
|
|
5414
5552
|
const promptSurface = askResult.promptSurface
|
|
5415
5553
|
?? (persistedKind === 'weekly-checkin' ? 'weekly-checkin' : 'ask');
|
|
5416
5554
|
const promptVersion = askResult.promptVersion
|
|
@@ -5424,7 +5562,7 @@ export function createSyncServiceRequestHandler({
|
|
|
5424
5562
|
...(programDraftRetryCount > 0 ? { programDraftRetryCount } : {}),
|
|
5425
5563
|
...(planChangesetRetryCount > 0 ? { planChangesetRetryCount } : {})
|
|
5426
5564
|
};
|
|
5427
|
-
const structured = buildAskStructuredResponse(assistantAnswer, routingMetadata, { programDraft, planChangeset, question });
|
|
5565
|
+
const structured = buildAskStructuredResponse(assistantAnswer, routingMetadata, { programDraft, planChangeset, programScheduleAction, question });
|
|
5428
5566
|
console.log(`ask-coach-meta ${JSON.stringify(buildAskInteractionLogPayload({
|
|
5429
5567
|
accountId: account.id,
|
|
5430
5568
|
status: 'ok',
|
|
@@ -5527,7 +5665,8 @@ export function createSyncServiceRequestHandler({
|
|
|
5527
5665
|
metadata,
|
|
5528
5666
|
structured,
|
|
5529
5667
|
programDraft,
|
|
5530
|
-
planChangeset
|
|
5668
|
+
planChangeset,
|
|
5669
|
+
programScheduleAction
|
|
5531
5670
|
});
|
|
5532
5671
|
} catch (err) {
|
|
5533
5672
|
console.error('AI ask error:', err.message);
|