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.
@@ -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
- const generationResult = await generateCoachObservationsForAccount(account, { sourceTrigger });
3327
- result.coachObservations = coachObservationGenerationMetadata(generationResult, sourceTrigger);
3431
+ skipReason = await coachObservationGenerationSkipReason({
3432
+ account,
3433
+ uploadResult: result,
3434
+ sourceTrigger,
3435
+ listCurrentCoachObservationsForAccount
3436
+ });
3328
3437
  } catch (error) {
3329
- console.error('Coach observation generation after score upload failed:', error.message);
3330
- result.coachObservations = {
3331
- attempted: true,
3332
- generated: 0,
3333
- persisted: 0,
3334
- skippedReason: 'generation_failed',
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
- if (generateCoachObservationsForAccount) {
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 answer = stripXMLTagBlocks(changesetParsed.answerText);
5277
- return { askResult: result, programDraft: draft, planChangeset: changeset, assistantAnswer: answer };
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);