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.
- package/README.md +186 -6
- package/dist/assets/board-C_m78kvK.js +6 -0
- package/dist/assets/board-C_m78kvK.js.map +1 -0
- package/dist/assets/favicon-BCHm9dUV.ico +0 -0
- package/dist/assets/index-BWtLtXwb.js +36 -0
- package/dist/assets/index-BWtLtXwb.js.map +1 -0
- package/dist/assets/index-Dp5GXY_z.css +1 -0
- package/dist/assets/motion-CpZvZumD.js +10 -0
- package/dist/assets/motion-CpZvZumD.js.map +1 -0
- package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
- package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/assets/table-DtyXTw03.js +23 -0
- package/dist/assets/table-DtyXTw03.js.map +1 -0
- package/dist/assets/ui-BXbpiKyS.js +46 -0
- package/dist/assets/ui-BXbpiKyS.js.map +1 -0
- package/dist/assets/vendor-CRS-psbw.css +1 -0
- package/dist/assets/vendor-QBH6qVEe.js +433 -0
- package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
- package/dist/assets/viz-w-IMeueL.js +34 -0
- package/dist/assets/viz-w-IMeueL.js.map +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +29 -0
- package/dist/openclaw/api-client.d.ts +9 -0
- package/dist/openclaw/api-client.js +31 -4
- package/dist/openclaw/local-runtime.d.ts +3 -0
- package/dist/openclaw/local-runtime.js +136 -0
- package/dist/openclaw/parity.d.ts +4 -4
- package/dist/openclaw/parity.js +23 -33
- package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
- package/dist/openclaw/plugin-entry-shared.js +63 -9
- package/dist/openclaw/routes.d.ts +12 -3
- package/dist/openclaw/routes.js +156 -924
- package/dist/openclaw/tools.js +242 -1100
- package/dist/server/app.js +2487 -0
- package/dist/server/db.js +313 -0
- package/dist/server/demo-data.js +49 -0
- package/dist/server/e2e-server.js +20 -0
- package/dist/server/errors.js +15 -0
- package/dist/server/index.js +16 -0
- package/dist/server/managers/base.js +17 -0
- package/dist/server/managers/contracts.js +47 -0
- package/dist/server/managers/platform/api-gateway-manager.js +11 -0
- package/dist/server/managers/platform/audit-manager.js +15 -0
- package/dist/server/managers/platform/authentication-manager.js +56 -0
- package/dist/server/managers/platform/authorization-manager.js +56 -0
- package/dist/server/managers/platform/background-job-manager.js +10 -0
- package/dist/server/managers/platform/configuration-manager.js +33 -0
- package/dist/server/managers/platform/database-manager.js +14 -0
- package/dist/server/managers/platform/event-bus-manager.js +7 -0
- package/dist/server/managers/platform/external-service-manager.js +11 -0
- package/dist/server/managers/platform/health-manager.js +7 -0
- package/dist/server/managers/platform/migration-manager.js +8 -0
- package/dist/server/managers/platform/search-index-manager.js +4 -0
- package/dist/server/managers/platform/secrets-manager.js +19 -0
- package/dist/server/managers/platform/session-manager.js +121 -0
- package/dist/server/managers/platform/storage-manager.js +16 -0
- package/dist/server/managers/platform/token-manager.js +37 -0
- package/dist/server/managers/platform/transaction-manager.js +8 -0
- package/dist/server/managers/platform/trusted-network.js +39 -0
- package/dist/server/managers/runtime.js +56 -0
- package/dist/server/managers/type-guards.js +4 -0
- package/dist/server/openapi.js +3553 -0
- package/dist/server/psyche-types.js +366 -0
- package/dist/server/repositories/activity-events.js +157 -0
- package/dist/server/repositories/collaboration.js +497 -0
- package/dist/server/repositories/deleted-entities.js +226 -0
- package/dist/server/repositories/domains.js +30 -0
- package/dist/server/repositories/event-log.js +64 -0
- package/dist/server/repositories/goals.js +156 -0
- package/dist/server/repositories/notes.js +359 -0
- package/dist/server/repositories/projects.js +211 -0
- package/dist/server/repositories/psyche.js +1353 -0
- package/dist/server/repositories/rewards.js +675 -0
- package/dist/server/repositories/settings.js +399 -0
- package/dist/server/repositories/tags.js +160 -0
- package/dist/server/repositories/task-runs.js +490 -0
- package/dist/server/repositories/tasks.js +424 -0
- package/dist/server/seed-demo.js +11 -0
- package/dist/server/services/context.js +214 -0
- package/dist/server/services/dashboard.js +173 -0
- package/dist/server/services/entity-crud.js +573 -0
- package/dist/server/services/gamification.js +215 -0
- package/dist/server/services/insights.js +91 -0
- package/dist/server/services/projects.js +77 -0
- package/dist/server/services/psyche.js +63 -0
- package/dist/server/services/relations.js +28 -0
- package/dist/server/services/reviews.js +88 -0
- package/dist/server/services/run-recovery.js +13 -0
- package/dist/server/services/tagging.js +49 -0
- package/dist/server/services/task-run-watchdog.js +92 -0
- package/dist/server/services/work-time.js +176 -0
- package/dist/server/types.js +1058 -0
- package/dist/server/web.js +91 -0
- package/openclaw.plugin.json +32 -9
- package/package.json +17 -4
- package/server/migrations/001_core.sql +411 -0
- package/server/migrations/002_psyche.sql +392 -0
- 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
|
+
}
|