forge-openclaw-plugin 0.2.19 → 0.2.21

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 (82) hide show
  1. package/README.md +133 -2
  2. package/dist/assets/board-_C6oMy5w.js +6 -0
  3. package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
  4. package/dist/assets/index-B4A6TooJ.js +63 -0
  5. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  6. package/dist/assets/index-D6Xs_2mo.css +1 -0
  7. package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
  8. package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
  9. package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
  10. package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
  11. package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
  12. package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
  13. package/dist/assets/vendor-DT3pnAKJ.css +1 -0
  14. package/dist/assets/vendor-De38P6YR.js +729 -0
  15. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  16. package/dist/assets/viz-C6hfyqzu.js +34 -0
  17. package/dist/assets/viz-C6hfyqzu.js.map +1 -0
  18. package/dist/index.html +9 -9
  19. package/dist/openclaw/parity.d.ts +1 -1
  20. package/dist/openclaw/parity.js +29 -2
  21. package/dist/openclaw/routes.js +207 -24
  22. package/dist/openclaw/tools.js +324 -35
  23. package/dist/server/app.js +2080 -92
  24. package/dist/server/db.js +3 -0
  25. package/dist/server/health.js +1284 -0
  26. package/dist/server/managers/platform/background-job-manager.js +138 -2
  27. package/dist/server/managers/platform/llm-manager.js +126 -0
  28. package/dist/server/managers/platform/openai-responses-provider.js +773 -0
  29. package/dist/server/managers/runtime.js +6 -1
  30. package/dist/server/openapi.js +718 -0
  31. package/dist/server/preferences-seeds.js +409 -0
  32. package/dist/server/preferences-types.js +368 -0
  33. package/dist/server/psyche-types.js +42 -18
  34. package/dist/server/repositories/activity-events.js +53 -4
  35. package/dist/server/repositories/calendar.js +89 -15
  36. package/dist/server/repositories/collaboration.js +8 -3
  37. package/dist/server/repositories/diagnostic-logs.js +243 -0
  38. package/dist/server/repositories/entity-ownership.js +92 -0
  39. package/dist/server/repositories/goals.js +7 -2
  40. package/dist/server/repositories/habits.js +122 -16
  41. package/dist/server/repositories/notes.js +119 -41
  42. package/dist/server/repositories/preferences.js +1765 -0
  43. package/dist/server/repositories/projects.js +18 -7
  44. package/dist/server/repositories/psyche.js +84 -27
  45. package/dist/server/repositories/rewards.js +112 -4
  46. package/dist/server/repositories/strategies.js +450 -0
  47. package/dist/server/repositories/tags.js +11 -6
  48. package/dist/server/repositories/task-runs.js +10 -2
  49. package/dist/server/repositories/tasks.js +99 -17
  50. package/dist/server/repositories/users.js +417 -0
  51. package/dist/server/repositories/wiki-memory.js +3366 -0
  52. package/dist/server/services/context.js +20 -18
  53. package/dist/server/services/dashboard.js +29 -6
  54. package/dist/server/services/entity-crud.js +21 -3
  55. package/dist/server/services/insights.js +9 -7
  56. package/dist/server/services/projects.js +2 -1
  57. package/dist/server/services/psyche.js +10 -9
  58. package/dist/server/types.js +594 -30
  59. package/openclaw.plugin.json +1 -1
  60. package/package.json +1 -1
  61. package/server/migrations/015_multi_user_and_strategies.sql +244 -0
  62. package/server/migrations/016_health_companion.sql +158 -0
  63. package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  64. package/server/migrations/017_preferences.sql +131 -0
  65. package/server/migrations/018_preference_catalogs.sql +31 -0
  66. package/server/migrations/019_wiki_memory.sql +255 -0
  67. package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  68. package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  69. package/server/migrations/022_wiki_ingest_background.sql +85 -0
  70. package/server/migrations/023_diagnostic_logs.sql +28 -0
  71. package/skills/forge-openclaw/SKILL.md +126 -34
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
  74. package/dist/assets/board-8L3uX7_O.js +0 -6
  75. package/dist/assets/index-Cj1IBH_w.js +0 -36
  76. package/dist/assets/index-Cj1IBH_w.js.map +0 -1
  77. package/dist/assets/index-DQT6EbuS.css +0 -1
  78. package/dist/assets/vendor-BvM2F9Dp.js +0 -503
  79. package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
  80. package/dist/assets/vendor-CRS-psbw.css +0 -1
  81. package/dist/assets/viz-CNeunkfu.js +0 -34
  82. package/dist/assets/viz-CNeunkfu.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDatabase, runInTransaction } from "../db.js";
3
3
  import { recordActivityEvent } from "./activity-events.js";
4
+ import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
4
5
  import { getProjectById } from "./projects.js";
5
6
  import { getTaskById } from "./tasks.js";
6
7
  import { calendarConnectionSchema, calendarContextConflictSchema, calendarEventSchema, calendarEventLinkSchema, calendarEventSourceSchema, calendarOverviewPayloadSchema, calendarSchema, calendarSchedulingRulesSchema, taskTimeboxSchema, workBlockInstanceSchema, workBlockTemplateSchema } from "../types.js";
@@ -130,6 +131,15 @@ function mapEvent(row) {
130
131
  title: row.title,
131
132
  description: row.description,
132
133
  location: row.location,
134
+ place: {
135
+ label: row.place_label,
136
+ address: row.place_address,
137
+ timezone: row.place_timezone,
138
+ latitude: row.place_latitude,
139
+ longitude: row.place_longitude,
140
+ source: row.place_source,
141
+ externalPlaceId: row.place_external_id
142
+ },
133
143
  startAt: row.start_at,
134
144
  endAt: row.end_at,
135
145
  timezone: normalizeTimezone(row.timezone),
@@ -181,6 +191,20 @@ function mapTimebox(row) {
181
191
  updatedAt: row.updated_at
182
192
  });
183
193
  }
194
+ function inferCalendarEventOwnerId(input) {
195
+ return (input.userId ??
196
+ inferFirstOwnedUserId((input.links ?? []).map((link) => ({
197
+ entityType: link.entityType,
198
+ entityId: link.entityId
199
+ }))));
200
+ }
201
+ function inferTaskTimeboxOwnerId(input) {
202
+ return (input.userId ??
203
+ inferFirstOwnedUserId([
204
+ { entityType: "task", entityId: input.taskId },
205
+ { entityType: "project", entityId: input.projectId ?? null }
206
+ ]));
207
+ }
184
208
  function addMinutes(date, minutes) {
185
209
  return new Date(date.getTime() + minutes * 60 * 1000);
186
210
  }
@@ -385,25 +409,28 @@ export function listCalendarEvents(query) {
385
409
  params.push(query.to);
386
410
  const rows = getDatabase()
387
411
  .prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
412
+ place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
388
413
  start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
389
414
  FROM forge_events
390
415
  WHERE ${clauses.join(" AND ")}
391
416
  ORDER BY start_at ASC, title ASC`)
392
417
  .all(...params);
393
- return rows.map(mapEvent);
418
+ return filterOwnedEntities("calendar_event", rows.map(mapEvent), query.userIds);
394
419
  }
395
420
  export function getCalendarEventById(eventId) {
396
421
  const row = getDatabase()
397
422
  .prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
423
+ place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
398
424
  start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
399
425
  FROM forge_events
400
426
  WHERE id = ?`)
401
427
  .get(eventId);
402
- return row ? mapEvent(row) : undefined;
428
+ return row ? decorateOwnedEntity("calendar_event", mapEvent(row)) : undefined;
403
429
  }
404
430
  export function getCalendarEventStorageRecord(eventId) {
405
431
  return getDatabase()
406
432
  .prepare(`SELECT id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
433
+ place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
407
434
  start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
408
435
  FROM forge_events
409
436
  WHERE id = ?`)
@@ -413,6 +440,8 @@ export function getCalendarEventByRemoteId(connectionId, calendarId, remoteId) {
413
440
  const row = getDatabase()
414
441
  .prepare(`SELECT forge_events.id, forge_events.preferred_connection_id, forge_events.preferred_calendar_id, forge_events.ownership,
415
442
  forge_events.origin_type, forge_events.status, forge_events.title, forge_events.description, forge_events.location,
443
+ forge_events.place_label, forge_events.place_address, forge_events.place_timezone, forge_events.place_latitude,
444
+ forge_events.place_longitude, forge_events.place_source, forge_events.place_external_id,
416
445
  forge_events.start_at, forge_events.end_at, forge_events.timezone, forge_events.is_all_day, forge_events.availability,
417
446
  forge_events.event_type, forge_events.categories_json, forge_events.deleted_at, forge_events.created_at, forge_events.updated_at
418
447
  FROM forge_event_sources
@@ -489,9 +518,10 @@ export function upsertCalendarEventRecord(connectionId, input) {
489
518
  getDatabase()
490
519
  .prepare(`UPDATE forge_events
491
520
  SET preferred_connection_id = ?, preferred_calendar_id = ?, ownership = ?, origin_type = ?, status = ?, title = ?, description = ?, location = ?,
521
+ place_label = ?, place_address = ?, place_timezone = ?, place_latitude = ?, place_longitude = ?, place_source = ?, place_external_id = ?,
492
522
  start_at = ?, end_at = ?, timezone = ?, is_all_day = ?, availability = ?, event_type = ?, categories_json = ?, deleted_at = ?, updated_at = ?
493
523
  WHERE id = ?`)
494
- .run(connectionId, calendar.id, input.ownership ?? existing.ownership, connection.provider, input.status ?? existing.status, input.title, input.description ?? "", input.location ?? "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? existing.availability, input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, existing.id);
524
+ .run(connectionId, calendar.id, input.ownership ?? existing.ownership, connection.provider, input.status ?? existing.status, input.title, input.description ?? "", input.location ?? "", input.location ?? "", "", "", null, null, "", "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? existing.availability, input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, existing.id);
495
525
  upsertEventSource({
496
526
  forgeEventId: existing.id,
497
527
  provider: connection.provider,
@@ -516,10 +546,11 @@ export function upsertCalendarEventRecord(connectionId, input) {
516
546
  getDatabase()
517
547
  .prepare(`INSERT INTO forge_events (
518
548
  id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
549
+ place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
519
550
  start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, deleted_at, created_at, updated_at
520
551
  )
521
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
522
- .run(id, connectionId, calendar.id, input.ownership ?? "external", connection.provider, input.status ?? "confirmed", input.title, input.description ?? "", input.location ?? "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? "busy", input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, now);
552
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
553
+ .run(id, connectionId, calendar.id, input.ownership ?? "external", connection.provider, input.status ?? "confirmed", input.title, input.description ?? "", input.location ?? "", input.location ?? "", "", "", null, null, "", "", input.startAt, input.endAt, calendar.timezone, input.isAllDay ? 1 : 0, input.availability ?? "busy", input.eventType ?? "", JSON.stringify(input.categories ?? []), input.deletedAt ?? null, now, now);
523
554
  upsertEventSource({
524
555
  forgeEventId: id,
525
556
  provider: connection.provider,
@@ -543,6 +574,15 @@ export function upsertCalendarEventRecord(connectionId, input) {
543
574
  export function createCalendarEvent(input) {
544
575
  const now = nowIso();
545
576
  const id = `calevent_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
577
+ const place = input.place ?? {
578
+ label: "",
579
+ address: "",
580
+ timezone: "",
581
+ latitude: null,
582
+ longitude: null,
583
+ source: "",
584
+ externalPlaceId: ""
585
+ };
546
586
  const preferredCalendar = input.preferredCalendarId === undefined
547
587
  ? getDefaultWritableCalendar() ?? null
548
588
  : input.preferredCalendarId
@@ -551,11 +591,13 @@ export function createCalendarEvent(input) {
551
591
  getDatabase()
552
592
  .prepare(`INSERT INTO forge_events (
553
593
  id, preferred_connection_id, preferred_calendar_id, ownership, origin_type, status, title, description, location,
594
+ place_label, place_address, place_timezone, place_latitude, place_longitude, place_source, place_external_id,
554
595
  start_at, end_at, timezone, is_all_day, availability, event_type, categories_json, created_at, updated_at
555
596
  )
556
- VALUES (?, ?, ?, 'forge', 'native', 'confirmed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
557
- .run(id, preferredCalendar?.connectionId ?? null, preferredCalendar?.id ?? null, input.title, input.description, input.location, input.startAt, input.endAt, normalizeTimezone(input.timezone), input.isAllDay ? 1 : 0, input.availability, input.eventType, JSON.stringify(input.categories), now, now);
597
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
598
+ .run(id, preferredCalendar?.connectionId ?? null, preferredCalendar?.id ?? null, "forge", "native", "confirmed", input.title, input.description, input.location, place.label || input.location, place.address, place.timezone, place.latitude, place.longitude, place.source, place.externalPlaceId, input.startAt, input.endAt, normalizeTimezone(input.timezone), input.isAllDay ? 1 : 0, input.availability, input.eventType, JSON.stringify(input.categories), now, now);
558
599
  replaceEventLinks(id, input.links);
600
+ setEntityOwner("calendar_event", id, inferCalendarEventOwnerId(input));
559
601
  return getCalendarEventById(id);
560
602
  }
561
603
  export function updateCalendarEvent(eventId, patch) {
@@ -578,6 +620,19 @@ export function updateCalendarEvent(eventId, patch) {
578
620
  title: patch.title ?? current.title,
579
621
  description: patch.description ?? current.description,
580
622
  location: patch.location ?? current.location,
623
+ place: {
624
+ label: patch.place?.label ?? current.place.label,
625
+ address: patch.place?.address ?? current.place.address,
626
+ timezone: patch.place?.timezone ?? current.place.timezone,
627
+ latitude: patch.place?.latitude === undefined
628
+ ? current.place.latitude
629
+ : patch.place.latitude,
630
+ longitude: patch.place?.longitude === undefined
631
+ ? current.place.longitude
632
+ : patch.place.longitude,
633
+ source: patch.place?.source ?? current.place.source,
634
+ externalPlaceId: patch.place?.externalPlaceId ?? current.place.externalPlaceId
635
+ },
581
636
  startAt: patch.startAt ?? current.startAt,
582
637
  endAt: patch.endAt ?? current.endAt,
583
638
  timezone: normalizeTimezone(patch.timezone ?? current.timezone),
@@ -590,12 +645,21 @@ export function updateCalendarEvent(eventId, patch) {
590
645
  getDatabase()
591
646
  .prepare(`UPDATE forge_events
592
647
  SET preferred_connection_id = ?, preferred_calendar_id = ?, title = ?, description = ?, location = ?,
648
+ place_label = ?, place_address = ?, place_timezone = ?, place_latitude = ?, place_longitude = ?, place_source = ?, place_external_id = ?,
593
649
  start_at = ?, end_at = ?, timezone = ?, is_all_day = ?, availability = ?, event_type = ?, categories_json = ?, updated_at = ?
594
650
  WHERE id = ?`)
595
- .run(next.preferredConnectionId, next.preferredCalendarId, next.title, next.description, next.location, next.startAt, next.endAt, next.timezone, next.isAllDay ? 1 : 0, next.availability, next.eventType, JSON.stringify(next.categories), next.updatedAt, eventId);
651
+ .run(next.preferredConnectionId, next.preferredCalendarId, next.title, next.description, next.location, next.place.label, next.place.address, next.place.timezone, next.place.latitude, next.place.longitude, next.place.source, next.place.externalPlaceId, next.startAt, next.endAt, next.timezone, next.isAllDay ? 1 : 0, next.availability, next.eventType, JSON.stringify(next.categories), next.updatedAt, eventId);
596
652
  if (patch.links) {
597
653
  replaceEventLinks(eventId, patch.links);
598
654
  }
655
+ if (patch.userId !== undefined || patch.links !== undefined) {
656
+ setEntityOwner("calendar_event", eventId, patch.userId === undefined
657
+ ? inferCalendarEventOwnerId({
658
+ userId: current.userId ?? null,
659
+ links: patch.links ?? current.links
660
+ })
661
+ : patch.userId);
662
+ }
599
663
  if (current.sourceMappings.length > 0) {
600
664
  const nextSyncState = current.deletedAt !== null ? "deleted" : current.originType === "native" ? "pending_update" : "synced";
601
665
  getDatabase()
@@ -635,16 +699,17 @@ export function createWorkBlockTemplate(input) {
635
699
  )
636
700
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
637
701
  .run(id, input.title, input.kind, input.color, normalizeTimezone(input.timezone), JSON.stringify(input.weekDays), input.startMinute, input.endMinute, input.startsOn ?? null, input.endsOn ?? null, input.blockingState, now, now);
702
+ setEntityOwner("work_block_template", id, input.userId);
638
703
  return getWorkBlockTemplateById(id);
639
704
  });
640
705
  }
641
- export function listWorkBlockTemplates() {
706
+ export function listWorkBlockTemplates(filters = {}) {
642
707
  const rows = getDatabase()
643
708
  .prepare(`SELECT id, title, kind, color, timezone, weekdays_json, start_minute, end_minute, starts_on, ends_on, blocking_state, created_at, updated_at
644
709
  FROM work_block_templates
645
710
  ORDER BY COALESCE(starts_on, ''), start_minute ASC, title ASC`)
646
711
  .all();
647
- return rows.map(mapWorkBlockTemplate);
712
+ return filterOwnedEntities("work_block_template", rows.map(mapWorkBlockTemplate), filters.userIds);
648
713
  }
649
714
  export function getWorkBlockTemplateById(templateId) {
650
715
  const row = getDatabase()
@@ -652,7 +717,9 @@ export function getWorkBlockTemplateById(templateId) {
652
717
  FROM work_block_templates
653
718
  WHERE id = ?`)
654
719
  .get(templateId);
655
- return row ? mapWorkBlockTemplate(row) : undefined;
720
+ return row
721
+ ? decorateOwnedEntity("work_block_template", mapWorkBlockTemplate(row))
722
+ : undefined;
656
723
  }
657
724
  export function updateWorkBlockTemplate(templateId, patch) {
658
725
  const current = getWorkBlockTemplateById(templateId);
@@ -677,6 +744,9 @@ export function updateWorkBlockTemplate(templateId, patch) {
677
744
  SET title = ?, kind = ?, color = ?, timezone = ?, weekdays_json = ?, start_minute = ?, end_minute = ?, starts_on = ?, ends_on = ?, blocking_state = ?, updated_at = ?
678
745
  WHERE id = ?`)
679
746
  .run(next.title, next.kind, next.color, next.timezone, JSON.stringify(next.weekDays), next.startMinute, next.endMinute, next.startsOn, next.endsOn, next.blockingState, next.updatedAt, templateId);
747
+ if (patch.userId !== undefined) {
748
+ setEntityOwner("work_block_template", templateId, patch.userId);
749
+ }
680
750
  return getWorkBlockTemplateById(templateId);
681
751
  }
682
752
  export function deleteWorkBlockTemplate(templateId) {
@@ -730,7 +800,7 @@ export function ensureWorkBlockInstancesInRange(_query) {
730
800
  return [];
731
801
  }
732
802
  export function listWorkBlockInstances(query) {
733
- return listWorkBlockTemplates()
803
+ return listWorkBlockTemplates({ userIds: query.userIds })
734
804
  .flatMap((template) => deriveWorkBlockInstances(template, query))
735
805
  .sort((left, right) => left.startAt.localeCompare(right.startAt) || left.title.localeCompare(right.title));
736
806
  }
@@ -752,7 +822,7 @@ export function listTaskTimeboxes(query) {
752
822
  WHERE ${clauses.join(" AND ")}
753
823
  ORDER BY starts_at ASC`)
754
824
  .all(...params);
755
- return rows.map(mapTimebox);
825
+ return filterOwnedEntities("task_timebox", rows.map(mapTimebox), query.userIds);
756
826
  }
757
827
  export function getTaskTimeboxById(timeboxId) {
758
828
  const row = getDatabase()
@@ -761,7 +831,7 @@ export function getTaskTimeboxById(timeboxId) {
761
831
  FROM task_timeboxes
762
832
  WHERE id = ?`)
763
833
  .get(timeboxId);
764
- return row ? mapTimebox(row) : undefined;
834
+ return row ? decorateOwnedEntity("task_timebox", mapTimebox(row)) : undefined;
765
835
  }
766
836
  export function createTaskTimebox(input) {
767
837
  const now = nowIso();
@@ -772,6 +842,7 @@ export function createTaskTimebox(input) {
772
842
  )
773
843
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
774
844
  .run(id, input.taskId, input.projectId ?? null, input.connectionId ?? null, input.calendarId ?? null, input.linkedTaskRunId ?? null, input.status ?? "planned", input.source ?? "manual", input.title, input.startsAt, input.endsAt, input.overrideReason ?? null, now, now);
845
+ setEntityOwner("task_timebox", id, inferTaskTimeboxOwnerId(input));
775
846
  return getTaskTimeboxById(id);
776
847
  }
777
848
  export function updateTaskTimebox(timeboxId, patch) {
@@ -798,6 +869,9 @@ export function updateTaskTimebox(timeboxId, patch) {
798
869
  starts_at = ?, ends_at = ?, override_reason = ?, updated_at = ?
799
870
  WHERE id = ?`)
800
871
  .run(next.connectionId, next.calendarId, next.remoteEventId, next.linkedTaskRunId, next.status, next.source, next.title, next.startsAt, next.endsAt, next.overrideReason, next.updatedAt, timeboxId);
872
+ if (patch.userId !== undefined) {
873
+ setEntityOwner("task_timebox", timeboxId, patch.userId);
874
+ }
801
875
  return getTaskTimeboxById(timeboxId);
802
876
  }
803
877
  export function deleteTaskTimebox(timeboxId) {
@@ -1082,7 +1156,7 @@ export function getCalendarOverview(query) {
1082
1156
  connections: listCalendarConnections().map(({ credentialsSecretId: _secret, ...connection }) => connection),
1083
1157
  calendars: listCalendars(),
1084
1158
  events: listCalendarEvents(query),
1085
- workBlockTemplates: listWorkBlockTemplates(),
1159
+ workBlockTemplates: listWorkBlockTemplates({ userIds: query.userIds }),
1086
1160
  workBlockInstances: listWorkBlockInstances(query),
1087
1161
  timeboxes: listTaskTimeboxes(query)
1088
1162
  });
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDatabase } from "../db.js";
3
+ import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
3
4
  import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
4
5
  import { recordActivityEvent } from "./activity-events.js";
5
6
  import { recordEventLog } from "./event-log.js";
@@ -8,7 +9,7 @@ import { createTask } from "./tasks.js";
8
9
  import { recordInsightAppliedReward } from "./rewards.js";
9
10
  import { agentActionSchema, approvalRequestSchema, createAgentActionSchema, createInsightFeedbackSchema, createInsightSchema, insightFeedbackSchema, insightSchema, updateInsightSchema } from "../types.js";
10
11
  function mapInsight(row) {
11
- return insightSchema.parse({
12
+ return insightSchema.parse(decorateOwnedEntity("insight", {
12
13
  id: row.id,
13
14
  originType: row.origin_type,
14
15
  originAgentId: row.origin_agent_id,
@@ -27,7 +28,7 @@ function mapInsight(row) {
27
28
  evidence: JSON.parse(row.evidence_json),
28
29
  createdAt: row.created_at,
29
30
  updatedAt: row.updated_at
30
- });
31
+ }));
31
32
  }
32
33
  function mapFeedback(row) {
33
34
  return insightFeedbackSchema.parse({
@@ -132,7 +133,7 @@ export function listInsights(filters = {}) {
132
133
  ORDER BY created_at DESC
133
134
  ${limitSql}`)
134
135
  .all(...params);
135
- return filterDeletedEntities("insight", rows.map(mapInsight));
136
+ return filterDeletedEntities("insight", filterOwnedEntities("insight", rows.map(mapInsight), filters.userIds));
136
137
  }
137
138
  export function getInsightById(insightId) {
138
139
  if (isEntityDeleted("insight", insightId)) {
@@ -151,6 +152,9 @@ export function createInsight(input, context) {
151
152
  title, summary, recommendation, rationale, confidence, cta_label, evidence_json, created_at, updated_at
152
153
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
153
154
  .run(insightId, parsed.originType, parsed.originAgentId, parsed.originLabel, parsed.visibility, parsed.status, parsed.entityType, parsed.entityId, parsed.timeframeLabel, parsed.title, parsed.summary, parsed.recommendation, parsed.rationale, parsed.confidence, parsed.ctaLabel, JSON.stringify(parsed.evidence), now, now);
155
+ setEntityOwner("insight", insightId, inferFirstOwnedUserId(parsed.entityType && parsed.entityId
156
+ ? [{ entityType: parsed.entityType, entityId: parsed.entityId }]
157
+ : []), context.actor ?? parsed.originLabel ?? null);
154
158
  recordActivityEvent({
155
159
  entityType: "insight",
156
160
  entityId: insightId,
@@ -191,6 +195,7 @@ export function updateInsight(insightId, input, context) {
191
195
  recommendation = ?, rationale = ?, confidence = ?, cta_label = ?, evidence_json = ?, updated_at = ?
192
196
  WHERE id = ?`)
193
197
  .run(parsed.visibility ?? current.visibility, parsed.status ?? current.status, parsed.entityType === undefined ? current.entityType : parsed.entityType, parsed.entityId === undefined ? current.entityId : parsed.entityId, parsed.timeframeLabel === undefined ? current.timeframeLabel : parsed.timeframeLabel, parsed.title ?? current.title, parsed.summary ?? current.summary, parsed.recommendation ?? current.recommendation, parsed.rationale ?? current.rationale, parsed.confidence ?? current.confidence, parsed.ctaLabel ?? current.ctaLabel, JSON.stringify(parsed.evidence ?? current.evidence), updatedAt, insightId);
198
+ setEntityOwner("insight", insightId, current.userId, context.actor ?? current.originLabel ?? null);
194
199
  recordEventLog({
195
200
  eventKind: "insight.updated",
196
201
  entityType: "insight",
@@ -0,0 +1,243 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+ import { getDatabase } from "../db.js";
4
+ import { createDiagnosticLogSchema, diagnosticLogEntrySchema } from "../types.js";
5
+ const MAX_LOG_ENTRIES = 5_000;
6
+ export const DIAGNOSTIC_LOG_RETENTION_DAYS = 14;
7
+ export const DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS = 15 * 60 * 1000;
8
+ const MAX_STRING_LENGTH = 4_000;
9
+ const MAX_ARRAY_ITEMS = 24;
10
+ const MAX_OBJECT_KEYS = 40;
11
+ const MAX_DEPTH = 4;
12
+ let nextRetentionSweepAt = 0;
13
+ function nowIso() {
14
+ return new Date().toISOString();
15
+ }
16
+ function sanitizeDiagnosticValue(value, depth = 0) {
17
+ if (value === null ||
18
+ typeof value === "boolean" ||
19
+ typeof value === "number") {
20
+ return value;
21
+ }
22
+ if (typeof value === "string") {
23
+ return value.length > MAX_STRING_LENGTH
24
+ ? `${value.slice(0, MAX_STRING_LENGTH)}…`
25
+ : value;
26
+ }
27
+ if (typeof value === "bigint") {
28
+ return value.toString();
29
+ }
30
+ if (typeof value === "function" || typeof value === "symbol") {
31
+ return String(value);
32
+ }
33
+ if (value instanceof Date) {
34
+ return value.toISOString();
35
+ }
36
+ if (value instanceof Error) {
37
+ return {
38
+ name: value.name,
39
+ message: value.message,
40
+ stack: typeof value.stack === "string"
41
+ ? sanitizeDiagnosticValue(value.stack, depth + 1)
42
+ : null
43
+ };
44
+ }
45
+ if (Buffer.isBuffer(value)) {
46
+ return {
47
+ type: "Buffer",
48
+ bytes: value.byteLength
49
+ };
50
+ }
51
+ if (depth >= MAX_DEPTH) {
52
+ if (Array.isArray(value)) {
53
+ return `[Array(${value.length})]`;
54
+ }
55
+ return "[Object]";
56
+ }
57
+ if (Array.isArray(value)) {
58
+ return value
59
+ .slice(0, MAX_ARRAY_ITEMS)
60
+ .map((entry) => sanitizeDiagnosticValue(entry, depth + 1));
61
+ }
62
+ if (value && typeof value === "object") {
63
+ const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
64
+ return Object.fromEntries(entries.map(([key, entry]) => [
65
+ key,
66
+ sanitizeDiagnosticValue(entry, depth + 1)
67
+ ]));
68
+ }
69
+ return String(value);
70
+ }
71
+ function sanitizeDetails(details) {
72
+ if (!details) {
73
+ return {};
74
+ }
75
+ return Object.fromEntries(Object.entries(details).map(([key, value]) => [
76
+ key,
77
+ sanitizeDiagnosticValue(value)
78
+ ]));
79
+ }
80
+ function mapRow(row) {
81
+ return diagnosticLogEntrySchema.parse({
82
+ id: row.id,
83
+ level: row.level,
84
+ source: row.source,
85
+ scope: row.scope,
86
+ eventKey: row.event_key,
87
+ message: row.message,
88
+ route: row.route,
89
+ functionName: row.function_name,
90
+ requestId: row.request_id,
91
+ entityType: row.entity_type,
92
+ entityId: row.entity_id,
93
+ jobId: row.job_id,
94
+ details: JSON.parse(row.details_json),
95
+ createdAt: row.created_at
96
+ });
97
+ }
98
+ function pruneDiagnosticLogs() {
99
+ const expiredBefore = new Date(Date.now() - DIAGNOSTIC_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1_000).toISOString();
100
+ const expiredResult = getDatabase()
101
+ .prepare(`DELETE FROM diagnostic_logs
102
+ WHERE created_at < ?`)
103
+ .run(expiredBefore);
104
+ const overflowResult = getDatabase().prepare(`DELETE FROM diagnostic_logs
105
+ WHERE id IN (
106
+ SELECT id
107
+ FROM diagnostic_logs
108
+ ORDER BY created_at DESC
109
+ LIMIT -1 OFFSET ?
110
+ )`).run(MAX_LOG_ENTRIES);
111
+ return (expiredResult.changes ?? 0) + (overflowResult.changes ?? 0);
112
+ }
113
+ function checkpointDiagnosticLogStore() {
114
+ getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE);");
115
+ }
116
+ export function enforceDiagnosticLogRetention(options = {}) {
117
+ const now = options.now ?? new Date();
118
+ const startedAt = now.getTime();
119
+ if (!options.force && startedAt < nextRetentionSweepAt) {
120
+ return { prunedCount: 0, ran: false };
121
+ }
122
+ nextRetentionSweepAt =
123
+ startedAt + DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS;
124
+ const prunedCount = pruneDiagnosticLogs();
125
+ if (prunedCount > 0) {
126
+ checkpointDiagnosticLogStore();
127
+ }
128
+ return {
129
+ prunedCount,
130
+ ran: true
131
+ };
132
+ }
133
+ export function recordDiagnosticLog(input, now = new Date()) {
134
+ const parsed = createDiagnosticLogSchema.parse(input);
135
+ const entry = diagnosticLogEntrySchema.parse({
136
+ id: `diag_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
137
+ level: parsed.level,
138
+ source: parsed.source ?? "server",
139
+ scope: parsed.scope,
140
+ eventKey: parsed.eventKey,
141
+ message: parsed.message,
142
+ route: parsed.route ?? null,
143
+ functionName: parsed.functionName ?? null,
144
+ requestId: parsed.requestId ?? null,
145
+ entityType: parsed.entityType ?? null,
146
+ entityId: parsed.entityId ?? null,
147
+ jobId: parsed.jobId ?? null,
148
+ details: sanitizeDetails(parsed.details),
149
+ createdAt: now.toISOString()
150
+ });
151
+ getDatabase().prepare(`INSERT INTO diagnostic_logs (
152
+ id, level, source, scope, event_key, message, route, function_name,
153
+ request_id, entity_type, entity_id, job_id, details_json, created_at
154
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(entry.id, entry.level, entry.source, entry.scope, entry.eventKey, entry.message, entry.route, entry.functionName, entry.requestId, entry.entityType, entry.entityId, entry.jobId, JSON.stringify(entry.details), entry.createdAt);
155
+ enforceDiagnosticLogRetention({ now });
156
+ return entry;
157
+ }
158
+ export function listDiagnosticLogs(filters = {}) {
159
+ enforceDiagnosticLogRetention();
160
+ const whereClauses = [];
161
+ const params = [];
162
+ if (filters.level) {
163
+ whereClauses.push("level = ?");
164
+ params.push(filters.level);
165
+ }
166
+ if (filters.source) {
167
+ whereClauses.push("source = ?");
168
+ params.push(filters.source);
169
+ }
170
+ if (filters.scope) {
171
+ whereClauses.push("scope = ?");
172
+ params.push(filters.scope);
173
+ }
174
+ if (filters.route) {
175
+ whereClauses.push("route = ?");
176
+ params.push(filters.route);
177
+ }
178
+ if (filters.entityType) {
179
+ whereClauses.push("entity_type = ?");
180
+ params.push(filters.entityType);
181
+ }
182
+ if (filters.entityId) {
183
+ whereClauses.push("entity_id = ?");
184
+ params.push(filters.entityId);
185
+ }
186
+ if (filters.jobId) {
187
+ whereClauses.push("job_id = ?");
188
+ params.push(filters.jobId);
189
+ }
190
+ if (filters.search) {
191
+ whereClauses.push("(message LIKE ? OR scope LIKE ? OR event_key LIKE ? OR IFNULL(route, '') LIKE ? OR details_json LIKE ?)");
192
+ const term = `%${filters.search}%`;
193
+ params.push(term, term, term, term, term);
194
+ }
195
+ if (filters.beforeCreatedAt && filters.beforeId) {
196
+ whereClauses.push("(created_at < ? OR (created_at = ? AND id < ?))");
197
+ params.push(filters.beforeCreatedAt, filters.beforeCreatedAt, filters.beforeId);
198
+ }
199
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
200
+ const limit = filters.limit ?? 200;
201
+ const rows = getDatabase()
202
+ .prepare(`SELECT id, level, source, scope, event_key, message, route, function_name,
203
+ request_id, entity_type, entity_id, job_id, details_json, created_at
204
+ FROM diagnostic_logs
205
+ ${whereSql}
206
+ ORDER BY created_at DESC, id DESC
207
+ LIMIT ?`)
208
+ .all(...params, limit);
209
+ const logs = rows.map(mapRow);
210
+ const tail = rows.at(-1) ?? null;
211
+ return {
212
+ logs,
213
+ nextCursor: rows.length >= limit && tail
214
+ ? {
215
+ beforeCreatedAt: tail.created_at,
216
+ beforeId: tail.id
217
+ }
218
+ : null
219
+ };
220
+ }
221
+ export function normalizeDiagnosticSource(value) {
222
+ return value === "ui" ||
223
+ value === "openclaw" ||
224
+ value === "agent" ||
225
+ value === "system" ||
226
+ value === "server"
227
+ ? value
228
+ : "server";
229
+ }
230
+ export function serializeDiagnosticError(error) {
231
+ return sanitizeDiagnosticValue(error);
232
+ }
233
+ export function createDiagnosticMessage(input) {
234
+ const method = input.method?.toUpperCase() || "CALL";
235
+ const route = input.route || "unknown-route";
236
+ if (typeof input.statusCode === "number") {
237
+ return `${method} ${route} -> ${input.statusCode}`;
238
+ }
239
+ return `${method} ${route}`;
240
+ }
241
+ export function createDiagnosticTimestamp() {
242
+ return nowIso();
243
+ }
@@ -0,0 +1,92 @@
1
+ import { getDatabase } from "../db.js";
2
+ import { listUsersByIds, resolveUserForMutation } from "./users.js";
3
+ export function setEntityOwner(entityType, entityId, userId, fallbackLabel) {
4
+ const user = resolveUserForMutation(userId, fallbackLabel);
5
+ const now = new Date().toISOString();
6
+ getDatabase()
7
+ .prepare(`INSERT INTO entity_owners (entity_type, entity_id, user_id, role, created_at, updated_at)
8
+ VALUES (?, ?, ?, 'owner', ?, ?)
9
+ ON CONFLICT(entity_type, entity_id)
10
+ DO UPDATE SET user_id = excluded.user_id, updated_at = excluded.updated_at`)
11
+ .run(entityType, entityId, user.id, now, now);
12
+ return { userId: user.id, user };
13
+ }
14
+ export function clearEntityOwner(entityType, entityId) {
15
+ getDatabase()
16
+ .prepare(`DELETE FROM entity_owners WHERE entity_type = ? AND entity_id = ?`)
17
+ .run(entityType, entityId);
18
+ }
19
+ export function getEntityOwnerId(entityType, entityId) {
20
+ const row = getDatabase()
21
+ .prepare(`SELECT user_id
22
+ FROM entity_owners
23
+ WHERE entity_type = ? AND entity_id = ?`)
24
+ .get(entityType, entityId);
25
+ return row?.user_id ?? null;
26
+ }
27
+ export function getEntityOwner(entityType, entityId) {
28
+ const userId = getEntityOwnerId(entityType, entityId);
29
+ return userId ? (listUsersByIds([userId])[0] ?? null) : null;
30
+ }
31
+ export function inferFirstOwnedUserId(candidates) {
32
+ for (const candidate of candidates) {
33
+ if (!candidate.entityId) {
34
+ continue;
35
+ }
36
+ const userId = getEntityOwnerId(candidate.entityType, candidate.entityId);
37
+ if (userId) {
38
+ return userId;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ function listOwnerRows(entityType, entityIds) {
44
+ if (entityIds.length === 0) {
45
+ return [];
46
+ }
47
+ const placeholders = entityIds.map(() => "?").join(", ");
48
+ return getDatabase()
49
+ .prepare(`SELECT entity_id, user_id
50
+ FROM entity_owners
51
+ WHERE entity_type = ?
52
+ AND entity_id IN (${placeholders})`)
53
+ .all(entityType, ...entityIds);
54
+ }
55
+ export function buildEntityOwnerIndex(entityType, entityIds) {
56
+ const rows = listOwnerRows(entityType, entityIds);
57
+ const userIds = Array.from(new Set(rows.map((row) => row.user_id)));
58
+ const usersById = new Map(listUsersByIds(userIds).map((user) => [user.id, user]));
59
+ const index = new Map();
60
+ for (const entityId of entityIds) {
61
+ index.set(entityId, { userId: null, user: null });
62
+ }
63
+ for (const row of rows) {
64
+ index.set(row.entity_id, {
65
+ userId: row.user_id,
66
+ user: usersById.get(row.user_id) ?? null
67
+ });
68
+ }
69
+ return index;
70
+ }
71
+ export function decorateOwnedEntities(entityType, entities) {
72
+ const ownerIndex = buildEntityOwnerIndex(entityType, entities.map((entity) => entity.id));
73
+ return entities.map((entity) => {
74
+ const owner = ownerIndex.get(entity.id) ?? { userId: null, user: null };
75
+ return {
76
+ ...entity,
77
+ userId: owner.userId,
78
+ user: owner.user
79
+ };
80
+ });
81
+ }
82
+ export function decorateOwnedEntity(entityType, entity) {
83
+ return decorateOwnedEntities(entityType, [entity])[0];
84
+ }
85
+ export function filterOwnedEntities(entityType, entities, userIds) {
86
+ const decorated = decorateOwnedEntities(entityType, entities);
87
+ if (!userIds || userIds.length === 0) {
88
+ return decorated;
89
+ }
90
+ const allowed = new Set(userIds);
91
+ return decorated.filter((entity) => entity.userId !== null && allowed.has(entity.userId));
92
+ }