@swarmclawai/swarmclaw 1.2.1 → 1.2.3

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 (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -0,0 +1,35 @@
1
+ import type { WatchJob } from '@/types'
2
+
3
+ import {
4
+ deleteWatchJob as deleteStoredWatchJob,
5
+ loadWatchJobs as loadStoredWatchJobs,
6
+ upsertWatchJob as upsertStoredWatchJob,
7
+ upsertWatchJobs as upsertStoredWatchJobs,
8
+ } from '@/lib/server/storage'
9
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
+
11
+ export const watchJobRepository = createRecordRepository<WatchJob>(
12
+ 'watch-jobs',
13
+ {
14
+ get(id) {
15
+ return (loadStoredWatchJobs() as Record<string, WatchJob>)[id] || null
16
+ },
17
+ list() {
18
+ return loadStoredWatchJobs() as Record<string, WatchJob>
19
+ },
20
+ upsert(id, value) {
21
+ upsertStoredWatchJob(id, value as WatchJob)
22
+ },
23
+ upsertMany(entries) {
24
+ upsertStoredWatchJobs(entries as Array<[string, WatchJob]>)
25
+ },
26
+ delete(id) {
27
+ deleteStoredWatchJob(id)
28
+ },
29
+ },
30
+ )
31
+
32
+ export const loadWatchJobs = () => watchJobRepository.list()
33
+ export const upsertWatchJob = (id: string, value: WatchJob | Record<string, unknown>) => watchJobRepository.upsert(id, value as WatchJob)
34
+ export const upsertWatchJobs = (entries: Array<[string, WatchJob | Record<string, unknown>]>) => watchJobRepository.upsertMany(entries as Array<[string, WatchJob]>)
35
+ export const deleteWatchJob = (id: string) => watchJobRepository.delete(id)
@@ -5,7 +5,9 @@ import { genId } from '@/lib/id'
5
5
  import type { MailboxEnvelope, WatchJob } from '@/types'
6
6
  import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
7
7
  import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
8
- import { loadApprovals, loadTasks, loadWatchJobs, upsertWatchJob, upsertWatchJobs } from '@/lib/server/storage'
8
+ import { loadApprovals } from '@/lib/server/approvals/approval-repository'
9
+ import { loadTasks } from '@/lib/server/tasks/task-repository'
10
+ import { loadWatchJobs, upsertWatchJob, upsertWatchJobs } from '@/lib/server/runtime/watch-job-repository'
9
11
  import { notify } from '@/lib/server/ws-hub'
10
12
  import { fetchMailboxMessages, getMailboxHighwaterUid } from '@/lib/server/chatrooms/mailbox-utils'
11
13
  import { errorMessage } from '@/lib/shared-utils'
@@ -0,0 +1,42 @@
1
+ import type { Schedule } from '@/types'
2
+
3
+ import {
4
+ deleteSchedule as deleteStoredSchedule,
5
+ loadSchedule as loadStoredSchedule,
6
+ loadSchedules as loadStoredSchedules,
7
+ saveSchedules as saveStoredSchedules,
8
+ upsertSchedule as upsertStoredSchedule,
9
+ upsertSchedules as upsertStoredSchedules,
10
+ } from '@/lib/server/storage'
11
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
12
+
13
+ export const scheduleRepository = createRecordRepository<Schedule>(
14
+ 'schedules',
15
+ {
16
+ get(id) {
17
+ return loadStoredSchedule(id) as Schedule | null
18
+ },
19
+ list() {
20
+ return loadStoredSchedules() as Record<string, Schedule>
21
+ },
22
+ upsert(id, value) {
23
+ upsertStoredSchedule(id, value as Schedule)
24
+ },
25
+ upsertMany(entries) {
26
+ upsertStoredSchedules(entries as Array<[string, Schedule]>)
27
+ },
28
+ replace(data) {
29
+ saveStoredSchedules(data as Record<string, Schedule>)
30
+ },
31
+ delete(id) {
32
+ deleteStoredSchedule(id)
33
+ },
34
+ },
35
+ )
36
+
37
+ export const loadSchedules = () => scheduleRepository.list()
38
+ export const loadSchedule = (id: string) => scheduleRepository.get(id)
39
+ export const saveSchedules = (items: Record<string, Schedule | Record<string, unknown>>) => scheduleRepository.replace(items as Record<string, Schedule>)
40
+ export const upsertSchedule = (id: string, value: Schedule | Record<string, unknown>) => scheduleRepository.upsert(id, value as Schedule)
41
+ export const upsertSchedules = (entries: Array<[string, Schedule | Record<string, unknown>]>) => scheduleRepository.upsertMany(entries as Array<[string, Schedule]>)
42
+ export const deleteSchedule = (id: string) => scheduleRepository.delete(id)
@@ -0,0 +1,85 @@
1
+ import type { Message, Session } from '@/types'
2
+
3
+ import {
4
+ deleteSession as deleteStoredSession,
5
+ disableAllSessionHeartbeats as disableAllStoredSessionHeartbeats,
6
+ getSessionMessages as getStoredSessionMessages,
7
+ loadSession as loadStoredSession,
8
+ loadSessions as loadStoredSessions,
9
+ patchSession as patchStoredSession,
10
+ saveSessions as replaceStoredSessions,
11
+ upsertSession as upsertStoredSession,
12
+ } from '@/lib/server/storage'
13
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
14
+
15
+ export const sessionRepository = createRecordRepository<Session>(
16
+ 'sessions',
17
+ {
18
+ get(id) {
19
+ return loadStoredSession(id) as Session | null
20
+ },
21
+ list() {
22
+ return loadStoredSessions() as Record<string, Session>
23
+ },
24
+ upsert(id, value) {
25
+ upsertStoredSession(id, value as Session)
26
+ },
27
+ upsertMany(entries) {
28
+ replaceStoredSessions(Object.fromEntries(entries))
29
+ },
30
+ replace(data) {
31
+ replaceStoredSessions(data)
32
+ },
33
+ patch(id, updater) {
34
+ return patchStoredSession(id, updater as (current: Session | null) => Session | null) as Session | null
35
+ },
36
+ delete(id) {
37
+ deleteStoredSession(id)
38
+ },
39
+ },
40
+ )
41
+
42
+ export function listSessions(): Record<string, Session> {
43
+ return sessionRepository.list()
44
+ }
45
+
46
+ export function getSession(id: string): Session | null {
47
+ return sessionRepository.get(id)
48
+ }
49
+
50
+ export function getSessions(ids: string[]): Record<string, Session> {
51
+ return sessionRepository.getMany(ids)
52
+ }
53
+
54
+ export function saveSession(id: string, session: Session | Record<string, unknown>): void {
55
+ sessionRepository.upsert(id, session as Session)
56
+ }
57
+
58
+ export function saveSessionMany(entries: Array<[string, Session | Record<string, unknown>]>): void {
59
+ sessionRepository.upsertMany(entries as Array<[string, Session]>)
60
+ }
61
+
62
+ export function replaceSessions(sessions: Record<string, Session | Record<string, unknown>>): void {
63
+ sessionRepository.replace(sessions as Record<string, Session>)
64
+ }
65
+
66
+ export function patchSession(id: string, updater: (current: Session | null) => Session | null): Session | null {
67
+ return sessionRepository.patch(id, updater)
68
+ }
69
+
70
+ export function deleteSession(id: string): void {
71
+ sessionRepository.delete(id)
72
+ }
73
+
74
+ export function getSessionMessages(sessionId: string): Message[] {
75
+ return getStoredSessionMessages(sessionId)
76
+ }
77
+
78
+ export function disableAllSessionHeartbeats(): number {
79
+ return disableAllStoredSessionHeartbeats()
80
+ }
81
+
82
+ export const loadSessions = listSessions
83
+ export const loadSession = getSession
84
+ export const saveSessions = replaceSessions
85
+ export const upsertSession = saveSession
@@ -0,0 +1,25 @@
1
+ import type { AppSettings } from '@/types'
2
+
3
+ import {
4
+ loadPublicSettings as loadStoredPublicSettings,
5
+ loadSettings as loadStoredSettings,
6
+ saveSettings as saveStoredSettings,
7
+ } from '@/lib/server/storage'
8
+ import { createSingletonRepository } from '@/lib/server/persistence/repository-utils'
9
+
10
+ export const settingsRepository = createSingletonRepository<AppSettings>(
11
+ 'settings',
12
+ {
13
+ get() {
14
+ return loadStoredSettings()
15
+ },
16
+ save(value) {
17
+ saveStoredSettings(value)
18
+ },
19
+ },
20
+ )
21
+
22
+ export const loadSettings = () => settingsRepository.get()
23
+ export const saveSettings = (value: AppSettings | Record<string, unknown>) => settingsRepository.save(value as AppSettings)
24
+ export const patchSettings = (updater: (current: AppSettings) => AppSettings | Record<string, unknown>) => settingsRepository.patch(updater as (current: AppSettings) => AppSettings)
25
+ export const loadPublicSettings = () => loadStoredPublicSettings()
@@ -5,14 +5,14 @@ import path from 'node:path'
5
5
  import test from 'node:test'
6
6
  import { clearDiscoveredSkillsCache, discoverSkills } from './skill-discovery'
7
7
 
8
- test('discoverSkills includes tracked bundled skills from bundled-skills', () => {
8
+ test('discoverSkills includes tracked bundled skills from skills', () => {
9
9
  const skills = discoverSkills({ cwd: path.join(process.cwd(), 'src') })
10
10
  const googleWorkspaceSkill = skills.find((skill) => skill.name === 'google-workspace')
11
11
 
12
12
  assert.ok(googleWorkspaceSkill)
13
13
  assert.equal(googleWorkspaceSkill?.source, 'bundled')
14
14
  assert.equal(
15
- googleWorkspaceSkill?.sourcePath.endsWith(path.join('bundled-skills', 'google-workspace', 'SKILL.md')),
15
+ googleWorkspaceSkill?.sourcePath.endsWith(path.join('skills', 'google-workspace', 'SKILL.md')),
16
16
  true,
17
17
  )
18
18
  })
@@ -19,7 +19,7 @@ interface DiscoveryCache {
19
19
  }
20
20
 
21
21
  const CACHE_TTL_MS = 5_000
22
- const BUNDLED_SKILLS_DIR = path.join(process.cwd(), 'bundled-skills')
22
+ const BUNDLED_SKILLS_DIR = path.join(process.cwd(), 'skills')
23
23
  const LEGACY_BUNDLED_SKILLS_DIR = path.join(DATA_DIR, 'skills')
24
24
 
25
25
  let cache: DiscoveryCache | null = null
@@ -84,7 +84,7 @@ function scanLayer(
84
84
 
85
85
  /**
86
86
  * Discover skills from three layers:
87
- * 1. Bundled: `bundled-skills/` (tracked with the app)
87
+ * 1. Bundled: `skills/` (tracked with the app)
88
88
  * Legacy fallback: `data/skills/`
89
89
  * 2. Workspace: `<swarmclaw-home>/skills/` (user-installed)
90
90
  * 3. Project: `<cwd>/skills/` (project-local)
@@ -0,0 +1,14 @@
1
+ export {
2
+ deleteLearnedSkill,
3
+ deleteSkill,
4
+ loadLearnedSkill,
5
+ loadLearnedSkills,
6
+ loadSkillSuggestions,
7
+ loadSkills,
8
+ patchLearnedSkill,
9
+ patchSkillSuggestion,
10
+ saveLearnedSkills,
11
+ saveSkills,
12
+ upsertLearnedSkill,
13
+ upsertSkillSuggestion,
14
+ } from '@/lib/server/storage'
@@ -1,8 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import crypto from 'crypto'
4
- import os from 'os'
5
- import type { ChildProcess } from 'node:child_process'
6
4
  import Database from 'better-sqlite3'
7
5
 
8
6
  import { perf } from '@/lib/server/runtime/perf'
@@ -105,11 +103,6 @@ export function withTransaction<T>(fn: () => T): T {
105
103
  type StoredObject = Record<string, unknown>
106
104
  type StoredSessionRecord = Session
107
105
  type StoredAgentRecord = Agent
108
- type ActiveProcess = ChildProcess | {
109
- runId?: string | null
110
- source?: string
111
- kill: (signal?: NodeJS.Signals | number) => boolean | void
112
- }
113
106
 
114
107
  // Collection tables (id → JSON blob)
115
108
  const COLLECTIONS = [
@@ -306,7 +299,18 @@ function deleteCollectionItem(table: string, id: string) {
306
299
  db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id)
307
300
  const cached = collectionCache.get(table)
308
301
  if (cached) cached.delete(id)
302
+ invalidateDerivedCollectionCaches(table)
303
+ }
304
+
305
+ function invalidateDerivedCollectionCaches(table: string): void {
309
306
  factoryTtlCaches.get(table)?.invalidate()
307
+ if (table === 'sessions') {
308
+ getSessionsCache().invalidate()
309
+ return
310
+ }
311
+ if (table === 'agents') {
312
+ getAgentsCache().invalidate()
313
+ }
310
314
  }
311
315
 
312
316
  /**
@@ -322,7 +326,7 @@ function upsertCollectionItem(table: string, id: string, value: unknown) {
322
326
  if (cached) {
323
327
  cached.set(id, serialized)
324
328
  }
325
- factoryTtlCaches.get(table)?.invalidate()
329
+ invalidateDerivedCollectionCaches(table)
326
330
  }
327
331
 
328
332
  function loadCollectionItem(table: string, id: string): unknown | null {
@@ -356,7 +360,7 @@ function upsertCollectionItems(table: string, entries: Array<[string, unknown]>)
356
360
  cached.set(id, serialized)
357
361
  }
358
362
  }
359
- factoryTtlCaches.get(table)?.invalidate()
363
+ invalidateDerivedCollectionCaches(table)
360
364
  }
361
365
 
362
366
  export function loadStoredItem(table: StorageCollection, id: string): unknown | null {
@@ -1438,21 +1442,6 @@ const webhooksStore = createCollectionStore('webhooks')
1438
1442
  export const loadWebhooks = webhooksStore.load
1439
1443
  export const saveWebhooks = webhooksStore.save
1440
1444
 
1441
- // --- Active processes ---
1442
- export const active = new Map<string, ActiveProcess>()
1443
- export const devServers = new Map<string, { proc: ChildProcess; url: string }>()
1444
-
1445
- // --- Utilities ---
1446
- export function localIP(): string {
1447
- for (const ifaces of Object.values(os.networkInterfaces())) {
1448
- if (!ifaces) continue
1449
- for (const i of ifaces) {
1450
- if (i.family === 'IPv4' && !i.internal) return i.address
1451
- }
1452
- }
1453
- return 'localhost'
1454
- }
1455
-
1456
1445
  // --- MCP Servers ---
1457
1446
  const mcpServersStore = createCollectionStore('mcp_servers')
1458
1447
  export const loadMcpServers = mcpServersStore.load
@@ -0,0 +1,54 @@
1
+ import type { BoardTask } from '@/types'
2
+
3
+ import {
4
+ deleteTask as deleteStoredTask,
5
+ loadTask as loadStoredTask,
6
+ loadTasks as loadStoredTasks,
7
+ patchTask as patchStoredTask,
8
+ saveTasks as saveStoredTasks,
9
+ upsertTask as upsertStoredTask,
10
+ upsertTasks as upsertStoredTasks,
11
+ } from '@/lib/server/storage'
12
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
13
+
14
+ export const taskRepository = createRecordRepository<BoardTask>(
15
+ 'tasks',
16
+ {
17
+ get(id) {
18
+ return loadStoredTask(id) as BoardTask | null
19
+ },
20
+ list() {
21
+ return loadStoredTasks() as Record<string, BoardTask>
22
+ },
23
+ upsert(id, value) {
24
+ upsertStoredTask(id, value as BoardTask)
25
+ },
26
+ upsertMany(entries) {
27
+ upsertStoredTasks(entries as Array<[string, BoardTask]>)
28
+ },
29
+ replace(data) {
30
+ saveStoredTasks(data as Record<string, BoardTask>)
31
+ },
32
+ patch(id, updater) {
33
+ return patchStoredTask(id, updater as (current: BoardTask | null) => BoardTask | null) as BoardTask | null
34
+ },
35
+ delete(id) {
36
+ deleteStoredTask(id)
37
+ },
38
+ },
39
+ )
40
+
41
+ export const getTask = (id: string) => taskRepository.get(id)
42
+ export const getTasks = (ids: string[]) => taskRepository.getMany(ids)
43
+ export const listTasks = () => taskRepository.list()
44
+ export const saveTask = (id: string, task: BoardTask | Record<string, unknown>) => taskRepository.upsert(id, task as BoardTask)
45
+ export const saveTaskMany = (entries: Array<[string, BoardTask | Record<string, unknown>]>) => taskRepository.upsertMany(entries as Array<[string, BoardTask]>)
46
+ export const replaceTasks = (items: Record<string, BoardTask | Record<string, unknown>>) => taskRepository.replace(items as Record<string, BoardTask>)
47
+ export const patchTask = (id: string, updater: (current: BoardTask | null) => BoardTask | null) => taskRepository.patch(id, updater)
48
+ export const deleteTask = (id: string) => taskRepository.delete(id)
49
+
50
+ export const loadTasks = listTasks
51
+ export const loadTask = getTask
52
+ export const saveTasks = replaceTasks
53
+ export const upsertTask = saveTask
54
+ export const upsertTasks = saveTaskMany
@@ -0,0 +1,30 @@
1
+ import type { UsageRecord } from '@/types'
2
+
3
+ import {
4
+ appendUsage as appendStoredUsage,
5
+ getUsageSpendSince as getStoredUsageSpendSince,
6
+ loadUsage as loadStoredUsage,
7
+ pruneOldUsage as pruneStoredUsage,
8
+ saveUsage as saveStoredUsage,
9
+ } from '@/lib/server/storage'
10
+ import { perf } from '@/lib/server/runtime/perf'
11
+
12
+ export function loadUsage(): Record<string, UsageRecord[]> {
13
+ return perf.measureSync('repository', 'usage.list', () => loadStoredUsage())
14
+ }
15
+
16
+ export function saveUsage(data: Record<string, UsageRecord[]>): void {
17
+ perf.measureSync('repository', 'usage.replace', () => saveStoredUsage(data), { count: Object.keys(data).length })
18
+ }
19
+
20
+ export function appendUsage(sessionId: string, record: unknown): void {
21
+ perf.measureSync('repository', 'usage.append', () => appendStoredUsage(sessionId, record), { sessionId })
22
+ }
23
+
24
+ export function getUsageSpendSince(minTimestamp: number): number {
25
+ return perf.measureSync('repository', 'usage.spendSince', () => getStoredUsageSpendSince(minTimestamp), { minTimestamp })
26
+ }
27
+
28
+ export function pruneOldUsage(maxAgeMs: number): number {
29
+ return perf.measureSync('repository', 'usage.prune', () => pruneStoredUsage(maxAgeMs), { maxAgeMs })
30
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ appendWebhookLog,
3
+ deleteWebhookRetry,
4
+ loadWebhookLogs,
5
+ loadWebhookRetryQueue,
6
+ loadWebhooks,
7
+ saveWebhookRetryQueue,
8
+ saveWebhooks,
9
+ upsertWebhookRetry,
10
+ } from '@/lib/server/storage'
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
2
3
  import { stripInternalJson, stripLoopDetectionMessages, stripAllInternalMetadata } from './strip-internal-metadata'
3
4
 
4
5
  // ---------------------------------------------------------------------------
@@ -8,37 +9,37 @@ import { stripInternalJson, stripLoopDetectionMessages, stripAllInternalMetadata
8
9
  describe('stripInternalJson', () => {
9
10
  it('removes single-line classification JSON', () => {
10
11
  const input = '{ "isDeliverableTask": true, "quality_score": 0.8, "isBroadGoal": false }'
11
- expect(stripInternalJson(input).trim()).toBe('')
12
+ assert.equal(stripInternalJson(input).trim(), '')
12
13
  })
13
14
 
14
15
  it('removes classification JSON embedded in surrounding text', () => {
15
16
  const input = 'Here is the answer.\n{ "isDeliverableTask": true, "confidence": 0.9 }\nMore text follows.'
16
17
  const result = stripInternalJson(input)
17
- expect(result).toContain('Here is the answer.')
18
- expect(result).toContain('More text follows.')
19
- expect(result).not.toContain('isDeliverableTask')
18
+ assert.match(result, /Here is the answer\./)
19
+ assert.match(result, /More text follows\./)
20
+ assert.doesNotMatch(result, /isDeliverableTask/)
20
21
  })
21
22
 
22
23
  it('preserves legitimate JSON that does not contain internal keys', () => {
23
24
  const input = 'The config is { "name": "test", "port": 3000 }'
24
- expect(stripInternalJson(input)).toBe(input)
25
+ assert.equal(stripInternalJson(input), input)
25
26
  })
26
27
 
27
28
  it('preserves JSON with nested objects if no internal keys', () => {
28
29
  const input = '{ "user": { "name": "alice" } }'
29
- expect(stripInternalJson(input)).toBe(input)
30
+ assert.equal(stripInternalJson(input), input)
30
31
  })
31
32
 
32
33
  it('removes JSON with nested objects when internal keys are present', () => {
33
34
  const input = '{ "walletIntent": "send", "details": { "amount": 100 } }'
34
- expect(stripInternalJson(input).trim()).toBe('')
35
+ assert.equal(stripInternalJson(input).trim(), '')
35
36
  })
36
37
 
37
38
  it('handles multiple JSON blocks, only removing internal ones', () => {
38
39
  const input = '{ "isDeliverableTask": true } some text { "foo": "bar" }'
39
40
  const result = stripInternalJson(input)
40
- expect(result).not.toContain('isDeliverableTask')
41
- expect(result).toContain('{ "foo": "bar" }')
41
+ assert.doesNotMatch(result, /isDeliverableTask/)
42
+ assert.match(result, /\{ "foo": "bar" \}/)
42
43
  })
43
44
  })
44
45
 
@@ -49,110 +50,110 @@ describe('stripInternalJson', () => {
49
50
  describe('stripLoopDetectionMessages', () => {
50
51
  it('strips tool frequency "called N times" messages', () => {
51
52
  const input = 'Tool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.'
52
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
53
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
53
54
  })
54
55
 
55
56
  it('strips tool frequency "would be called" messages', () => {
56
57
  const input = 'Tool "shell" would be called 31 times this turn. Excessive repetition — wrap up with available results.'
57
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
58
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
58
59
  })
59
60
 
60
61
  it('strips "nearing overuse" messages', () => {
61
62
  const input = 'Tool "read" is nearing overuse (15 calls this turn). Consider whether another call is needed.'
62
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
63
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
63
64
  })
64
65
 
65
66
  it('strips generic repeat "You called" messages', () => {
66
67
  const input = 'You called "browser" 6 times with identical input. Input: "{"action":"screenshot"}" — State your blocker or deliver what you have.'
67
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
68
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
68
69
  })
69
70
 
70
71
  it('strips generic repeat "You called" warning messages', () => {
71
72
  const input = 'You called "search" 4 times with identical input. Input: "query" — Try a fundamentally different approach or deliver partial results.'
72
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
73
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
73
74
  })
74
75
 
75
76
  it('strips "would repeat the same input" messages', () => {
76
77
  const input = '"search" would repeat the same input 12 times. Input: "query" — State your blocker or deliver what you have.'
77
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
78
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
78
79
  })
79
80
 
80
81
  it('strips "is about to repeat the same input" messages', () => {
81
82
  const input = '"search" is about to repeat the same input 6 times. Input: "query" — Try a different approach.'
82
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
83
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
83
84
  })
84
85
 
85
86
  it('strips circuit breaker messages', () => {
86
87
  const input = 'Circuit breaker: "shell" called 20 times with identical input. Halting to prevent runaway.'
87
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
88
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
88
89
  })
89
90
 
90
91
  it('strips circuit breaker preview messages', () => {
91
92
  const input = 'Circuit breaker: "shell" would be called 20 times with identical input. Halting before another runaway call.'
92
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
93
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
93
94
  })
94
95
 
95
96
  it('strips polling stall messages', () => {
96
97
  const input = 'Polling stall: "status_check" returned identical output 8 times consecutively. The polled resource is not changing.'
97
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
98
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
98
99
  })
99
100
 
100
101
  it('strips ping-pong "are alternating" messages', () => {
101
102
  const input = 'Ping-pong: "read" and "write" are alternating with identical results (5 cycles). Breaking the loop.'
102
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
103
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
103
104
  })
104
105
 
105
106
  it('strips ping-pong "may be stuck" messages', () => {
106
107
  const input = 'Ping-pong: "read" and "write" may be stuck in an alternating loop (3 cycles).'
107
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
108
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
108
109
  })
109
110
 
110
111
  it('strips output stagnation messages', () => {
111
112
  const input = 'Output stagnation: last 8 tool calls all produced identical output. The approach is not working — try something fundamentally different or report the blocker.'
112
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
113
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
113
114
  })
114
115
 
115
116
  it('strips output stagnation warning messages', () => {
116
117
  const input = 'Output stagnation: 6 of the last 8 tool calls produced identical output. Your tools may not be making progress.'
117
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
118
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
118
119
  })
119
120
 
120
121
  it('strips error convergence messages', () => {
121
122
  const input = 'Error convergence: 5 of the last 6 tool calls returned errors. Stop retrying and report the underlying issue (likely an infrastructure or configuration problem).'
122
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
123
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
123
124
  })
124
125
 
125
126
  it('strips error convergence warning messages', () => {
126
127
  const input = 'Error convergence: 4 of the last 6 tool calls returned errors. You may be hitting a systemic issue — consider a different approach or report the blocker.'
127
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
128
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
128
129
  })
129
130
 
130
131
  it('strips messages wrapped in [Error: ...] brackets', () => {
131
132
  const input = '[Error: You called "browser" 6 times with identical input. Input: "{"action":"screenshot"}" — State your blocker or deliver what you have.]'
132
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
133
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
133
134
  })
134
135
 
135
136
  it('strips [Error: ...] wrapped tool frequency messages', () => {
136
137
  const input = '[Error: Tool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.]'
137
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
138
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
138
139
  })
139
140
 
140
141
  it('strips [Error: ...] wrapped output stagnation messages', () => {
141
142
  const input = '[Error: Output stagnation: last 8 tool calls all produced identical output.]'
142
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
143
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
143
144
  })
144
145
 
145
146
  it('preserves normal text that mentions tools', () => {
146
147
  const input = 'I used the shell tool to run the command.'
147
- expect(stripLoopDetectionMessages(input)).toBe(input)
148
+ assert.equal(stripLoopDetectionMessages(input), input)
148
149
  })
149
150
 
150
151
  it('strips loop message embedded in surrounding text', () => {
151
152
  const input = 'Working on it.\nTool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.\nHere are the results.'
152
153
  const result = stripLoopDetectionMessages(input)
153
- expect(result).toContain('Working on it.')
154
- expect(result).toContain('Here are the results.')
155
- expect(result).not.toContain('called 30 times')
154
+ assert.match(result, /Working on it\./)
155
+ assert.match(result, /Here are the results\./)
156
+ assert.doesNotMatch(result, /called 30 times/)
156
157
  })
157
158
  })
158
159
 
@@ -169,32 +170,32 @@ describe('stripAllInternalMetadata', () => {
169
170
  'The answer is 42.',
170
171
  ].join('\n')
171
172
  const result = stripAllInternalMetadata(input)
172
- expect(result).not.toContain('isDeliverableTask')
173
- expect(result).not.toContain('called 30 times')
174
- expect(result).toContain('Here is my analysis.')
175
- expect(result).toContain('The answer is 42.')
173
+ assert.doesNotMatch(result, /isDeliverableTask/)
174
+ assert.doesNotMatch(result, /called 30 times/)
175
+ assert.match(result, /Here is my analysis\./)
176
+ assert.match(result, /The answer is 42\./)
176
177
  })
177
178
 
178
179
  it('collapses excessive newlines after stripping', () => {
179
180
  const input = 'Hello.\n\n\n\n{ "isDeliverableTask": true }\n\n\n\nWorld.'
180
181
  const result = stripAllInternalMetadata(input)
181
- expect(result).not.toMatch(/\n{3,}/)
182
+ assert.doesNotMatch(result, /\n{3,}/)
182
183
  })
183
184
 
184
185
  it('returns empty string for purely internal content', () => {
185
186
  const input = '{ "isDeliverableTask": true, "quality_score": 0.9 }'
186
- expect(stripAllInternalMetadata(input)).toBe('')
187
+ assert.equal(stripAllInternalMetadata(input), '')
187
188
  })
188
189
 
189
190
  it('leaves normal messages untouched', () => {
190
191
  const input = 'Here is a normal response with no internal metadata.'
191
- expect(stripAllInternalMetadata(input)).toBe(input)
192
+ assert.equal(stripAllInternalMetadata(input), input)
192
193
  })
193
194
 
194
195
  it('preserves code blocks with JSON that happen to have similar-looking keys', () => {
195
196
  // JSON in code blocks uses real braces, so the regex will match the block.
196
197
  // But since 'name' and 'age' are not internal keys, it should be preserved.
197
198
  const input = 'Result: { "name": "Alice", "age": 30 }'
198
- expect(stripAllInternalMetadata(input)).toBe(input)
199
+ assert.equal(stripAllInternalMetadata(input), input)
199
200
  })
200
201
  })