forge-openclaw-plugin 0.2.78 → 0.2.80

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.
@@ -2895,7 +2895,7 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
2895
2895
  searchHints: [
2896
2896
  "Clarify whether the user wants flow discovery, editing, execution, published output, run inspection, or node-level output before choosing the route.",
2897
2897
  "Distinguish flow contract, published output, run history, latest-node-output, and chat follow-up questions before reaching for a route.",
2898
- "If the user is still deciding how to run or edit a flow, read flow detail or the box catalog before asking them for payload structure."
2898
+ "If the user is still deciding how to run or edit a flow, read flow detail or the box catalog before asking them for structured input details."
2899
2899
  ],
2900
2900
  fieldGuide: []
2901
2901
  }),
@@ -2969,7 +2969,7 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
2969
2969
  "Do not minimize functional analysis, trigger chains, behavior patterns, modes, beliefs, or schema themes. After at least one concrete example is clear, offer one careful interpretive hypothesis when it would help the user understand what the reaction may be protecting, predicting, relieving, or costing.",
2970
2970
  "Phrase Psyche interpretive hypotheses as collaborative and testable, not as verdicts. Ask whether the hypothesis lands or needs correction before turning it into a saved belief, pattern, mode, trigger report, or note.",
2971
2971
  "Once the Movement, Life Force, or Workbench job is clear, speak in product nouns such as timeline, overlay, weekday template, published output, run detail, or node result instead of generic record language.",
2972
- "If the next answer would not change the route, wording, timing, or write payload in a meaningful way, stop asking and act.",
2972
+ "If the next answer would not change the route, wording, timing, or write shape in a meaningful way, stop asking and act.",
2973
2973
  "Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
2974
2974
  "Once the record is clear enough to name, stop exploring broadly and ask only for the last structural detail that still matters.",
2975
2975
  "If the record is already clear enough to save, save it instead of performing a ceremonial extra question.",
@@ -2980,7 +2980,7 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
2980
2980
  "When the user already named a precise correction or review target, do not widen back out into a meta lane question. Confirm only the missing route-selecting detail and then act.",
2981
2981
  "Once the route family is clear, say it plainly enough that another agent could follow the same path without guessing.",
2982
2982
  "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.",
2983
- "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 payload and do not downgrade the request into generic batch CRUD.",
2983
+ "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.",
2984
2984
  "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.",
2985
2985
  "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.",
2986
2986
  "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.",
@@ -3344,26 +3344,28 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3344
3344
  },
3345
3345
  {
3346
3346
  focus: "questionnaire_instrument",
3347
- openingQuestion: "What would this questionnaire help someone notice or track?",
3348
- coachingGoal: "Clarify whether the user is authoring a reusable questionnaire and what the instrument is for.",
3347
+ openingQuestion: "What honest moment or decision should this questionnaire help someone notice or track?",
3348
+ coachingGoal: "Clarify whether the user is authoring a reusable questionnaire and what honest moment, pattern, or decision the instrument should help someone notice.",
3349
3349
  askSequence: [
3350
- "Ask what the questionnaire is meant to measure or surface.",
3350
+ "Ask what honest moment, pattern, or decision the questionnaire should help someone notice.",
3351
3351
  "Ask who it is for and when it should be used.",
3352
- "Ask what kind of honest moment or decision it should help someone answer before getting into item wording.",
3353
- "Reflect the practical use case back in plain language.",
3352
+ "Ask what the respondent should understand after answering that they might otherwise miss.",
3353
+ "Reflect the practical use case back in plain language before asking for item wording.",
3354
3354
  "Ask what would make the instrument distinct instead of redundant if a near-duplicate risk is visible.",
3355
+ "Ask about item shape, response scale, scoring, or provenance only after the purpose and use context are steady.",
3355
3356
  "Use batch CRUD for ordinary questionnaire instrument create or update work; use clone, draft, and publish actions only for version lifecycle.",
3356
3357
  "Move to draft creation once the purpose is clear."
3357
3358
  ]
3358
3359
  },
3359
3360
  {
3360
3361
  focus: "questionnaire_run",
3361
- openingQuestion: "Do you want to start, continue, review, or finish a questionnaire run?",
3362
- coachingGoal: "Clarify whether the user wants to start, continue, or complete one answer session.",
3362
+ openingQuestion: "What do you want from this questionnaire run right now: start, continue, review, or finish it?",
3363
+ coachingGoal: "Clarify whether the user wants to start, continue, review, or complete one answer session without turning the run into a mechanical form fill.",
3363
3364
  askSequence: [
3364
3365
  "Ask what the user wants from the run right now: start, continue, review, or finish.",
3365
3366
  "Ask which questionnaire or existing run this is about.",
3366
3367
  "If the user wants to continue or finish, ask what feels most stuck, unfinished, or important before asking for more content.",
3368
+ "If the user is reviewing answers, ask what the run should help them understand before proposing edits or completion.",
3367
3369
  "Use the dedicated questionnaire run start, read, update, and complete routes instead of generic entity CRUD.",
3368
3370
  "If answering is still in progress, ask only for the next answer or note that matters."
3369
3371
  ]
@@ -3427,7 +3429,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3427
3429
  "If the user wants to send a follow-up into a saved flow chat, confirm the saved flow and what the message should accomplish instead of treating it as a new run or note.",
3428
3430
  "If the user already named the flow and action clearly, skip the meta lane question and ask only for the missing run, node, or output scope.",
3429
3431
  "If the user wants a stable public input contract or published output, prefer those dedicated reads instead of detouring through run history first.",
3430
- "If the user is still shaping a payload or edit, prefer flow detail or box catalog reads before asking for structured inputs.",
3432
+ "If the user is still shaping one-off inputs or an edit, prefer flow detail or box catalog reads before asking for structured inputs.",
3431
3433
  "If the user is debugging one failed run, ask whether the useful artifact is the run summary, one node result, the latest node output, or the published output before you start asking for edits.",
3432
3434
  "Prefer flow detail or published-output reads for stable contracts, and use run or node-result routes only when the user is asking about execution history or debugging.",
3433
3435
  "Route to the dedicated workbench route family once the execution lane is clear."
@@ -4474,7 +4476,7 @@ function buildAgentOnboardingPayload(request) {
4474
4476
  taskTimebox: "A planned or live calendar slot tied to a task. Timeboxes can be suggested in advance or created automatically from active task runs.",
4475
4477
  workAdjustment: "A work adjustment is a truthful signed minute correction on an existing task or project when real work happened but no live run was active.",
4476
4478
  movement: "Forge Movement is the first-class mobility surface. It is a timeline of stays and trips: stays capture time spent in the same place, and trips capture travel between places. Use it for time-in-place questions, travel-history review, specific stay or trip edits, selected-span aggregates, known places, and links to other Forge records rather than pretending stays and trips are normal batch CRUD entities.",
4477
- lifeForce: "Life Force is Forge's energy-budget and fatigue model. Read it through the dedicated life-force payload and update it through focused profile, weekday-template, and fatigue-signal routes rather than generic entity CRUD.",
4479
+ lifeForce: "Life Force is Forge's energy-budget and fatigue model. Read it through the dedicated life-force state and update it through focused profile, weekday-template, and fatigue-signal routes rather than generic entity CRUD.",
4478
4480
  workbench: "Workbench is Forge's graph-flow execution system. Treat flows, runs, published outputs, node results, and latest-node-output reads as a dedicated API family instead of a normal entity-batch surface.",
4479
4481
  psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, flashcards, and trigger reports. It is sensitive and should be handled deliberately."
4480
4482
  },
@@ -4491,7 +4493,10 @@ function buildAgentOnboardingPayload(request) {
4491
4493
  emotionDefinition: "An emotion definition is reusable emotion vocabulary for trigger reports. Reports can either reference one or fall back to raw labels.",
4492
4494
  triggerReport: "A trigger report is the one-episode incident chain: situation, emotions, thoughts, behaviors, consequences, extra mode labels, schema themes, and next moves."
4493
4495
  },
4494
- psycheCoachingPlaybooks: AGENT_ONBOARDING_PSYCHE_PLAYBOOKS.map(enrichConversationPlaybookWithRouteInfo),
4496
+ psycheCoachingPlaybooks: AGENT_ONBOARDING_PSYCHE_PLAYBOOKS.map((playbook) => enrichConversationPlaybookWithRouteInfo({
4497
+ ...playbook,
4498
+ openingQuestion: playbook.exampleQuestions[0] ?? playbook.askSequence[0] ?? ""
4499
+ })),
4495
4500
  conversationRules: AGENT_ONBOARDING_CONVERSATION_RULES,
4496
4501
  entityConversationPlaybooks: AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS.map(enrichConversationPlaybookWithRouteInfo),
4497
4502
  relationshipModel: [
@@ -4870,8 +4875,8 @@ function buildAgentOnboardingPayload(request) {
4870
4875
  "Workbench is a dedicated execution surface, not a batch CRUD entity family.",
4871
4876
  "Route-selection questions are internal. User-facing questions should ask whether the user needs the saved flow, its input contract, one run, one node, or the public result instead of reciting Workbench route keys.",
4872
4877
  "Use the flow routes when the agent needs stable public input contracts, published outputs, node-level results, or reusable execution history.",
4873
- "If the user is still figuring out inputs or editable structure, read flow detail or box catalog before asking them to author a payload from memory.",
4874
- "For flow creation, clarify what the flow should reliably produce, which input contract it should accept, and which first node or box anchors the flow before asking for structured payload details.",
4878
+ "If the user is still figuring out inputs or editable structure, read flow detail or box catalog before asking them to reconstruct structured inputs from memory.",
4879
+ "For flow creation, clarify what the flow should reliably produce, which input contract it should accept, and which first node or box anchors the flow before asking for structured input details.",
4875
4880
  "For flow edits, ask what behavior should change while preserving the public contract unless the user explicitly wants the contract changed.",
4876
4881
  "For flow deletion, confirm the saved flow and whether published outputs or run history need preservation elsewhere before using the delete route.",
4877
4882
  "For saved flow chat follow-ups, use POST /api/v1/workbench/flows/:id/chat only when the user wants to continue a flow-specific conversation. Do not turn that into a new run, note, or generic entity update unless the user asks.",
@@ -7665,7 +7670,7 @@ export async function buildServer(options = {}) {
7665
7670
  app.post("/api/v1/mobile/healthkit/sync-sessions", async (request) => ({
7666
7671
  upload: startMobileHealthSyncSession(mobileHealthSyncSessionStartSchema.parse(request.body ?? {}))
7667
7672
  }));
7668
- app.post("/api/v1/mobile/healthkit/sync-sessions/:id/chunks", { bodyLimit: 1_600_000 }, async (request) => {
7673
+ app.post("/api/v1/mobile/healthkit/sync-sessions/:id/chunks", { bodyLimit: 40_000_000 }, async (request) => {
7669
7674
  const { id } = request.params;
7670
7675
  const rawPayloadJson = JSON.stringify((request.body ?? {}).payload ?? {});
7671
7676
  return {
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { inflateSync } from "node:zlib";
2
3
  import { z } from "zod";
3
4
  import { getDatabase, runInTransaction } from "./db.js";
4
5
  import { HttpError } from "./errors.js";
@@ -260,11 +261,13 @@ export const mobileHealthSyncSchema = z.object({
260
261
  screenTime: screenTimeSyncPayloadSchema.default({})
261
262
  });
262
263
  const HEALTH_MOBILE_SYNC_SCHEMA_VERSION = "healthkit-sync-v2";
263
- const HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES = 512_000;
264
- const HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES = 1_000_000;
264
+ const HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES = 12_000_000;
265
+ const HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES = 24_000_000;
265
266
  const HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING = "payload_json_base64";
267
+ const HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING = "payload_json_deflate_base64";
266
268
  const HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS = [
267
269
  HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
270
+ HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING,
268
271
  "legacy_payload_object"
269
272
  ];
270
273
  const HEALTH_MOBILE_SYNC_SESSION_TTL_MS = 1000 * 60 * 60 * 24;
@@ -343,7 +346,9 @@ export const mobileHealthSyncChunkSchema = z.object({
343
346
  family: mobileHealthSyncFamilySchema,
344
347
  recordCount: z.number().int().nonnegative().default(0),
345
348
  byteCount: z.number().int().nonnegative().default(0),
349
+ compressedByteCount: z.number().int().nonnegative().optional(),
346
350
  checksumSha256: z.string().trim().min(1),
351
+ payloadJsonDeflateBase64: z.string().trim().min(1).optional(),
347
352
  payloadJsonBase64: z.string().trim().min(1).optional(),
348
353
  payload: mobileHealthSyncChunkPayloadSchema.default({})
349
354
  });
@@ -2691,6 +2696,13 @@ function chunkPayloadJson(payload) {
2691
2696
  function chunkPayloadChecksum(payloadJson) {
2692
2697
  return createHash("sha256").update(payloadJson).digest("hex");
2693
2698
  }
2699
+ function normalizedChunkChecksum(checksum) {
2700
+ const normalized = checksum.trim().toLowerCase();
2701
+ if (!/^[a-f0-9]{64}$/.test(normalized)) {
2702
+ throw new HttpError(400, "invalid_chunk_checksum", "The HealthKit sync chunk checksum is invalid.");
2703
+ }
2704
+ return normalized;
2705
+ }
2694
2706
  function parseBase64ChunkPayload(payloadJsonBase64, context) {
2695
2707
  const compactBase64 = payloadJsonBase64.replace(/\s/g, "");
2696
2708
  if (compactBase64.length === 0 ||
@@ -2724,10 +2736,71 @@ function parseBase64ChunkPayload(payloadJsonBase64, context) {
2724
2736
  payload: parsedPayload,
2725
2737
  payloadJson,
2726
2738
  byteCount: decoded.length,
2739
+ compressedByteCount: undefined,
2727
2740
  mode: "payload_json_base64"
2728
2741
  };
2729
2742
  }
2743
+ function parseDeflateBase64ChunkPayload(payloadJsonDeflateBase64, context) {
2744
+ const compactBase64 = payloadJsonDeflateBase64.replace(/\s/g, "");
2745
+ if (compactBase64.length === 0 ||
2746
+ compactBase64.length % 4 === 1 ||
2747
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(compactBase64)) {
2748
+ throw new HttpError(400, "invalid_chunk_payload", "The HealthKit sync compressed payload encoding is invalid.", { mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING });
2749
+ }
2750
+ const compressed = Buffer.from(compactBase64, "base64");
2751
+ const recoded = compressed.toString("base64").replace(/=+$/g, "");
2752
+ if (recoded !== compactBase64.replace(/=+$/g, "")) {
2753
+ throw new HttpError(400, "invalid_chunk_payload", "The HealthKit sync compressed payload encoding is invalid.", { mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING });
2754
+ }
2755
+ let decoded;
2756
+ try {
2757
+ decoded = inflateSync(compressed);
2758
+ }
2759
+ catch (error) {
2760
+ console.warn("[healthkit-sync] invalid compressed chunk payload", {
2761
+ syncSessionId: context.syncSessionId,
2762
+ chunkId: context.chunkId,
2763
+ family: context.family,
2764
+ mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING,
2765
+ compressedBytes: compressed.length,
2766
+ error: error instanceof Error ? error.message : String(error)
2767
+ });
2768
+ throw new HttpError(400, "invalid_chunk_payload", "The HealthKit sync compressed payload cannot be decompressed.", { mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING });
2769
+ }
2770
+ const payloadJson = decoded.toString("utf8");
2771
+ let rawPayload;
2772
+ try {
2773
+ rawPayload = JSON.parse(payloadJson);
2774
+ }
2775
+ catch (error) {
2776
+ console.warn("[healthkit-sync] invalid compressed chunk JSON payload", {
2777
+ syncSessionId: context.syncSessionId,
2778
+ chunkId: context.chunkId,
2779
+ family: context.family,
2780
+ mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING,
2781
+ bytes: decoded.length,
2782
+ compressedBytes: compressed.length,
2783
+ error: error instanceof Error ? error.message : String(error)
2784
+ });
2785
+ throw new HttpError(400, "invalid_chunk_payload", "The HealthKit sync compressed payload is not valid JSON.", { mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING });
2786
+ }
2787
+ const parsedPayload = mobileHealthSyncChunkPayloadSchema.parse(rawPayload);
2788
+ return {
2789
+ payload: parsedPayload,
2790
+ payloadJson,
2791
+ byteCount: decoded.length,
2792
+ compressedByteCount: compressed.length,
2793
+ mode: HEALTH_MOBILE_SYNC_COMPRESSED_CHUNK_PAYLOAD_ENCODING
2794
+ };
2795
+ }
2730
2796
  function resolveChunkWirePayload(parsed, syncSessionId, rawPayloadJson) {
2797
+ if (parsed.payloadJsonDeflateBase64) {
2798
+ return parseDeflateBase64ChunkPayload(parsed.payloadJsonDeflateBase64, {
2799
+ syncSessionId,
2800
+ chunkId: parsed.chunkId,
2801
+ family: parsed.family
2802
+ });
2803
+ }
2731
2804
  if (parsed.payloadJsonBase64) {
2732
2805
  return parseBase64ChunkPayload(parsed.payloadJsonBase64, {
2733
2806
  syncSessionId,
@@ -2740,6 +2813,7 @@ function resolveChunkWirePayload(parsed, syncSessionId, rawPayloadJson) {
2740
2813
  payload: parsed.payload,
2741
2814
  payloadJson,
2742
2815
  byteCount: Buffer.byteLength(payloadJson, "utf8"),
2816
+ compressedByteCount: undefined,
2743
2817
  mode: "legacy_payload_object"
2744
2818
  };
2745
2819
  }
@@ -2779,6 +2853,76 @@ function summarizeChunkPayload(family, payload) {
2779
2853
  };
2780
2854
  }
2781
2855
  }
2856
+ function mobileSyncSessionPairing(session) {
2857
+ const pairing = getDatabase()
2858
+ .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)
2859
+ .get(session.pairing_session_id);
2860
+ if (!pairing) {
2861
+ throw new HttpError(404, "pairing_invalid", "The sync pairing no longer exists.");
2862
+ }
2863
+ return pairing;
2864
+ }
2865
+ function mobileSyncSessionMetadata(session) {
2866
+ return safeJsonParse(session.source_metadata_json, {});
2867
+ }
2868
+ function applyWorkoutSummaryChunkImmediately(session, workouts) {
2869
+ if (workouts.length === 0) {
2870
+ return;
2871
+ }
2872
+ const pairing = mobileSyncSessionPairing(session);
2873
+ const metadata = mobileSyncSessionMetadata(session);
2874
+ const device = metadata.device ?? {
2875
+ name: pairing.device_name ?? "iPhone",
2876
+ platform: pairing.platform ?? "ios",
2877
+ appVersion: pairing.app_version ?? "",
2878
+ sourceDevice: pairing.device_name ?? "iPhone"
2879
+ };
2880
+ runInTransaction(() => {
2881
+ const now = nowIso();
2882
+ const runId = `hir_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
2883
+ let createdCount = 0;
2884
+ let updatedCount = 0;
2885
+ let mergedCount = 0;
2886
+ getDatabase()
2887
+ .prepare(`INSERT INTO health_import_runs (
2888
+ id, pairing_session_id, user_id, source, source_device, status, payload_summary_json,
2889
+ imported_count, created_count, updated_count, merged_count, imported_at, created_at, updated_at
2890
+ )
2891
+ VALUES (?, ?, ?, 'ios_companion', ?, 'running', '{}', 0, 0, 0, 0, ?, ?, ?)`)
2892
+ .run(runId, pairing.id, pairing.user_id, device.sourceDevice, now, now, now);
2893
+ for (const workout of workouts) {
2894
+ const result = insertOrUpdateWorkoutSession(pairing, workout);
2895
+ if (result.mode === "created") {
2896
+ createdCount += 1;
2897
+ }
2898
+ else if (result.mode === "merged") {
2899
+ mergedCount += 1;
2900
+ }
2901
+ else {
2902
+ updatedCount += 1;
2903
+ }
2904
+ summarizeUserHealthDay(pairing.user_id, dayKey(workout.startedAt));
2905
+ }
2906
+ getDatabase()
2907
+ .prepare(`UPDATE companion_pairing_sessions
2908
+ SET status = 'healthy', device_name = ?, platform = ?, app_version = ?,
2909
+ last_seen_at = ?, last_sync_at = ?, last_sync_error = NULL,
2910
+ paired_at = COALESCE(paired_at, ?), updated_at = ?
2911
+ WHERE id = ?`)
2912
+ .run(device.name, device.platform, device.appVersion, now, now, now, now, pairing.id);
2913
+ getDatabase()
2914
+ .prepare(`UPDATE health_import_runs
2915
+ SET source_device = ?, status = 'completed', payload_summary_json = ?,
2916
+ imported_count = ?, created_count = ?, updated_count = ?, merged_count = ?,
2917
+ imported_at = ?, updated_at = ?
2918
+ WHERE id = ?`)
2919
+ .run(device.sourceDevice, JSON.stringify({
2920
+ progressiveChunk: true,
2921
+ syncSessionId: session.id,
2922
+ workouts: workouts.length
2923
+ }), workouts.length, createdCount, updatedCount, mergedCount, now, now, runId);
2924
+ });
2925
+ }
2782
2926
  function updateMobileSyncSessionProgress(syncSessionId) {
2783
2927
  const chunks = getDatabase()
2784
2928
  .prepare(`SELECT family, record_count, byte_count
@@ -2837,7 +2981,7 @@ export function startMobileHealthSyncSession(payload) {
2837
2981
  chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
2838
2982
  chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
2839
2983
  acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
2840
- supportsCompression: false,
2984
+ supportsCompression: true,
2841
2985
  acceptedFamilies: safeJsonParse(existing.requested_families_json, parsed.requestedFamilies),
2842
2986
  receivedChunkIds
2843
2987
  };
@@ -2866,13 +3010,14 @@ export function startMobileHealthSyncSession(payload) {
2866
3010
  chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
2867
3011
  chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
2868
3012
  acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
2869
- supportsCompression: false,
3013
+ supportsCompression: true,
2870
3014
  acceptedFamilies: parsed.requestedFamilies,
2871
3015
  receivedChunkIds: []
2872
3016
  };
2873
3017
  }
2874
3018
  export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJson) {
2875
3019
  const parsed = mobileHealthSyncChunkSchema.parse(payload);
3020
+ const clientChecksum = normalizedChunkChecksum(parsed.checksumSha256);
2876
3021
  const session = ensureRunningMobileSyncSession(syncSessionId);
2877
3022
  const requestedFamilies = safeJsonParse(session.requested_families_json, []);
2878
3023
  if (requestedFamilies.length > 0 &&
@@ -2884,7 +3029,7 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
2884
3029
  WHERE sync_session_id = ? AND chunk_id = ?`)
2885
3030
  .get(syncSessionId, parsed.chunkId);
2886
3031
  if (existing) {
2887
- if (existing.checksum_sha256 !== parsed.checksumSha256) {
3032
+ if (existing.checksum_sha256 !== clientChecksum) {
2888
3033
  throw new HttpError(409, "chunk_checksum_mismatch", "A chunk with the same id was already accepted with different content.");
2889
3034
  }
2890
3035
  const progress = updateMobileSyncSessionProgress(syncSessionId);
@@ -2905,14 +3050,52 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
2905
3050
  actualBytes: actualByteCount
2906
3051
  });
2907
3052
  }
3053
+ if (wirePayload.mode !== "legacy_payload_object" && parsed.byteCount !== actualByteCount) {
3054
+ console.warn("[healthkit-sync] chunk byte count mismatch", {
3055
+ syncSessionId,
3056
+ chunkId: parsed.chunkId,
3057
+ family: parsed.family,
3058
+ mode: wirePayload.mode,
3059
+ clientByteCount: parsed.byteCount,
3060
+ actualByteCount
3061
+ });
3062
+ throw new HttpError(409, "chunk_byte_count_mismatch", "The HealthKit sync chunk byte count does not match its decoded payload.", {
3063
+ syncSessionId,
3064
+ chunkId: parsed.chunkId,
3065
+ family: parsed.family,
3066
+ actualBytes: actualByteCount,
3067
+ clientBytes: parsed.byteCount,
3068
+ mode: wirePayload.mode
3069
+ });
3070
+ }
3071
+ if (wirePayload.compressedByteCount !== undefined &&
3072
+ parsed.compressedByteCount !== undefined &&
3073
+ parsed.compressedByteCount !== wirePayload.compressedByteCount) {
3074
+ console.warn("[healthkit-sync] chunk compressed byte count mismatch", {
3075
+ syncSessionId,
3076
+ chunkId: parsed.chunkId,
3077
+ family: parsed.family,
3078
+ mode: wirePayload.mode,
3079
+ clientCompressedByteCount: parsed.compressedByteCount,
3080
+ actualCompressedByteCount: wirePayload.compressedByteCount
3081
+ });
3082
+ throw new HttpError(409, "chunk_compressed_byte_count_mismatch", "The HealthKit sync chunk compressed byte count does not match its decoded payload.", {
3083
+ syncSessionId,
3084
+ chunkId: parsed.chunkId,
3085
+ family: parsed.family,
3086
+ actualCompressedBytes: wirePayload.compressedByteCount,
3087
+ clientCompressedBytes: parsed.compressedByteCount,
3088
+ mode: wirePayload.mode
3089
+ });
3090
+ }
2908
3091
  const serverChecksum = chunkPayloadChecksum(payloadJson);
2909
- if (parsed.checksumSha256 !== serverChecksum) {
3092
+ if (clientChecksum !== serverChecksum) {
2910
3093
  console.warn("[healthkit-sync] chunk checksum mismatch", {
2911
3094
  syncSessionId,
2912
3095
  chunkId: parsed.chunkId,
2913
3096
  family: parsed.family,
2914
3097
  mode: wirePayload.mode,
2915
- clientChecksum: parsed.checksumSha256.slice(0, 12),
3098
+ clientChecksum: clientChecksum.slice(0, 12),
2916
3099
  serverChecksum: serverChecksum.slice(0, 12),
2917
3100
  clientByteCount: parsed.byteCount,
2918
3101
  actualByteCount
@@ -2923,7 +3106,7 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
2923
3106
  family: parsed.family,
2924
3107
  actualBytes: actualByteCount,
2925
3108
  clientBytes: parsed.byteCount,
2926
- clientChecksumPrefix: parsed.checksumSha256.slice(0, 12),
3109
+ clientChecksumPrefix: clientChecksum.slice(0, 12),
2927
3110
  serverChecksumPrefix: serverChecksum.slice(0, 12),
2928
3111
  mode: wirePayload.mode
2929
3112
  });
@@ -2936,13 +3119,17 @@ export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJs
2936
3119
  received_at, applied_at, created_at, updated_at
2937
3120
  )
2938
3121
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2939
- .run(mobileSyncChunkRecordId(), syncSessionId, parsed.chunkId, parsed.sequence, parsed.family, serverChecksum, parsed.recordCount, parsed.byteCount || actualByteCount, payloadJson, JSON.stringify({
3122
+ .run(mobileSyncChunkRecordId(), syncSessionId, parsed.chunkId, parsed.sequence, parsed.family, serverChecksum, parsed.recordCount, actualByteCount, payloadJson, JSON.stringify({
2940
3123
  ...summarizeChunkPayload(parsed.family, wirePayload.payload),
2941
3124
  clientByteCount: parsed.byteCount,
2942
3125
  actualByteCount,
3126
+ compressedByteCount: wirePayload.compressedByteCount ?? null,
2943
3127
  serverChecksum,
2944
3128
  mode: wirePayload.mode
2945
3129
  }), now, now, now, now);
3130
+ if (parsed.family === "workout_summaries") {
3131
+ applyWorkoutSummaryChunkImmediately(session, wirePayload.payload.workouts ?? []);
3132
+ }
2946
3133
  const progress = updateMobileSyncSessionProgress(syncSessionId);
2947
3134
  return {
2948
3135
  accepted: true,
@@ -3639,6 +3639,7 @@ export function buildOpenApiDocument() {
3639
3639
  "schemaCatalog",
3640
3640
  "modeProfile",
3641
3641
  "modeGuideSession",
3642
+ "flashcard",
3642
3643
  "eventType",
3643
3644
  "emotionDefinition",
3644
3645
  "triggerReport"
@@ -3651,6 +3652,7 @@ export function buildOpenApiDocument() {
3651
3652
  schemaCatalog: { type: "string" },
3652
3653
  modeProfile: { type: "string" },
3653
3654
  modeGuideSession: { type: "string" },
3655
+ flashcard: { type: "string" },
3654
3656
  eventType: { type: "string" },
3655
3657
  emotionDefinition: { type: "string" },
3656
3658
  triggerReport: { type: "string" }
@@ -3663,6 +3665,7 @@ export function buildOpenApiDocument() {
3663
3665
  "focus",
3664
3666
  "useWhen",
3665
3667
  "coachingGoal",
3668
+ "openingQuestion",
3666
3669
  "askSequence",
3667
3670
  "requiredForCreate",
3668
3671
  "highValueOptionalFields",
@@ -3675,6 +3678,7 @@ export function buildOpenApiDocument() {
3675
3678
  focus: { type: "string" },
3676
3679
  useWhen: { type: "string" },
3677
3680
  coachingGoal: { type: "string" },
3681
+ openingQuestion: { type: "string" },
3678
3682
  askSequence: arrayOf({ type: "string" }),
3679
3683
  requiredForCreate: arrayOf({ type: "string" }),
3680
3684
  highValueOptionalFields: arrayOf({ type: "string" }),
@@ -4044,6 +4048,9 @@ export function buildOpenApiDocument() {
4044
4048
  "saveSuggestionTone",
4045
4049
  "maxQuestionsPerTurn",
4046
4050
  "psycheExplorationRule",
4051
+ "specializedSurfaceRule",
4052
+ "reviewShortcutRule",
4053
+ "readModelWriteRule",
4047
4054
  "psycheOpeningQuestionRule",
4048
4055
  "duplicateCheckRoute",
4049
4056
  "uiSuggestionRule",
@@ -4056,6 +4063,9 @@ export function buildOpenApiDocument() {
4056
4063
  saveSuggestionTone: { type: "string" },
4057
4064
  maxQuestionsPerTurn: { type: "integer" },
4058
4065
  psycheExplorationRule: { type: "string" },
4066
+ specializedSurfaceRule: { type: "string" },
4067
+ reviewShortcutRule: { type: "string" },
4068
+ readModelWriteRule: { type: "string" },
4059
4069
  psycheOpeningQuestionRule: { type: "string" },
4060
4070
  duplicateCheckRoute: { type: "string" },
4061
4071
  uiSuggestionRule: { type: "string" },
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.78",
5
+ "version": "0.2.80",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.78",
3
+ "version": "0.2.80",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -470,7 +470,7 @@ Useful calibration heuristics:
470
470
  once and then return to the one missing structural detail.
471
471
  - If the next question would only decorate the record and not change its usefulness,
472
472
  skip it.
473
- - If the next question would not change the API path, payload, wording, timing, or
473
+ - If the next question would not change the API path, write shape, wording, timing, or
474
474
  useful links, skip it.
475
475
 
476
476
  ## Abstract And Reusable Record Moves
@@ -485,6 +485,7 @@ reusable records.
485
485
  retrieve later.
486
486
  - For collection records, ask what they are meant to help decide before you ask what
487
487
  belongs inside them.
488
+ - For questionnaire instruments, ask what kind of honest moment or decision it should help someone answer before you ask for item wording, scale, or scoring.
488
489
  - For vocabulary records, ask what counts as inside versus outside the term before you
489
490
  settle the wording.
490
491
  - If the user already proposes a label, keep it provisional until the boundary and
@@ -1731,7 +1732,7 @@ Lane-to-route map:
1731
1732
  `DELETE /api/v1/workbench/flows/:id` for an existing saved flow
1732
1733
  - run a known flow:
1733
1734
  `/api/v1/workbench/flows/:id/run`
1734
- - run from a payload-first contract:
1735
+ - run from a one-off input contract:
1735
1736
  `/api/v1/workbench/run`
1736
1737
  - send one follow-up message into a saved flow chat:
1737
1738
  `POST /api/v1/workbench/flows/:id/chat`
@@ -1758,7 +1759,7 @@ Direct action rules:
1758
1759
  inputs, use `GET /api/v1/workbench/catalog/boxes`. Do not blur those into one vague
1759
1760
  "catalog" read when the user needs a runnable flow versus an input-box contract.
1760
1761
  - If the user wants to execute a known saved flow, use `/api/v1/workbench/flows/:id/run`.
1761
- - If the user wants payload-first execution without depending on a saved flow id, use
1762
+ - If the user wants one-off input execution without depending on a saved flow id, use
1762
1763
  `/api/v1/workbench/run`.
1763
1764
  - If the user wants to debug one failed execution, narrow whether they need the run
1764
1765
  detail, one node result, the latest node output, or the published output before you
@@ -1768,7 +1769,8 @@ Direct action rules:
1768
1769
  - If the user wants one node's latest successful output, do not browse old runs first
1769
1770
  unless they explicitly want historical debugging.
1770
1771
  - If the user wants to understand what inputs a flow can accept before editing or
1771
- running it, read the box catalog or flow detail before asking for a payload.
1772
+ running it, read the box catalog or flow detail before asking for structured
1773
+ input details.
1772
1774
  - For new flows, ask what the flow should reliably produce, what input contract it
1773
1775
  should accept, and what first node or box should anchor it. Do not start by asking
1774
1776
  for raw JSON.
@@ -1930,24 +1932,29 @@ Preferred opening question:
1930
1932
 
1931
1933
  ## Questionnaire Instrument
1932
1934
 
1933
- Aim: clarify whether the user is authoring a reusable questionnaire and what the
1934
- instrument is for.
1935
+ Aim: clarify whether the user is authoring a reusable questionnaire and what honest
1936
+ moment, pattern, or decision the instrument should help someone notice.
1935
1937
 
1936
1938
  Arc:
1937
1939
 
1938
- 1. Ask what the questionnaire is meant to measure or surface.
1940
+ 1. Ask what honest moment, pattern, or decision the questionnaire should help someone
1941
+ notice.
1939
1942
  2. Ask who it is for and when it should be used.
1940
- 3. Ask what kind of honest moment or decision it should help someone answer before
1941
- getting into item wording.
1942
- 4. Reflect the practical use case back in plain language.
1943
- 5. Ask what would make the instrument distinct instead of redundant if a near-duplicate
1944
- risk is visible.
1945
- 6. Move to draft creation once the purpose is clear.
1943
+ 3. Ask what the respondent should understand after answering that they might otherwise
1944
+ miss.
1945
+ 4. Reflect the practical use case back in plain language before asking for item
1946
+ wording.
1947
+ 5. Ask what would make the instrument distinct instead of redundant if a
1948
+ near-duplicate risk is visible.
1949
+ 6. Ask about item shape, response scale, scoring, or provenance only after the purpose
1950
+ and use context are steady.
1951
+ 7. Move to draft creation once the purpose is clear.
1946
1952
 
1947
1953
  Helpful follow-up lanes:
1948
1954
 
1949
1955
  - what honest moment, decision, or review this instrument should support
1950
1956
  - who will answer it and under what circumstances
1957
+ - what the answers should help the respondent understand or choose
1951
1958
  - what would make the instrument distinct instead of redundant
1952
1959
 
1953
1960
  Route note:
@@ -1962,15 +1969,17 @@ Ready to act when:
1962
1969
 
1963
1970
  - the purpose is clear
1964
1971
  - the audience or use context is clear
1972
+ - the respondent-facing insight or decision is clear
1965
1973
  - the instrument is distinct enough to draft
1966
1974
 
1967
1975
  Preferred opening question:
1968
1976
 
1969
- - "What would this questionnaire help someone notice or track?"
1977
+ - "What honest moment or decision should this questionnaire help someone notice or track?"
1970
1978
 
1971
1979
  ## Questionnaire Run
1972
1980
 
1973
- Aim: clarify whether the user wants to start, continue, or complete one answer session.
1981
+ Aim: clarify whether the user wants to start, continue, review, or complete one answer
1982
+ session without turning the run into a mechanical form fill.
1974
1983
 
1975
1984
  Arc:
1976
1985
 
@@ -1978,13 +1987,16 @@ Arc:
1978
1987
  2. Ask which questionnaire or existing run this is about.
1979
1988
  3. If the user wants to continue or finish, ask what feels most stuck, unfinished, or
1980
1989
  important before asking for more content.
1981
- 4. If answering is still in progress, ask only for the next answer or note that matters.
1990
+ 4. If the user is reviewing answers, ask what the run should help them understand
1991
+ before proposing edits or completion.
1992
+ 5. If answering is still in progress, ask only for the next answer or note that matters.
1982
1993
 
1983
1994
  Helpful follow-up lanes:
1984
1995
 
1985
1996
  - whether the job is to begin, resume, review, or complete
1986
1997
  - what questionnaire or run is in scope
1987
1998
  - what next answer, uncertainty, or note is actually blocking progress
1999
+ - what the completed run should help the user understand or decide
1988
2000
 
1989
2001
  Route note:
1990
2002
 
@@ -2001,7 +2013,7 @@ Ready to act when:
2001
2013
 
2002
2014
  Preferred opening question:
2003
2015
 
2004
- - "Do you want to start, continue, review, or finish a questionnaire run?"
2016
+ - "What do you want from this questionnaire run right now: start, continue, review, or finish it?"
2005
2017
 
2006
2018
  ## Event Type
2007
2019