forge-openclaw-plugin 0.2.4 → 0.2.10

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.
Files changed (114) hide show
  1. package/README.md +186 -6
  2. package/dist/assets/board-C_m78kvK.js +6 -0
  3. package/dist/assets/board-C_m78kvK.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-BWtLtXwb.js +36 -0
  6. package/dist/assets/index-BWtLtXwb.js.map +1 -0
  7. package/dist/assets/index-Dp5GXY_z.css +1 -0
  8. package/dist/assets/motion-CpZvZumD.js +10 -0
  9. package/dist/assets/motion-CpZvZumD.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-DtyXTw03.js +23 -0
  28. package/dist/assets/table-DtyXTw03.js.map +1 -0
  29. package/dist/assets/ui-BXbpiKyS.js +46 -0
  30. package/dist/assets/ui-BXbpiKyS.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-QBH6qVEe.js +433 -0
  33. package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
  34. package/dist/assets/viz-w-IMeueL.js +34 -0
  35. package/dist/assets/viz-w-IMeueL.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +9 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +136 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +63 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2487 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/demo-data.js +49 -0
  53. package/dist/server/e2e-server.js +20 -0
  54. package/dist/server/errors.js +15 -0
  55. package/dist/server/index.js +16 -0
  56. package/dist/server/managers/base.js +17 -0
  57. package/dist/server/managers/contracts.js +47 -0
  58. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  59. package/dist/server/managers/platform/audit-manager.js +15 -0
  60. package/dist/server/managers/platform/authentication-manager.js +56 -0
  61. package/dist/server/managers/platform/authorization-manager.js +56 -0
  62. package/dist/server/managers/platform/background-job-manager.js +10 -0
  63. package/dist/server/managers/platform/configuration-manager.js +33 -0
  64. package/dist/server/managers/platform/database-manager.js +14 -0
  65. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  66. package/dist/server/managers/platform/external-service-manager.js +11 -0
  67. package/dist/server/managers/platform/health-manager.js +7 -0
  68. package/dist/server/managers/platform/migration-manager.js +8 -0
  69. package/dist/server/managers/platform/search-index-manager.js +4 -0
  70. package/dist/server/managers/platform/secrets-manager.js +19 -0
  71. package/dist/server/managers/platform/session-manager.js +121 -0
  72. package/dist/server/managers/platform/storage-manager.js +16 -0
  73. package/dist/server/managers/platform/token-manager.js +37 -0
  74. package/dist/server/managers/platform/transaction-manager.js +8 -0
  75. package/dist/server/managers/platform/trusted-network.js +39 -0
  76. package/dist/server/managers/runtime.js +56 -0
  77. package/dist/server/managers/type-guards.js +4 -0
  78. package/dist/server/openapi.js +3553 -0
  79. package/dist/server/psyche-types.js +366 -0
  80. package/dist/server/repositories/activity-events.js +157 -0
  81. package/dist/server/repositories/collaboration.js +497 -0
  82. package/dist/server/repositories/deleted-entities.js +226 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +156 -0
  86. package/dist/server/repositories/notes.js +359 -0
  87. package/dist/server/repositories/projects.js +211 -0
  88. package/dist/server/repositories/psyche.js +1353 -0
  89. package/dist/server/repositories/rewards.js +675 -0
  90. package/dist/server/repositories/settings.js +399 -0
  91. package/dist/server/repositories/tags.js +160 -0
  92. package/dist/server/repositories/task-runs.js +490 -0
  93. package/dist/server/repositories/tasks.js +424 -0
  94. package/dist/server/seed-demo.js +11 -0
  95. package/dist/server/services/context.js +214 -0
  96. package/dist/server/services/dashboard.js +173 -0
  97. package/dist/server/services/entity-crud.js +573 -0
  98. package/dist/server/services/gamification.js +215 -0
  99. package/dist/server/services/insights.js +91 -0
  100. package/dist/server/services/projects.js +77 -0
  101. package/dist/server/services/psyche.js +63 -0
  102. package/dist/server/services/relations.js +28 -0
  103. package/dist/server/services/reviews.js +88 -0
  104. package/dist/server/services/run-recovery.js +13 -0
  105. package/dist/server/services/tagging.js +49 -0
  106. package/dist/server/services/task-run-watchdog.js +92 -0
  107. package/dist/server/services/work-time.js +176 -0
  108. package/dist/server/types.js +1058 -0
  109. package/dist/server/web.js +91 -0
  110. package/openclaw.plugin.json +32 -9
  111. package/package.json +17 -4
  112. package/server/migrations/001_core.sql +411 -0
  113. package/server/migrations/002_psyche.sql +392 -0
  114. package/skills/forge-openclaw/SKILL.md +197 -271
@@ -0,0 +1,675 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { recordEventLog } from "./event-log.js";
4
+ import { createManualRewardGrantSchema, rewardLedgerEventSchema, rewardRuleSchema, sessionEventSchema, updateRewardRuleSchema } from "../types.js";
5
+ const DEFAULT_RULES = [
6
+ {
7
+ id: "reward_rule_task_completion",
8
+ family: "completion",
9
+ code: "task_completion",
10
+ title: "Task completion",
11
+ description: "Award XP equal to the task points when work reaches done.",
12
+ config: { award: "task.points" }
13
+ },
14
+ {
15
+ id: "reward_rule_task_run_started",
16
+ family: "consistency",
17
+ code: "task_run_started",
18
+ title: "Task started",
19
+ description: "Award a small start bounty when real work begins on a task.",
20
+ config: { fixedXp: 8 }
21
+ },
22
+ {
23
+ id: "reward_rule_task_run_progress",
24
+ family: "consistency",
25
+ code: "task_run_progress",
26
+ title: "Work time bounty",
27
+ description: "Award a small XP bounty for each ten credited minutes of active work.",
28
+ config: { fixedXp: 4, intervalMinutes: 10 }
29
+ },
30
+ {
31
+ id: "reward_rule_task_run_completion",
32
+ family: "completion",
33
+ code: "task_run_completion",
34
+ title: "Focused run completion",
35
+ description: "Award a small bonus when a claimed execution run is completed cleanly.",
36
+ config: { fixedXp: 20 }
37
+ },
38
+ {
39
+ id: "reward_rule_insight_applied",
40
+ family: "collaboration",
41
+ code: "insight_applied",
42
+ title: "Insight applied",
43
+ description: "Reward a concrete decision to apply a useful insight.",
44
+ config: { fixedXp: 15 }
45
+ },
46
+ {
47
+ id: "reward_rule_psyche_reflection_capture",
48
+ family: "alignment",
49
+ code: "psyche_reflection_capture",
50
+ title: "Functional analysis captured",
51
+ description: "Reward a completed therapeutic reflection capture in a bounded, explainable way.",
52
+ config: { fixedXp: 8 }
53
+ },
54
+ {
55
+ id: "reward_rule_psyche_value_defined",
56
+ family: "alignment",
57
+ code: "psyche_value_defined",
58
+ title: "Value clarified",
59
+ description: "Reward the user for naming a value in concrete life language.",
60
+ config: { fixedXp: 5 }
61
+ },
62
+ {
63
+ id: "reward_rule_psyche_pattern_defined",
64
+ family: "alignment",
65
+ code: "psyche_pattern_defined",
66
+ title: "Pattern named",
67
+ description: "Reward honest identification of a recurring loop.",
68
+ config: { fixedXp: 5 }
69
+ },
70
+ {
71
+ id: "reward_rule_psyche_behavior_defined",
72
+ family: "recovery",
73
+ code: "psyche_behavior_defined",
74
+ title: "Behavior mapped",
75
+ description: "Reward mapping an away, committed, or recovery move clearly enough to work with it later.",
76
+ config: { fixedXp: 6 }
77
+ },
78
+ {
79
+ id: "reward_rule_psyche_belief_captured",
80
+ family: "alignment",
81
+ code: "psyche_belief_captured",
82
+ title: "Belief surfaced",
83
+ description: "Reward naming a belief and beginning to loosen its grip.",
84
+ config: { fixedXp: 4 }
85
+ },
86
+ {
87
+ id: "reward_rule_psyche_mode_named",
88
+ family: "consistency",
89
+ code: "psyche_mode_named",
90
+ title: "Mode mapped",
91
+ description: "Reward giving a recurring mode enough shape to recognize it later.",
92
+ config: { fixedXp: 4 }
93
+ },
94
+ {
95
+ id: "reward_rule_session_dwell",
96
+ family: "ambient",
97
+ code: "session_dwell_120",
98
+ title: "Active dwell milestone",
99
+ description: "Award a small amount of XP for sustained focused presence in the app.",
100
+ config: { fixedXp: 2, dailyCap: 12 }
101
+ },
102
+ {
103
+ id: "reward_rule_scroll_depth",
104
+ family: "ambient",
105
+ code: "scroll_depth_75",
106
+ title: "Review depth milestone",
107
+ description: "Award a bounded ambient nudge when the user actively explores the product deeply.",
108
+ config: { fixedXp: 3, dailyCap: 12 }
109
+ }
110
+ ];
111
+ function mapRule(row) {
112
+ return rewardRuleSchema.parse({
113
+ id: row.id,
114
+ family: row.family,
115
+ code: row.code,
116
+ title: row.title,
117
+ description: row.description,
118
+ active: row.active === 1,
119
+ config: JSON.parse(row.config_json),
120
+ createdAt: row.created_at,
121
+ updatedAt: row.updated_at
122
+ });
123
+ }
124
+ function mapLedger(row) {
125
+ return rewardLedgerEventSchema.parse({
126
+ id: row.id,
127
+ ruleId: row.rule_id,
128
+ eventLogId: row.event_log_id,
129
+ entityType: row.entity_type,
130
+ entityId: row.entity_id,
131
+ actor: row.actor,
132
+ source: row.source,
133
+ deltaXp: row.delta_xp,
134
+ reasonTitle: row.reason_title,
135
+ reasonSummary: row.reason_summary,
136
+ reversibleGroup: row.reversible_group,
137
+ reversedByRewardId: row.reversed_by_reward_id,
138
+ metadata: JSON.parse(row.metadata_json),
139
+ createdAt: row.created_at
140
+ });
141
+ }
142
+ function mapSession(row) {
143
+ return sessionEventSchema.parse({
144
+ id: row.id,
145
+ sessionId: row.session_id,
146
+ eventType: row.event_type,
147
+ actor: row.actor,
148
+ source: row.source,
149
+ metrics: JSON.parse(row.metrics_json),
150
+ createdAt: row.created_at
151
+ });
152
+ }
153
+ export function ensureDefaultRewardRules(now = new Date().toISOString()) {
154
+ const insert = getDatabase().prepare(`INSERT OR IGNORE INTO reward_rules (id, family, code, title, description, active, config_json, created_at, updated_at)
155
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`);
156
+ for (const rule of DEFAULT_RULES) {
157
+ insert.run(rule.id, rule.family, rule.code, rule.title, rule.description, JSON.stringify(rule.config), now, now);
158
+ }
159
+ }
160
+ export function listRewardRules() {
161
+ ensureDefaultRewardRules();
162
+ const rows = getDatabase()
163
+ .prepare(`SELECT id, family, code, title, description, active, config_json, created_at, updated_at
164
+ FROM reward_rules
165
+ ORDER BY family, created_at`)
166
+ .all();
167
+ return rows.map(mapRule);
168
+ }
169
+ export function getRewardRuleById(ruleId) {
170
+ ensureDefaultRewardRules();
171
+ const row = getDatabase()
172
+ .prepare(`SELECT id, family, code, title, description, active, config_json, created_at, updated_at
173
+ FROM reward_rules
174
+ WHERE id = ?`)
175
+ .get(ruleId);
176
+ return row ? mapRule(row) : undefined;
177
+ }
178
+ function getRuleByCode(code) {
179
+ return listRewardRules().find((rule) => rule.code === code);
180
+ }
181
+ export function updateRewardRule(ruleId, input, activity) {
182
+ ensureDefaultRewardRules();
183
+ const current = getRewardRuleById(ruleId);
184
+ if (!current) {
185
+ return undefined;
186
+ }
187
+ const parsed = updateRewardRuleSchema.parse(input);
188
+ const next = rewardRuleSchema.parse({
189
+ ...current,
190
+ ...parsed,
191
+ config: parsed.config ?? current.config,
192
+ updatedAt: new Date().toISOString()
193
+ });
194
+ getDatabase()
195
+ .prepare(`UPDATE reward_rules
196
+ SET title = ?, description = ?, active = ?, config_json = ?, updated_at = ?
197
+ WHERE id = ?`)
198
+ .run(next.title, next.description, next.active ? 1 : 0, JSON.stringify(next.config), next.updatedAt, ruleId);
199
+ recordEventLog({
200
+ eventKind: "reward.rule_updated",
201
+ entityType: "reward",
202
+ entityId: ruleId,
203
+ actor: activity.actor ?? null,
204
+ source: activity.source,
205
+ metadata: {
206
+ ruleId,
207
+ code: next.code,
208
+ active: next.active
209
+ }
210
+ });
211
+ return getRewardRuleById(ruleId);
212
+ }
213
+ function insertLedgerEvent(input, now = new Date()) {
214
+ const event = rewardLedgerEventSchema.parse({
215
+ id: `rwd_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
216
+ ruleId: input.ruleId ?? null,
217
+ eventLogId: input.eventLogId ?? null,
218
+ entityType: input.entityType,
219
+ entityId: input.entityId,
220
+ actor: input.actor ?? null,
221
+ source: input.source,
222
+ deltaXp: input.deltaXp,
223
+ reasonTitle: input.reasonTitle,
224
+ reasonSummary: input.reasonSummary ?? "",
225
+ reversibleGroup: input.reversibleGroup ?? null,
226
+ reversedByRewardId: null,
227
+ metadata: input.metadata ?? {},
228
+ createdAt: now.toISOString()
229
+ });
230
+ getDatabase()
231
+ .prepare(`INSERT INTO reward_ledger (
232
+ id, rule_id, event_log_id, entity_type, entity_id, actor, source, delta_xp, reason_title, reason_summary,
233
+ reversible_group, reversed_by_reward_id, metadata_json, created_at
234
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`)
235
+ .run(event.id, event.ruleId, event.eventLogId, event.entityType, event.entityId, event.actor, event.source, event.deltaXp, event.reasonTitle, event.reasonSummary, event.reversibleGroup, JSON.stringify(event.metadata), event.createdAt);
236
+ return event;
237
+ }
238
+ export function listRewardLedger(filters = {}) {
239
+ ensureDefaultRewardRules();
240
+ const whereClauses = [];
241
+ const params = [];
242
+ if (filters.entityType) {
243
+ whereClauses.push("entity_type = ?");
244
+ params.push(filters.entityType);
245
+ }
246
+ if (filters.entityId) {
247
+ whereClauses.push("entity_id = ?");
248
+ params.push(filters.entityId);
249
+ }
250
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
251
+ const limitSql = filters.limit ? "LIMIT ?" : "";
252
+ if (filters.limit) {
253
+ params.push(filters.limit);
254
+ }
255
+ const rows = getDatabase()
256
+ .prepare(`SELECT
257
+ id, rule_id, event_log_id, entity_type, entity_id, actor, source, delta_xp, reason_title, reason_summary,
258
+ reversible_group, reversed_by_reward_id, metadata_json, created_at
259
+ FROM reward_ledger
260
+ ${whereSql}
261
+ ORDER BY created_at DESC
262
+ ${limitSql}`)
263
+ .all(...params);
264
+ return rows.map(mapLedger);
265
+ }
266
+ export function getTotalXp() {
267
+ ensureDefaultRewardRules();
268
+ const row = getDatabase().prepare(`SELECT COALESCE(SUM(delta_xp), 0) AS total FROM reward_ledger`).get();
269
+ return row.total;
270
+ }
271
+ export function getWeeklyXp(weekStartIso) {
272
+ ensureDefaultRewardRules();
273
+ const row = getDatabase()
274
+ .prepare(`SELECT COALESCE(SUM(delta_xp), 0) AS total FROM reward_ledger WHERE created_at >= ?`)
275
+ .get(weekStartIso);
276
+ return row.total;
277
+ }
278
+ export function getDailyAmbientXp(dayKey) {
279
+ ensureDefaultRewardRules();
280
+ const row = getDatabase()
281
+ .prepare(`SELECT COALESCE(SUM(reward_ledger.delta_xp), 0) AS total
282
+ FROM reward_ledger
283
+ JOIN reward_rules ON reward_rules.id = reward_ledger.rule_id
284
+ WHERE reward_rules.family = 'ambient'
285
+ AND reward_ledger.created_at >= ?
286
+ AND reward_ledger.created_at < ?`)
287
+ .get(`${dayKey}T00:00:00.000Z`, `${dayKey}T23:59:59.999Z`);
288
+ return row.total;
289
+ }
290
+ export function awardTaskCompletionReward(task, activity) {
291
+ ensureDefaultRewardRules();
292
+ const rule = getRuleByCode("task_completion");
293
+ const eventLog = recordEventLog({
294
+ eventKind: "reward.task_completion",
295
+ entityType: "task",
296
+ entityId: task.id,
297
+ actor: activity.actor ?? null,
298
+ source: activity.source,
299
+ metadata: {
300
+ taskId: task.id,
301
+ points: task.points,
302
+ completedAt: task.completedAt ?? ""
303
+ }
304
+ });
305
+ return insertLedgerEvent({
306
+ ruleId: rule?.id ?? null,
307
+ eventLogId: eventLog.id,
308
+ entityType: "task",
309
+ entityId: task.id,
310
+ actor: activity.actor ?? null,
311
+ source: activity.source,
312
+ deltaXp: task.points,
313
+ reasonTitle: `Task completed: ${task.title}`,
314
+ reasonSummary: "Completion XP awarded from the reward engine.",
315
+ reversibleGroup: `task_completion:${task.id}:${task.completedAt ?? eventLog.createdAt}`,
316
+ metadata: {
317
+ taskId: task.id,
318
+ points: task.points
319
+ }
320
+ });
321
+ }
322
+ export function reverseLatestTaskCompletionReward(task, activity) {
323
+ ensureDefaultRewardRules();
324
+ const latest = getDatabase()
325
+ .prepare(`SELECT
326
+ id, rule_id, event_log_id, entity_type, entity_id, actor, source, delta_xp, reason_title, reason_summary,
327
+ reversible_group, reversed_by_reward_id, metadata_json, created_at
328
+ FROM reward_ledger
329
+ WHERE entity_type = 'task'
330
+ AND entity_id = ?
331
+ AND delta_xp > 0
332
+ AND reversible_group LIKE 'task_completion:%'
333
+ AND reversed_by_reward_id IS NULL
334
+ ORDER BY created_at DESC
335
+ LIMIT 1`)
336
+ .get(task.id);
337
+ if (!latest) {
338
+ return null;
339
+ }
340
+ const reversalEventLog = recordEventLog({
341
+ eventKind: "reward.task_completion_reversed",
342
+ entityType: "task",
343
+ entityId: task.id,
344
+ actor: activity.actor ?? null,
345
+ source: activity.source,
346
+ metadata: {
347
+ rewardId: latest.id,
348
+ taskId: task.id
349
+ }
350
+ });
351
+ const reversal = insertLedgerEvent({
352
+ ruleId: latest.rule_id,
353
+ eventLogId: reversalEventLog.id,
354
+ entityType: latest.entity_type,
355
+ entityId: latest.entity_id,
356
+ actor: activity.actor ?? null,
357
+ source: activity.source,
358
+ deltaXp: -Math.abs(latest.delta_xp),
359
+ reasonTitle: `Task reopened: ${task.title}`,
360
+ reasonSummary: "Completion XP reversed because the task left done.",
361
+ reversibleGroup: latest.reversible_group,
362
+ metadata: {
363
+ reversedRewardId: latest.id,
364
+ taskId: task.id
365
+ }
366
+ });
367
+ getDatabase().prepare(`UPDATE reward_ledger SET reversed_by_reward_id = ? WHERE id = ?`).run(reversal.id, latest.id);
368
+ return reversal;
369
+ }
370
+ export function recordInsightAppliedReward(insightId, entityType, entityId, activity) {
371
+ ensureDefaultRewardRules();
372
+ const rule = getRuleByCode("insight_applied");
373
+ const eventLog = recordEventLog({
374
+ eventKind: "reward.insight_applied",
375
+ entityType,
376
+ entityId,
377
+ actor: activity.actor ?? null,
378
+ source: activity.source,
379
+ metadata: {
380
+ insightId
381
+ }
382
+ });
383
+ return insertLedgerEvent({
384
+ ruleId: rule?.id ?? null,
385
+ eventLogId: eventLog.id,
386
+ entityType,
387
+ entityId,
388
+ actor: activity.actor ?? null,
389
+ source: activity.source,
390
+ deltaXp: Number(rule?.config.fixedXp ?? 15),
391
+ reasonTitle: "Insight applied",
392
+ reasonSummary: "A structured insight was accepted and marked as applied.",
393
+ reversibleGroup: `insight_applied:${insightId}`,
394
+ metadata: {
395
+ insightId
396
+ }
397
+ });
398
+ }
399
+ export function recordPsycheReflectionReward(reportId, title, activity) {
400
+ ensureDefaultRewardRules();
401
+ const rule = getRuleByCode("psyche_reflection_capture");
402
+ const eventLog = recordEventLog({
403
+ eventKind: "reward.psyche_reflection_capture",
404
+ entityType: "trigger_report",
405
+ entityId: reportId,
406
+ actor: activity.actor ?? null,
407
+ source: activity.source,
408
+ metadata: {
409
+ triggerReportId: reportId,
410
+ title
411
+ }
412
+ });
413
+ return insertLedgerEvent({
414
+ ruleId: rule?.id ?? null,
415
+ eventLogId: eventLog.id,
416
+ entityType: "trigger_report",
417
+ entityId: reportId,
418
+ actor: activity.actor ?? null,
419
+ source: activity.source,
420
+ deltaXp: Number(rule?.config.fixedXp ?? 8),
421
+ reasonTitle: `Psyche reflection captured: ${title}`,
422
+ reasonSummary: "A structured trigger report was stored and the reflection ledger was updated.",
423
+ reversibleGroup: `psyche_reflection_capture:${reportId}`,
424
+ metadata: {
425
+ triggerReportId: reportId
426
+ }
427
+ });
428
+ }
429
+ export function recordPsycheClarityReward(entityType, entityId, title, ruleCode, activity) {
430
+ ensureDefaultRewardRules();
431
+ const rule = getRuleByCode(ruleCode);
432
+ const eventLog = recordEventLog({
433
+ eventKind: `reward.${ruleCode}`,
434
+ entityType,
435
+ entityId,
436
+ actor: activity.actor ?? null,
437
+ source: activity.source,
438
+ metadata: {
439
+ entityId,
440
+ entityType,
441
+ title
442
+ }
443
+ });
444
+ return insertLedgerEvent({
445
+ ruleId: rule?.id ?? null,
446
+ eventLogId: eventLog.id,
447
+ entityType,
448
+ entityId,
449
+ actor: activity.actor ?? null,
450
+ source: activity.source,
451
+ deltaXp: Number(rule?.config.fixedXp ?? 4),
452
+ reasonTitle: rule?.title ?? "Psyche clarity gained",
453
+ reasonSummary: rule?.description ?? "A Psyche entity was clarified and stored.",
454
+ reversibleGroup: `${ruleCode}:${entityId}`,
455
+ metadata: {
456
+ entityType,
457
+ title
458
+ }
459
+ });
460
+ }
461
+ export function recordTaskRunCompletionReward(taskRunId, taskId, actor, source) {
462
+ ensureDefaultRewardRules();
463
+ const rule = getRuleByCode("task_run_completion");
464
+ const eventLog = recordEventLog({
465
+ eventKind: "reward.task_run_completion",
466
+ entityType: "task_run",
467
+ entityId: taskRunId,
468
+ actor,
469
+ source,
470
+ metadata: {
471
+ taskId,
472
+ taskRunId
473
+ }
474
+ });
475
+ return insertLedgerEvent({
476
+ ruleId: rule?.id ?? null,
477
+ eventLogId: eventLog.id,
478
+ entityType: "task_run",
479
+ entityId: taskRunId,
480
+ actor,
481
+ source,
482
+ deltaXp: Number(rule?.config.fixedXp ?? 20),
483
+ reasonTitle: rule?.title ?? "Focused run completion",
484
+ reasonSummary: rule?.description ?? "A claimed execution run was completed.",
485
+ reversibleGroup: `task_run_completion:${taskRunId}`,
486
+ metadata: {
487
+ taskId,
488
+ taskRunId
489
+ }
490
+ });
491
+ }
492
+ export function recordTaskRunStartReward(taskRunId, taskId, actor, source) {
493
+ ensureDefaultRewardRules();
494
+ const rule = getRuleByCode("task_run_started");
495
+ const eventLog = recordEventLog({
496
+ eventKind: "reward.task_run_started",
497
+ entityType: "task_run",
498
+ entityId: taskRunId,
499
+ actor,
500
+ source,
501
+ metadata: {
502
+ taskId,
503
+ taskRunId
504
+ }
505
+ });
506
+ return insertLedgerEvent({
507
+ ruleId: rule?.id ?? null,
508
+ eventLogId: eventLog.id,
509
+ entityType: "task_run",
510
+ entityId: taskRunId,
511
+ actor,
512
+ source,
513
+ deltaXp: Number(rule?.config.fixedXp ?? 8),
514
+ reasonTitle: rule?.title ?? "Task started",
515
+ reasonSummary: rule?.description ?? "A live work timer was started for a task.",
516
+ reversibleGroup: `task_run_started:${taskRunId}`,
517
+ metadata: {
518
+ taskId,
519
+ taskRunId
520
+ }
521
+ });
522
+ }
523
+ export function recordTaskRunProgressRewards(taskRunId, taskId, actor, source, creditedSeconds) {
524
+ ensureDefaultRewardRules();
525
+ const rule = getRuleByCode("task_run_progress");
526
+ const intervalMinutes = Math.max(1, Number(rule?.config.intervalMinutes ?? 10));
527
+ const intervalSeconds = intervalMinutes * 60;
528
+ const fixedXp = Number(rule?.config.fixedXp ?? 4);
529
+ const earnedBuckets = Math.floor(Math.max(0, creditedSeconds) / intervalSeconds);
530
+ if (earnedBuckets <= 0) {
531
+ return [];
532
+ }
533
+ const existingCount = getDatabase()
534
+ .prepare(`SELECT COUNT(*) AS count
535
+ FROM reward_ledger
536
+ WHERE entity_type = 'task_run'
537
+ AND entity_id = ?
538
+ AND reversible_group LIKE ?`)
539
+ .get(taskRunId, `task_run_progress:${taskRunId}:%`).count;
540
+ if (existingCount >= earnedBuckets) {
541
+ return [];
542
+ }
543
+ const rewards = [];
544
+ for (let bucketIndex = existingCount + 1; bucketIndex <= earnedBuckets; bucketIndex += 1) {
545
+ const creditedMinutes = bucketIndex * intervalMinutes;
546
+ const eventLog = recordEventLog({
547
+ eventKind: "reward.task_run_progress",
548
+ entityType: "task_run",
549
+ entityId: taskRunId,
550
+ actor,
551
+ source,
552
+ metadata: {
553
+ taskId,
554
+ taskRunId,
555
+ bucketIndex,
556
+ creditedMinutes
557
+ }
558
+ });
559
+ rewards.push(insertLedgerEvent({
560
+ ruleId: rule?.id ?? null,
561
+ eventLogId: eventLog.id,
562
+ entityType: "task_run",
563
+ entityId: taskRunId,
564
+ actor,
565
+ source,
566
+ deltaXp: fixedXp,
567
+ reasonTitle: rule?.title ?? "Work time bounty",
568
+ reasonSummary: `Awarded after ${creditedMinutes} credited minutes of active work.`,
569
+ reversibleGroup: `task_run_progress:${taskRunId}:${bucketIndex}`,
570
+ metadata: {
571
+ taskId,
572
+ taskRunId,
573
+ bucketIndex,
574
+ creditedMinutes
575
+ }
576
+ }));
577
+ }
578
+ return rewards;
579
+ }
580
+ export function recordSessionEvent(input, activity, now = new Date()) {
581
+ ensureDefaultRewardRules();
582
+ const sessionEvent = sessionEventSchema.parse({
583
+ id: `ses_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
584
+ sessionId: input.sessionId,
585
+ eventType: input.eventType,
586
+ actor: activity.actor ?? null,
587
+ source: activity.source,
588
+ metrics: input.metrics,
589
+ createdAt: now.toISOString()
590
+ });
591
+ getDatabase()
592
+ .prepare(`INSERT INTO session_events (id, session_id, event_type, actor, source, metrics_json, created_at)
593
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
594
+ .run(sessionEvent.id, sessionEvent.sessionId, sessionEvent.eventType, sessionEvent.actor, sessionEvent.source, JSON.stringify(sessionEvent.metrics), sessionEvent.createdAt);
595
+ recordEventLog({
596
+ eventKind: `session.${sessionEvent.eventType}`,
597
+ entityType: "session",
598
+ entityId: sessionEvent.id,
599
+ actor: sessionEvent.actor,
600
+ source: sessionEvent.source,
601
+ metadata: {
602
+ sessionId: sessionEvent.sessionId
603
+ }
604
+ }, now);
605
+ const day = sessionEvent.createdAt.slice(0, 10);
606
+ const currentAmbientXp = getDailyAmbientXp(day);
607
+ const active = sessionEvent.metrics.visible === true && sessionEvent.metrics.interacted === true;
608
+ const ruleCode = sessionEvent.eventType === "dwell_120_seconds"
609
+ ? "session_dwell_120"
610
+ : sessionEvent.eventType === "scroll_depth_75"
611
+ ? "scroll_depth_75"
612
+ : null;
613
+ const rule = ruleCode ? getRuleByCode(ruleCode) : null;
614
+ const dailyCap = Number(rule?.config.dailyCap ?? 12);
615
+ const awardXp = Number(rule?.config.fixedXp ?? 0);
616
+ if (!rule || !active || currentAmbientXp >= dailyCap) {
617
+ return { sessionEvent, rewardEvent: null };
618
+ }
619
+ const rewardEvent = insertLedgerEvent({
620
+ ruleId: rule.id,
621
+ entityType: "session",
622
+ entityId: sessionEvent.id,
623
+ actor: activity.actor ?? null,
624
+ source: activity.source,
625
+ deltaXp: Math.max(0, Math.min(awardXp, dailyCap - currentAmbientXp)),
626
+ reasonTitle: rule.title,
627
+ reasonSummary: rule.description,
628
+ reversibleGroup: `session:${sessionEvent.id}:${rule.code}`,
629
+ metadata: {
630
+ sessionId: sessionEvent.sessionId,
631
+ eventType: sessionEvent.eventType
632
+ }
633
+ }, now);
634
+ return { sessionEvent, rewardEvent };
635
+ }
636
+ export function listSessionEvents(limit = 50) {
637
+ const rows = getDatabase()
638
+ .prepare(`SELECT id, session_id, event_type, actor, source, metrics_json, created_at
639
+ FROM session_events
640
+ ORDER BY created_at DESC
641
+ LIMIT ?`)
642
+ .all(limit);
643
+ return rows.map(mapSession);
644
+ }
645
+ export function createManualRewardGrant(input, activity) {
646
+ ensureDefaultRewardRules();
647
+ const parsed = createManualRewardGrantSchema.parse(input);
648
+ const eventLog = recordEventLog({
649
+ eventKind: "reward.manual_bonus",
650
+ entityType: parsed.entityType,
651
+ entityId: parsed.entityId,
652
+ actor: activity.actor ?? null,
653
+ source: activity.source,
654
+ metadata: {
655
+ deltaXp: parsed.deltaXp,
656
+ reasonTitle: parsed.reasonTitle
657
+ }
658
+ });
659
+ return insertLedgerEvent({
660
+ ruleId: null,
661
+ eventLogId: eventLog.id,
662
+ entityType: parsed.entityType,
663
+ entityId: parsed.entityId,
664
+ actor: activity.actor ?? null,
665
+ source: activity.source,
666
+ deltaXp: parsed.deltaXp,
667
+ reasonTitle: parsed.reasonTitle,
668
+ reasonSummary: parsed.reasonSummary,
669
+ reversibleGroup: `manual_bonus:${parsed.entityType}:${parsed.entityId}:${eventLog.id}`,
670
+ metadata: {
671
+ manual: true,
672
+ ...parsed.metadata
673
+ }
674
+ });
675
+ }