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
|
|
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
|
|
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
|
|
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
|
|
3348
|
-
coachingGoal: "Clarify whether the user is authoring a reusable questionnaire and what the instrument
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
264
|
-
const HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES =
|
|
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:
|
|
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:
|
|
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 !==
|
|
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 (
|
|
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:
|
|
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:
|
|
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,
|
|
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" },
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.2.80",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1934
|
-
instrument
|
|
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
|
|
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
|
|
1941
|
-
|
|
1942
|
-
4. Reflect the practical use case back in plain language
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- "
|
|
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
|
|