forge-openclaw-plugin 0.2.60 → 0.2.65

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 (59) hide show
  1. package/README.md +121 -51
  2. package/dist/assets/{board-B1V3M__K.js → board-DUwMfZvN.js} +1 -1
  3. package/dist/assets/index-B9VOpR7r.css +1 -0
  4. package/dist/assets/index-DoHjjze2.js +90 -0
  5. package/dist/assets/{motion-CltSTItx.js → motion-Crg3QyXD.js} +1 -1
  6. package/dist/assets/{table-B-VrSFx8.js → table-CTlDeYRs.js} +1 -1
  7. package/dist/assets/{ui-DUqM4jkt.js → ui-CJPaElbj.js} +1 -1
  8. package/dist/assets/{vendor-C0otBhgu.js → vendor-BdrT2htV.js} +217 -207
  9. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  11. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  12. package/dist/companion-iroh-src/Cargo.lock +4559 -0
  13. package/dist/companion-iroh-src/Cargo.toml +37 -0
  14. package/dist/companion-iroh-src/src/lib.rs +279 -0
  15. package/dist/companion-iroh-src/src/main.rs +478 -0
  16. package/dist/companion-iroh-src/src/protocol.rs +129 -0
  17. package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
  18. package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
  19. package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
  20. package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
  21. package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
  22. package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
  23. package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
  24. package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
  25. package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
  26. package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
  27. package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
  28. package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
  29. package/dist/index.html +7 -7
  30. package/dist/openclaw/parity.js +27 -0
  31. package/dist/openclaw/plugin-entry-shared.js +2 -2
  32. package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
  33. package/dist/openclaw/routes.d.ts +4 -0
  34. package/dist/openclaw/routes.js +112 -3
  35. package/dist/openclaw/tools.js +32 -4
  36. package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
  37. package/dist/server/server/src/app.js +288 -61
  38. package/dist/server/server/src/data-management-types.js +2 -0
  39. package/dist/server/server/src/discovery-advertiser.js +13 -0
  40. package/dist/server/server/src/health.js +58 -3
  41. package/dist/server/server/src/movement.js +16 -1
  42. package/dist/server/server/src/openapi.js +410 -9
  43. package/dist/server/server/src/repositories/rewards.js +60 -0
  44. package/dist/server/server/src/services/companion-iroh.js +425 -0
  45. package/dist/server/server/src/services/data-management.js +32 -2
  46. package/dist/server/server/src/services/doctor.js +762 -0
  47. package/dist/server/server/src/services/gamification.js +75 -3
  48. package/dist/server/server/src/services/life-force.js +166 -25
  49. package/dist/server/server/src/web.js +88 -12
  50. package/dist/server/src/lib/api.js +9 -0
  51. package/dist/server/src/lib/gamification-catalog.js +1 -1
  52. package/openclaw.plugin.json +85 -3
  53. package/package.json +10 -6
  54. package/server/migrations/059_data_backup_retention.sql +2 -0
  55. package/skills/forge-openclaw/SKILL.md +80 -19
  56. package/skills/forge-openclaw/entity_conversation_playbooks.md +283 -25
  57. package/skills/forge-openclaw/psyche_entity_playbooks.md +82 -0
  58. package/dist/assets/index-BwKAPo98.css +0 -1
  59. package/dist/assets/index-Dy7c-dRY.js +0 -90
@@ -1,10 +1,41 @@
1
1
  import { getDatabase } from "../db.js";
2
2
  import { enqueueGamificationCelebration, getGamificationEquipment, insertGamificationUnlock, listGamificationDailyActivity, listGamificationUnlocks, listUnseenGamificationCelebrations, replaceGamificationDailyActivity, upsertGamificationEquipment } from "../repositories/gamification.js";
3
- import { getDailyAmbientXp, listRewardRules } from "../repositories/rewards.js";
3
+ import { getDailyAmbientXp, listRewardRules, recordEntityCreationReward } from "../repositories/rewards.js";
4
4
  import { getDefaultUser, listUsers, listUsersByIds } from "../repositories/users.js";
5
5
  import { GAMIFICATION_CATALOG, GAMIFICATION_STREAK_AWAY_DAY_KEYS, GAMIFICATION_STREAK_POWER_DAY_KEYS } from "../../../src/lib/gamification-catalog.js";
6
6
  import { achievementSignalSchema, gamificationCatalogPayloadSchema, gamificationProfileSchema, milestoneRewardSchema, rewardLedgerEventSchema } from "../types.js";
7
7
  const XP_CURVE_VERSION = "smith-forge";
8
+ const ENTITY_CREATION_REWARD_SOURCES = [
9
+ { entityType: "goal", tableName: "goals", titleColumn: "title" },
10
+ { entityType: "project", tableName: "projects", titleColumn: "title" },
11
+ { entityType: "strategy", tableName: "strategies", titleColumn: "title" },
12
+ { entityType: "task", tableName: "tasks", titleColumn: "title" },
13
+ { entityType: "habit", tableName: "habits", titleColumn: "title" },
14
+ { entityType: "note", tableName: "notes", titleColumn: "content_plain" },
15
+ { entityType: "tag", tableName: "tags", titleColumn: "name" },
16
+ { entityType: "calendar_event", tableName: "calendar_events", titleColumn: "title" },
17
+ {
18
+ entityType: "work_block_template",
19
+ tableName: "work_block_templates",
20
+ titleColumn: "title"
21
+ },
22
+ { entityType: "task_timebox", tableName: "task_timeboxes", titleColumn: "title" },
23
+ {
24
+ entityType: "questionnaire_instrument",
25
+ tableName: "questionnaire_instruments",
26
+ titleColumn: "title"
27
+ },
28
+ { entityType: "psyche_value", tableName: "psyche_values", titleColumn: "title" },
29
+ {
30
+ entityType: "behavior_pattern",
31
+ tableName: "behavior_patterns",
32
+ titleColumn: "title"
33
+ },
34
+ { entityType: "behavior", tableName: "psyche_behaviors", titleColumn: "title" },
35
+ { entityType: "belief_entry", tableName: "belief_entries", titleColumn: "statement" },
36
+ { entityType: "mode_profile", tableName: "mode_profiles", titleColumn: "title" },
37
+ { entityType: "trigger_report", tableName: "trigger_reports", titleColumn: "title" }
38
+ ];
8
39
  function startOfWeek(date) {
9
40
  const clone = new Date(date);
10
41
  const day = clone.getDay();
@@ -209,6 +240,42 @@ function loadScopedRewardEvents(scope) {
209
240
  ? true
210
241
  : event.ownerUserId !== null && scopeUserIds.has(event.ownerUserId));
211
242
  }
243
+ function syncEntityCreationRewards(scope) {
244
+ const database = getDatabase();
245
+ const scopeUserIds = [...new Set(scope.userIds)];
246
+ const scopePlaceholders = scopeUserIds.map(() => "?").join(", ");
247
+ for (const source of ENTITY_CREATION_REWARD_SOURCES) {
248
+ const scopedWhere = scopeUserIds.length > 0
249
+ ? `WHERE (
250
+ entity_owners.user_id IN (${scopePlaceholders})
251
+ OR (entity_owners.user_id IS NULL AND ? IS NOT NULL)
252
+ )`
253
+ : "";
254
+ const params = scopeUserIds.length > 0
255
+ ? [source.entityType, ...scopeUserIds, scopeUserIds[0] ?? null]
256
+ : [source.entityType];
257
+ const rows = database
258
+ .prepare(`SELECT
259
+ ${source.tableName}.id AS id,
260
+ ${source.tableName}.${source.titleColumn} AS title,
261
+ ${source.tableName}.created_at AS created_at
262
+ FROM ${source.tableName}
263
+ LEFT JOIN entity_owners
264
+ ON entity_owners.entity_type = ?
265
+ AND entity_owners.entity_id = ${source.tableName}.id
266
+ ${scopedWhere}`)
267
+ .all(...params);
268
+ for (const row of rows) {
269
+ recordEntityCreationReward({
270
+ entityType: source.entityType,
271
+ entityId: row.id,
272
+ title: row.title,
273
+ source: "system",
274
+ createdAt: row.created_at
275
+ });
276
+ }
277
+ }
278
+ }
212
279
  function isQualifyingStreakReward(event) {
213
280
  return (event.deltaXp > 0 &&
214
281
  event.reversedByRewardId === null &&
@@ -245,8 +312,9 @@ function syncDailyActivity(userId, scopedRewards, timezone) {
245
312
  }
246
313
  function calculateStreakFromActivity(activeDateKeys, now, timezone) {
247
314
  const today = dateKeyInTimezone(now, timezone);
315
+ const yesterday = subtractDaysFromDateKey(today, 1);
248
316
  let streak = 0;
249
- let cursor = today;
317
+ let cursor = activeDateKeys.has(today) ? today : yesterday;
250
318
  while (activeDateKeys.has(cursor)) {
251
319
  streak += 1;
252
320
  cursor = subtractDaysFromDateKey(cursor, 1);
@@ -297,8 +365,11 @@ function calculateMissedDays(activeDateKeys, now, timezone) {
297
365
  if (!latest || latest === today) {
298
366
  return { missedDays: 0, lastActiveDateKey: latest };
299
367
  }
368
+ if (latest === subtractDaysFromDateKey(today, 1)) {
369
+ return { missedDays: 0, lastActiveDateKey: latest };
370
+ }
300
371
  return {
301
- missedDays: Math.max(0, daysBetweenDateKeys(latest, today)),
372
+ missedDays: Math.max(0, daysBetweenDateKeys(latest, today) - 1),
302
373
  lastActiveDateKey: latest
303
374
  };
304
375
  }
@@ -746,6 +817,7 @@ function syncCatalog(input) {
746
817
  function buildGamificationState(goals, tasks, habits, options = {}) {
747
818
  const now = options.now ?? new Date();
748
819
  const scope = resolveGamificationScope(options.userIds);
820
+ syncEntityCreationRewards(scope);
749
821
  const scopedRewards = loadScopedRewardEvents(scope);
750
822
  const timezone = resolveTimezone();
751
823
  const primaryUserId = scope.userIds[0] ?? "aggregate";
@@ -23,6 +23,73 @@ const LIFE_FORCE_STAT_LABELS = {
23
23
  composure: "Composure",
24
24
  flow: "Flow"
25
25
  };
26
+ const AGENT_ACTOR_PATTERN = /\b(codex|hermes|openclaw|agent|bot)\b|aurel\s+the\s+bot/i;
27
+ const PASSIVE_CALENDAR_PATTERN = /\b(vacation|vacances?|cong[eé]s?|absence|holiday|out\s+of\s+office|ooo|away|leave|off)\b/i;
28
+ function isAgentAuthoredActivity(input) {
29
+ if (input.source === "agent") {
30
+ return true;
31
+ }
32
+ return AGENT_ACTOR_PATTERN.test(input.actor ?? "");
33
+ }
34
+ function agentSupervisionMultiplier(input) {
35
+ if (!isAgentAuthoredActivity(input)) {
36
+ return 1;
37
+ }
38
+ return input.goalLinked ? 0.05 : 0.15;
39
+ }
40
+ function noteApMultiplier(input) {
41
+ const author = input.author?.trim().toLowerCase() ?? "";
42
+ if (input.source === "system" && author === "movement sync") {
43
+ return 0;
44
+ }
45
+ return isAgentAuthoredActivity({ actor: input.author, source: input.source })
46
+ ? 0.15
47
+ : 1;
48
+ }
49
+ function calendarCategoriesText(raw) {
50
+ try {
51
+ const parsed = JSON.parse(raw);
52
+ return Array.isArray(parsed) ? parsed.join(" ") : "";
53
+ }
54
+ catch {
55
+ return "";
56
+ }
57
+ }
58
+ function isPassiveCalendarContainer(row, profile) {
59
+ const hasManualOrCustomApProfile = profile?.sourceMethod === "manual" ||
60
+ (profile?.metadata.customSustainRateApPerHour !== null &&
61
+ profile?.metadata.customSustainRateApPerHour !== undefined);
62
+ if (hasManualOrCustomApProfile) {
63
+ return false;
64
+ }
65
+ const durationHours = Math.max(0, (Date.parse(row.end_at) - Date.parse(row.start_at)) / 3_600_000);
66
+ const searchable = [
67
+ row.title,
68
+ row.event_type,
69
+ calendarCategoriesText(row.categories_json)
70
+ ].join(" ");
71
+ if (row.availability === "free") {
72
+ return true;
73
+ }
74
+ if (row.is_all_day === 1) {
75
+ return true;
76
+ }
77
+ return durationHours >= 12 && PASSIVE_CALENDAR_PATTERN.test(searchable);
78
+ }
79
+ function scaleContributionAp(contribution, multiplier, reason) {
80
+ if (multiplier >= 1) {
81
+ return contribution;
82
+ }
83
+ return {
84
+ ...contribution,
85
+ totalAp: Number((contribution.totalAp * Math.max(0, multiplier)).toFixed(4)),
86
+ why: `${contribution.why} ${reason}`,
87
+ metadata: {
88
+ ...(contribution.metadata ?? {}),
89
+ personalApMultiplier: multiplier
90
+ }
91
+ };
92
+ }
26
93
  const CALENDAR_ACTIVITY_PRESETS = {
27
94
  deep_work: {
28
95
  title: "Deep work",
@@ -987,6 +1054,7 @@ function readTaskRunRows(range, userId) {
987
1054
  task_runs.timed_out_at,
988
1055
  task_runs.updated_at,
989
1056
  tasks.title AS task_title,
1057
+ tasks.goal_id AS task_goal_id,
990
1058
  task_runs.planned_duration_seconds,
991
1059
  tasks.planned_duration_seconds AS task_expected_duration_seconds
992
1060
  FROM task_runs
@@ -1103,7 +1171,39 @@ function getOrCreateDaySnapshot(userId, date) {
1103
1171
  WHERE user_id = ? AND date_key = ?`)
1104
1172
  .get(userId, range.dateKey);
1105
1173
  if (existing) {
1106
- return existing;
1174
+ const profile = ensureLifeForceProfile(userId);
1175
+ const template = readWeekdayTemplate(userId, date.getUTCDay());
1176
+ const sleepRecoveryMultiplier = computeSleepRecoveryMultiplier(userId, date);
1177
+ const fatigueDebtCarry = computeFatigueDebtCarry(userId, date);
1178
+ const readinessMultiplier = profile.readiness_multiplier;
1179
+ const dailyBudgetAp = Math.max(40, Math.round(profile.base_daily_ap *
1180
+ computeLifeForceMultiplier(profile) *
1181
+ sleepRecoveryMultiplier *
1182
+ readinessMultiplier) - fatigueDebtCarry);
1183
+ const derivedChanged = Math.abs(existing.daily_budget_ap - dailyBudgetAp) > 0.01 ||
1184
+ Math.abs(existing.sleep_recovery_multiplier - sleepRecoveryMultiplier) > 0.001 ||
1185
+ Math.abs(existing.readiness_multiplier - readinessMultiplier) > 0.001 ||
1186
+ Math.abs(existing.fatigue_debt_carry - fatigueDebtCarry) > 0.01;
1187
+ if (!derivedChanged) {
1188
+ return existing;
1189
+ }
1190
+ const points = normalizeCurveToBudget(parseCurvePoints(template.points_json), dailyBudgetAp);
1191
+ const updatedAt = nowIso();
1192
+ getDatabase()
1193
+ .prepare(`UPDATE life_force_day_snapshots
1194
+ SET daily_budget_ap = ?,
1195
+ sleep_recovery_multiplier = ?,
1196
+ readiness_multiplier = ?,
1197
+ fatigue_debt_carry = ?,
1198
+ points_json = ?,
1199
+ updated_at = ?
1200
+ WHERE id = ?`)
1201
+ .run(dailyBudgetAp, sleepRecoveryMultiplier, readinessMultiplier, fatigueDebtCarry, JSON.stringify(points), updatedAt, existing.id);
1202
+ return getDatabase()
1203
+ .prepare(`SELECT *
1204
+ FROM life_force_day_snapshots
1205
+ WHERE id = ?`)
1206
+ .get(existing.id);
1107
1207
  }
1108
1208
  const profile = ensureLifeForceProfile(userId);
1109
1209
  const template = readWeekdayTemplate(userId, date.getUTCDay());
@@ -1190,8 +1290,11 @@ function readTodayAdjustmentRows(userId, range) {
1190
1290
  work_adjustments.entity_id,
1191
1291
  work_adjustments.applied_delta_minutes,
1192
1292
  work_adjustments.note,
1293
+ work_adjustments.actor,
1294
+ work_adjustments.source,
1193
1295
  work_adjustments.created_at,
1194
- tasks.planned_duration_seconds
1296
+ tasks.planned_duration_seconds,
1297
+ tasks.goal_id AS task_goal_id
1195
1298
  FROM work_adjustments
1196
1299
  LEFT JOIN tasks
1197
1300
  ON work_adjustments.entity_type = 'task'
@@ -1216,7 +1319,11 @@ function readTodayAdjustmentApByTaskId(userId, range, lifeForceProfile) {
1216
1319
  id: row.entity_id,
1217
1320
  plannedDurationSeconds: row.planned_duration_seconds
1218
1321
  }, lifeForceProfile);
1219
- const deltaAp = rateToTotalAp(profile.sustainRateApPerHour, row.applied_delta_minutes * 60);
1322
+ const deltaAp = rateToTotalAp(profile.sustainRateApPerHour, row.applied_delta_minutes * 60) * agentSupervisionMultiplier({
1323
+ actor: row.actor,
1324
+ source: row.source,
1325
+ goalLinked: Boolean(row.task_goal_id)
1326
+ });
1220
1327
  totals.set(row.entity_id, (totals.get(row.entity_id) ?? 0) + deltaAp);
1221
1328
  }
1222
1329
  return totals;
@@ -1301,7 +1408,7 @@ function buildWorkAdjustmentContributions(userId, range, lifeForceProfile) {
1301
1408
  plannedDurationSeconds: row.planned_duration_seconds
1302
1409
  }, lifeForceProfile);
1303
1410
  const totalAp = rateToTotalAp(profile.sustainRateApPerHour, row.applied_delta_minutes * 60);
1304
- return {
1411
+ return scaleContributionAp({
1305
1412
  entityType: "task",
1306
1413
  entityId: row.entity_id,
1307
1414
  eventKind: "work_adjustment",
@@ -1315,9 +1422,15 @@ function buildWorkAdjustmentContributions(userId, range, lifeForceProfile) {
1315
1422
  role: "background",
1316
1423
  metadata: {
1317
1424
  adjustmentId: row.id,
1318
- appliedDeltaMinutes: row.applied_delta_minutes
1425
+ appliedDeltaMinutes: row.applied_delta_minutes,
1426
+ actor: row.actor,
1427
+ source: row.source
1319
1428
  }
1320
- };
1429
+ }, agentSupervisionMultiplier({
1430
+ actor: row.actor,
1431
+ source: row.source,
1432
+ goalLinked: Boolean(row.task_goal_id)
1433
+ }), "Agent-authored manual work is charged as light supervision AP instead of full human effort.");
1321
1434
  });
1322
1435
  }
1323
1436
  function buildTaskRunContributions(userId, range, now, lifeForceProfile) {
@@ -1336,7 +1449,7 @@ function buildTaskRunContributions(userId, range, now, lifeForceProfile) {
1336
1449
  const totalAp = rateToTotalAp(profile.sustainRateApPerHour, seconds);
1337
1450
  const startsAt = new Date(Math.max(range.startMs, Date.parse(row.claimed_at))).toISOString();
1338
1451
  const endsAt = new Date(Math.min(range.endMs, terminalRunMs(row, now))).toISOString();
1339
- const contribution = {
1452
+ const contribution = scaleContributionAp({
1340
1453
  entityType: "task",
1341
1454
  entityId: row.task_id,
1342
1455
  eventKind: "task_run",
@@ -1348,8 +1461,11 @@ function buildTaskRunContributions(userId, range, now, lifeForceProfile) {
1348
1461
  startsAt,
1349
1462
  endsAt,
1350
1463
  role: row.is_current === 1 ? "primary" : "secondary",
1351
- metadata: { taskRunId: row.id }
1352
- };
1464
+ metadata: { taskRunId: row.id, actor: row.actor }
1465
+ }, agentSupervisionMultiplier({
1466
+ actor: row.actor,
1467
+ goalLinked: Boolean(row.task_goal_id)
1468
+ }), "Agent-authored task runs are charged as supervision AP for the human user.");
1353
1469
  contributions.push(contribution);
1354
1470
  const existing = totalsByTaskId.get(row.task_id) ?? { todayAp: 0, totalAp: 0 };
1355
1471
  existing.todayAp += totalAp;
@@ -1372,6 +1488,8 @@ function buildNoteContributions(userId, range, now, lifeForceProfile) {
1372
1488
  .prepare(`SELECT
1373
1489
  notes.id,
1374
1490
  notes.title,
1491
+ notes.author,
1492
+ notes.source,
1375
1493
  notes.created_at,
1376
1494
  GROUP_CONCAT(
1377
1495
  CASE
@@ -1399,19 +1517,34 @@ function buildNoteContributions(userId, range, now, lifeForceProfile) {
1399
1517
  .filter(Boolean);
1400
1518
  return !linkedTaskIds.some((taskId) => (taskRunWindowsByTaskId.get(taskId) ?? []).some((window) => createdAtMs >= window.startMs && createdAtMs <= window.endMs));
1401
1519
  })
1402
- .map((row) => ({
1403
- entityType: "note",
1404
- entityId: row.id,
1405
- eventKind: "note_created",
1406
- sourceKind: "note",
1407
- totalAp: noteProfile.totalCostAp,
1408
- rateApPerHour: null,
1409
- title: row.title || "Note",
1410
- why: "Standalone capture takes a small impulse of activation and focus.",
1411
- startsAt: row.created_at,
1412
- endsAt: row.created_at,
1413
- role: "background"
1414
- }));
1520
+ .flatMap((row) => {
1521
+ const multiplier = noteApMultiplier({
1522
+ author: row.author,
1523
+ source: row.source
1524
+ });
1525
+ if (multiplier <= 0) {
1526
+ return [];
1527
+ }
1528
+ return [
1529
+ scaleContributionAp({
1530
+ entityType: "note",
1531
+ entityId: row.id,
1532
+ eventKind: "note_created",
1533
+ sourceKind: "note",
1534
+ totalAp: noteProfile.totalCostAp,
1535
+ rateApPerHour: null,
1536
+ title: row.title || "Note",
1537
+ why: "Standalone capture takes a small impulse of activation and focus.",
1538
+ startsAt: row.created_at,
1539
+ endsAt: row.created_at,
1540
+ role: "background",
1541
+ metadata: {
1542
+ author: row.author,
1543
+ source: row.source
1544
+ }
1545
+ }, multiplier, "Agent-authored notes are charged as a small monitoring impulse instead of full personal work.")
1546
+ ];
1547
+ });
1415
1548
  }
1416
1549
  catch {
1417
1550
  return [];
@@ -1652,8 +1785,10 @@ function readCalendarEventLifeForceRows(range) {
1652
1785
  forge_events.title,
1653
1786
  forge_events.start_at,
1654
1787
  forge_events.end_at,
1788
+ forge_events.is_all_day,
1655
1789
  forge_events.availability,
1656
1790
  forge_events.event_type,
1791
+ forge_events.categories_json,
1657
1792
  COUNT(forge_event_links.id) AS link_count
1658
1793
  FROM forge_events
1659
1794
  LEFT JOIN forge_event_links
@@ -1666,8 +1801,10 @@ function readCalendarEventLifeForceRows(range) {
1666
1801
  forge_events.title,
1667
1802
  forge_events.start_at,
1668
1803
  forge_events.end_at,
1804
+ forge_events.is_all_day,
1669
1805
  forge_events.availability,
1670
- forge_events.event_type`)
1806
+ forge_events.event_type,
1807
+ forge_events.categories_json`)
1671
1808
  .all(range.from, range.to);
1672
1809
  }
1673
1810
  catch {
@@ -1973,11 +2110,15 @@ function buildCalendarDrains(rows, now, range, lifeForceProfile, blockingWindows
1973
2110
  const plannedDrains = [];
1974
2111
  try {
1975
2112
  for (const row of rows) {
1976
- const calendarProfile = buildEffectiveProfile(readEntityActionProfile("calendar_event", row.id, {
2113
+ const storedProfile = readEntityActionProfile("calendar_event", row.id, {
1977
2114
  profileKey: `calendar_event_${row.id}`,
1978
2115
  title: row.title,
1979
2116
  entityType: "calendar_event"
1980
- }) ??
2117
+ });
2118
+ if (isPassiveCalendarContainer(row, storedProfile)) {
2119
+ continue;
2120
+ }
2121
+ const calendarProfile = buildEffectiveProfile(storedProfile ??
1981
2122
  buildCalendarEventActionProfile({
1982
2123
  eventId: row.id,
1983
2124
  title: row.title,
@@ -1,5 +1,5 @@
1
- import { request as httpRequest } from "node:http";
2
- import { request as httpsRequest } from "node:https";
1
+ import { Agent as HttpAgent, request as httpRequest } from "node:http";
2
+ import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
3
3
  import { spawn } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
5
  import { access, readFile } from "node:fs/promises";
@@ -80,7 +80,8 @@ function buildManagedDevWebLaunch(input) {
80
80
  };
81
81
  }
82
82
  const host = input.env.FORGE_DEV_WEB_HOST?.trim() || "127.0.0.1";
83
- const port = input.env.FORGE_DEV_WEB_PORT?.trim() || getDefaultDevWebOriginPort(input.origin);
83
+ const port = input.env.FORGE_DEV_WEB_PORT?.trim() ||
84
+ getDefaultDevWebOriginPort(input.origin);
84
85
  return {
85
86
  command: process.execPath,
86
87
  args: [viteCliPath, "--host", host, "--port", port],
@@ -136,15 +137,23 @@ function parseRequestTarget(requestPath) {
136
137
  function copyProxyHeaders(response, reply) {
137
138
  for (const [name, value] of response.headers) {
138
139
  const lowerName = name.toLowerCase();
139
- if (lowerName === "connection" ||
140
- lowerName === "content-length" ||
141
- lowerName === "keep-alive" ||
142
- lowerName === "transfer-encoding") {
140
+ if (hopByHopHeaders.has(lowerName)) {
143
141
  continue;
144
142
  }
145
143
  reply.header(name, value);
146
144
  }
147
145
  }
146
+ const hopByHopHeaders = new Set([
147
+ "connection",
148
+ "content-length",
149
+ "keep-alive",
150
+ "proxy-authenticate",
151
+ "proxy-authorization",
152
+ "te",
153
+ "trailer",
154
+ "transfer-encoding",
155
+ "upgrade"
156
+ ]);
148
157
  function buildDevWebTarget(origin, pathname, search) {
149
158
  const target = new URL(pathname.startsWith("/") ? pathname.slice(1) : pathname, origin);
150
159
  target.search = search;
@@ -163,6 +172,72 @@ async function proxyDevAsset(input) {
163
172
  }
164
173
  return Buffer.from(await response.arrayBuffer());
165
174
  }
175
+ export function createKeepAliveDevAssetProxy() {
176
+ const httpAgent = new HttpAgent({
177
+ keepAlive: true,
178
+ maxFreeSockets: 8,
179
+ maxSockets: 32
180
+ });
181
+ const httpsAgent = new HttpsAgent({
182
+ keepAlive: true,
183
+ maxFreeSockets: 8,
184
+ maxSockets: 32
185
+ });
186
+ return {
187
+ fetch(input) {
188
+ const target = buildDevWebTarget(input.origin, input.pathname, input.search);
189
+ const isHttps = target.protocol === "https:";
190
+ const request = isHttps ? httpsRequest : httpRequest;
191
+ const agent = isHttps ? httpsAgent : httpAgent;
192
+ return new Promise((resolve, reject) => {
193
+ const proxyRequest = request(target, {
194
+ agent,
195
+ headers: {
196
+ Accept: "*/*",
197
+ Host: target.host
198
+ },
199
+ method: "GET"
200
+ }, (response) => {
201
+ input.reply.code(response.statusCode ?? 502);
202
+ for (const [name, value] of Object.entries(response.headers)) {
203
+ if (!value || hopByHopHeaders.has(name.toLowerCase())) {
204
+ continue;
205
+ }
206
+ input.reply.header(name, value);
207
+ }
208
+ if (!response.headers["cache-control"]) {
209
+ input.reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
210
+ }
211
+ const chunks = [];
212
+ response.on("data", (chunk) => {
213
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
214
+ });
215
+ response.on("end", () => {
216
+ resolve(Buffer.concat(chunks));
217
+ });
218
+ response.on("error", reject);
219
+ });
220
+ proxyRequest.on("error", reject);
221
+ proxyRequest.end();
222
+ });
223
+ },
224
+ close() {
225
+ httpAgent.destroy();
226
+ httpsAgent.destroy();
227
+ }
228
+ };
229
+ }
230
+ function createDevAssetProxy(fetchImpl) {
231
+ if (fetchImpl !== fetch) {
232
+ return {
233
+ fetch(input) {
234
+ return proxyDevAsset({ ...input, fetchImpl });
235
+ },
236
+ close() { }
237
+ };
238
+ }
239
+ return createKeepAliveDevAssetProxy();
240
+ }
166
241
  function writeProxyUpgradeResponse(socket, response) {
167
242
  const statusCode = response.statusCode ?? 101;
168
243
  const statusMessage = response.statusMessage ?? "Switching Protocols";
@@ -344,12 +419,11 @@ async function serveAsset(requestPath, reply, options) {
344
419
  : await options.devWebRuntime.ensureReady();
345
420
  if (devWebOrigin) {
346
421
  try {
347
- return await proxyDevAsset({
422
+ return await options.devAssetProxy.fetch({
348
423
  origin: devWebOrigin,
349
424
  pathname: normalizedRequestPath,
350
425
  search: requestTarget.search,
351
- reply,
352
- fetchImpl: options.fetchImpl
426
+ reply
353
427
  });
354
428
  }
355
429
  catch {
@@ -393,8 +467,10 @@ async function serveAsset(requestPath, reply, options) {
393
467
  export async function registerWebRoutes(app, options = {}) {
394
468
  const devWebRuntime = options.devWebRuntime ?? createManagedDevWebRuntime();
395
469
  const fetchImpl = options.fetchImpl ?? fetch;
470
+ const devAssetProxy = options.devAssetProxy ?? createDevAssetProxy(fetchImpl);
396
471
  app.addHook("onClose", async () => {
397
472
  await devWebRuntime.stop();
473
+ devAssetProxy.close();
398
474
  });
399
475
  app.server.on("upgrade", (request, socket, head) => {
400
476
  void (async () => {
@@ -406,6 +482,6 @@ export async function registerWebRoutes(app, options = {}) {
406
482
  });
407
483
  })();
408
484
  });
409
- app.get("/", async (_request, reply) => serveAsset("/", reply, { devWebRuntime, fetchImpl }));
410
- app.get("/*", async (request, reply) => serveAsset(request.url, reply, { devWebRuntime, fetchImpl }));
485
+ app.get("/", async (_request, reply) => serveAsset("/", reply, { devWebRuntime, devAssetProxy }));
486
+ app.get("/*", async (request, reply) => serveAsset(request.url, reply, { devWebRuntime, devAssetProxy }));
411
487
  }
@@ -1515,6 +1515,15 @@ export function getOperatorOverview() {
1515
1515
  export function getSettings() {
1516
1516
  return request("/api/v1/settings");
1517
1517
  }
1518
+ export function getForgeDoctor() {
1519
+ return request("/api/v1/doctor");
1520
+ }
1521
+ export function applyForgeDoctorFixes(input) {
1522
+ return request("/api/v1/doctor/fixes", {
1523
+ method: "POST",
1524
+ body: JSON.stringify(input)
1525
+ });
1526
+ }
1518
1527
  export function saveAiModelConnection(input) {
1519
1528
  return request("/api/v1/settings/models/connections", {
1520
1529
  method: "POST",
@@ -222,7 +222,7 @@ const PSYCHE_TROPHIES = [
222
222
  trophy("psyche", "Value Blade", allOf(metric("psycheValueCount", 10), metric("goalLinkedTaskCompletionCount", 100)), "Create 10 values and complete 100 goal-linked tasks.", "Values and work started cutting in the same direction."),
223
223
  trophy("psyche", "Shadow Temper", allOf(metric("modeProfileCount", 12), metric("triggerReportRichCount", 50)), "Create 12 modes and 50 rich trigger reports.", "Shadow material became usable steel."),
224
224
  trophy("psyche", "Inner Forge", allOf(metric("psycheValueCount", 12), metric("behaviorPatternCount", 25), metric("beliefFlexibleAlternativeCount", 30)), "Create 12 values, 25 patterns, and 30 flexible beliefs.", "A full inner forge takes shape."),
225
- trophy("psyche", "Schema Bell", metric("questionnaireRunCount", 10), "Complete 10 questionnaire runs.", "Structured self-observation rang the bell repeatedly."),
225
+ trophy("psyche", "Schema Bell", metric("questionnaireRunCount", 40), "Complete 40 questionnaire runs.", "Structured self-observation rang the bell repeatedly."),
226
226
  trophy("psyche", "Mode Guide", metric("modeGuideSessionCount", 5), "Complete 5 mode guide sessions.", "Guided mode work became an actual practice."),
227
227
  trophy("psyche", "Repair Script", metric("behaviorCount", 10), "Create 10 Psyche behaviors.", "Behaviors now carry repair plans, not just names."),
228
228
  trophy("psyche", "Flexible Self", metric("beliefFlexibleAlternativeCount", 50), "Create 50 beliefs with flexible alternatives.", "A trophy for not letting old beliefs remain iron cages."),