@swarmclawai/swarmclaw 1.1.8 → 1.2.0

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 (38) hide show
  1. package/README.md +17 -0
  2. package/next.config.ts +0 -1
  3. package/package.json +3 -3
  4. package/src/app/activity/loading.tsx +5 -0
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -1
  6. package/src/app/api/logs/route.ts +32 -5
  7. package/src/app/home/loading.tsx +5 -0
  8. package/src/app/logs/loading.tsx +5 -0
  9. package/src/app/memory/loading.tsx +5 -0
  10. package/src/app/tasks/loading.tsx +5 -0
  11. package/src/components/agents/agent-list.tsx +7 -12
  12. package/src/components/chat/chat-area.tsx +3 -3
  13. package/src/components/chat/chat-list.tsx +13 -2
  14. package/src/components/layout/sidebar-rail.tsx +14 -6
  15. package/src/components/memory/memory-graph-view.tsx +120 -68
  16. package/src/components/org-chart/mini-chat-bubble.tsx +58 -16
  17. package/src/components/shared/command-palette.tsx +35 -20
  18. package/src/components/shared/notification-center.tsx +1 -1
  19. package/src/hooks/use-app-bootstrap.ts +2 -1
  20. package/src/instrumentation.ts +27 -0
  21. package/src/lib/providers/anthropic.ts +14 -8
  22. package/src/lib/providers/openai.ts +3 -3
  23. package/src/lib/server/agents/agent-thread-session.ts +20 -14
  24. package/src/lib/server/chat-execution/continuation-evaluator.ts +2 -2
  25. package/src/lib/server/chat-execution/message-classifier.ts +2 -1
  26. package/src/lib/server/chat-execution/stream-agent-chat.ts +4 -19
  27. package/src/lib/server/chat-execution/stream-continuation.ts +4 -3
  28. package/src/lib/server/llm-response-cache.ts +2 -1
  29. package/src/lib/server/playwright-proxy.mjs +25 -6
  30. package/src/lib/server/runtime/run-ledger.ts +4 -4
  31. package/src/lib/server/runtime/scheduler.ts +9 -6
  32. package/src/lib/server/session-tools/skill-runtime.ts +10 -1
  33. package/src/lib/server/storage-cache.ts +2 -0
  34. package/src/lib/server/storage.ts +30 -3
  35. package/src/lib/server/tasks/task-quality-gate.test.ts +1 -1
  36. package/src/lib/server/tasks/task-quality-gate.ts +1 -1
  37. package/src/stores/slices/data-slice.ts +11 -0
  38. package/src/stores/slices/session-slice.ts +3 -0
@@ -266,6 +266,15 @@ async function dispatchSkillRun(params: {
266
266
  const toolArgs = normalizeDispatchArgs(params.rawArgs)
267
267
  const { buildSessionTools } = await import('./index')
268
268
  const built = await buildSessionTools(params.bctx.cwd, params.bctx.activeExtensions, params.bctx.ctx)
269
+ if (!built?.tools) {
270
+ return JSON.stringify({
271
+ ok: false,
272
+ executed: false,
273
+ mode: 'dispatch_blocked',
274
+ skill: summarizeRuntimeSkill(params.skill),
275
+ blocker: 'Unable to load session tools for skill dispatch.',
276
+ })
277
+ }
269
278
  try {
270
279
  const targetTool = built.tools.find((entry) => entry.name === dispatch.toolName)
271
280
  if (!targetTool) {
@@ -298,7 +307,7 @@ async function dispatchSkillRun(params: {
298
307
  : toolOutput,
299
308
  })
300
309
  } finally {
301
- await built.cleanup()
310
+ await built?.cleanup()
302
311
  }
303
312
  }
304
313
 
@@ -38,11 +38,13 @@ export class TTLCache<T> {
38
38
  type TTLCacheStore = {
39
39
  settings?: TTLCache<AppSettings>
40
40
  agents?: TTLCache<Record<string, unknown>>
41
+ sessions?: TTLCache<Record<string, unknown>>
41
42
  }
42
43
  const ttlCaches: TTLCacheStore = hmrSingleton<TTLCacheStore>('__swarmclaw_ttl_caches__', () => ({}))
43
44
 
44
45
  export function getSettingsCache() { return ttlCaches.settings ?? (ttlCaches.settings = new TTLCache(60_000)) }
45
46
  export function getAgentsCache() { return ttlCaches.agents ?? (ttlCaches.agents = new TTLCache(15_000)) }
47
+ export function getSessionsCache() { return ttlCaches.sessions ?? (ttlCaches.sessions = new TTLCache(5_000)) }
46
48
 
47
49
  // --- LRU Cache ---
48
50
 
@@ -48,6 +48,7 @@ import {
48
48
  capacityFor,
49
49
  getSettingsCache,
50
50
  getAgentsCache,
51
+ getSessionsCache,
51
52
  } from './storage-cache'
52
53
  import { normalizeStoredRecord, type NormalizationResult } from './storage-normalization'
53
54
  import {
@@ -86,6 +87,9 @@ const db = new Database(DB_PATH)
86
87
  if (!IS_BUILD_BOOTSTRAP) {
87
88
  db.pragma('journal_mode = WAL')
88
89
  db.pragma('busy_timeout = 5000')
90
+ db.pragma('synchronous = NORMAL')
91
+ db.pragma('cache_size = -64000')
92
+ db.pragma('mmap_size = 268435456')
89
93
  }
90
94
  db.pragma('foreign_keys = ON')
91
95
 
@@ -710,8 +714,12 @@ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "ho
710
714
 
711
715
  // --- Sessions ---
712
716
  export function loadSessions(): Record<string, StoredSessionRecord> {
717
+ const sessionsCache = getSessionsCache()
718
+ const cached = sessionsCache.get()
719
+ if (cached) return structuredClone(cached) as unknown as Record<string, StoredSessionRecord>
720
+
713
721
  const sessions = loadCollection('sessions') as unknown as Record<string, StoredSessionRecord>
714
- const agents = loadCollection('agents')
722
+ const agents = loadAgents()
715
723
  const changedEntries: Array<[string, StoredSessionRecord]> = []
716
724
 
717
725
  for (const [id, session] of Object.entries(sessions)) {
@@ -750,6 +758,7 @@ export function loadSessions(): Record<string, StoredSessionRecord> {
750
758
 
751
759
  // Upsert only changed entries — never full-replace, which deletes concurrent sessions
752
760
  if (changedEntries.length > 0) upsertCollectionItems('sessions', changedEntries)
761
+ sessionsCache.set(sessions as unknown as Record<string, unknown>)
753
762
  return sessions
754
763
  }
755
764
 
@@ -761,6 +770,7 @@ export function saveSessions(s: Record<string, Session | StoredObject>) {
761
770
  normalizeValue('sessions', structuredClone(session as unknown as StoredObject)),
762
771
  ])
763
772
  if (entries.length > 0) upsertCollectionItems('sessions', entries)
773
+ getSessionsCache().invalidate()
764
774
  }
765
775
 
766
776
  export function loadSession(id: string): Session | null {
@@ -769,10 +779,13 @@ export function loadSession(id: string): Session | null {
769
779
 
770
780
  export function upsertSession(id: string, session: Session | Record<string, unknown>) {
771
781
  upsertCollectionItem('sessions', id, session)
782
+ getSessionsCache().invalidate()
772
783
  }
773
784
 
774
785
  export function patchSession(id: string, updater: (current: Session | null) => Session | null): Session | null {
775
- return patchStoredItem<Session>('sessions', id, updater)
786
+ const result = patchStoredItem<Session>('sessions', id, updater)
787
+ getSessionsCache().invalidate()
788
+ return result
776
789
  }
777
790
 
778
791
  export function disableAllSessionHeartbeats(): number {
@@ -966,7 +979,7 @@ export const upsertTask = tasksStore.upsert
966
979
  export const upsertTasks = tasksStore.upsertMany
967
980
  export const patchTask = tasksStore.patch as (id: string, updater: (current: BoardTask | null) => BoardTask | null) => BoardTask | null
968
981
  export const deleteTask = tasksStore.deleteItem
969
- export function deleteSession(id: string) { deleteCollectionItem('sessions', id) }
982
+ export function deleteSession(id: string) { deleteCollectionItem('sessions', id); getSessionsCache().invalidate() }
970
983
  export function deleteAgent(id: string) { deleteCollectionItem('agents', id); getAgentsCache().invalidate() }
971
984
  export const deleteSchedule = schedulesStore.deleteItem
972
985
  export function deleteSkill(id: string) { skillsStore.deleteItem(id) }
@@ -1296,6 +1309,20 @@ export const loadRuntimeRunEvents = runtimeRunEventsStore.load as () => Record<s
1296
1309
  export const saveRuntimeRunEvents = runtimeRunEventsStore.save as (items: Record<string, RunEventRecord>) => void
1297
1310
  export const upsertRuntimeRunEvent = runtimeRunEventsStore.upsert as (id: string, value: RunEventRecord) => void
1298
1311
 
1312
+ /** Load run events filtered by runId at the SQL level (avoids full-table scan). */
1313
+ export function loadRuntimeRunEventsByRunId(runId: string): RunEventRecord[] {
1314
+ const rows = db.prepare(
1315
+ `SELECT data FROM runtime_run_events WHERE json_extract(data, '$.runId') = ? ORDER BY json_extract(data, '$.timestamp') ASC`,
1316
+ ).all(runId) as Array<{ data: string }>
1317
+ const results: RunEventRecord[] = []
1318
+ for (const row of rows) {
1319
+ try {
1320
+ results.push(JSON.parse(row.data) as RunEventRecord)
1321
+ } catch { /* skip malformed */ }
1322
+ }
1323
+ return results
1324
+ }
1325
+
1299
1326
  const runtimeEstopStore = createCollectionStore('runtime_estop')
1300
1327
  const ESTOP_STATE_ID = 'global'
1301
1328
  export const loadPersistedEstopState = () => runtimeEstopStore.loadItem(ESTOP_STATE_ID) as EstopState | null
@@ -6,7 +6,7 @@ test('normalizeTaskQualityGate uses defaults when unset', () => {
6
6
  const gate = normalizeTaskQualityGate(undefined, undefined)
7
7
  assert.equal(gate.enabled, true)
8
8
  assert.equal(gate.minResultChars, 80)
9
- assert.equal(gate.minEvidenceItems, 2)
9
+ assert.equal(gate.minEvidenceItems, 1)
10
10
  assert.equal(gate.requireVerification, false)
11
11
  assert.equal(gate.requireArtifact, false)
12
12
  assert.equal(gate.requireReport, false)
@@ -12,7 +12,7 @@ export interface NormalizedTaskQualityGate {
12
12
  export const DEFAULT_TASK_QUALITY_GATE: NormalizedTaskQualityGate = {
13
13
  enabled: true,
14
14
  minResultChars: 80,
15
- minEvidenceItems: 2,
15
+ minEvidenceItems: 1,
16
16
  requireVerification: false,
17
17
  requireArtifact: false,
18
18
  requireReport: false,
@@ -30,6 +30,8 @@ export interface DataSlice {
30
30
  loadGatewayProfiles: () => Promise<void>
31
31
  skills: Record<string, Skill>
32
32
  loadSkills: () => Promise<void>
33
+ skillDraftCount: number
34
+ loadSkillDraftCount: () => Promise<void>
33
35
  connectors: Record<string, Connector>
34
36
  loadConnectors: () => Promise<void>
35
37
  webhooks: Record<string, Webhook>
@@ -82,6 +84,15 @@ export const createDataSlice: StateCreator<AppState, [], [], DataSlice> = (set,
82
84
  loadGatewayProfiles: createLoader<AppState>(set, 'gatewayProfiles', () => api<GatewayProfile[]>('GET', '/gateways'), []),
83
85
  skills: {},
84
86
  loadSkills: createLoader<AppState>(set, 'skills', () => api<Record<string, Skill>>('GET', '/skills'), {}),
87
+ skillDraftCount: 0,
88
+ loadSkillDraftCount: async () => {
89
+ try {
90
+ const result = await api<{ total: number }>('GET', '/skill-review-counts')
91
+ setIfChanged<AppState>(set, 'skillDraftCount', result.total)
92
+ } catch (err: unknown) {
93
+ console.warn('Store error:', err)
94
+ }
95
+ },
85
96
  connectors: {},
86
97
  loadConnectors: createLoader<AppState>(set, 'connectors', () => api<Record<string, Connector>>('GET', '/connectors'), {}),
87
98
  webhooks: {},
@@ -33,6 +33,9 @@ export const createSessionSlice: StateCreator<AppState, [], [], SessionSlice> =
33
33
  await sessionRefreshDedup.dedup(id, async () => {
34
34
  try {
35
35
  const session = await fetchChat(id)
36
+ const existing = get().sessions[id]
37
+ // Skip update if the session data hasn't changed
38
+ if (existing && JSON.stringify(existing) === JSON.stringify(session)) return
36
39
  invalidateFingerprint('sessions')
37
40
  set({
38
41
  sessions: { ...get().sessions, [id]: session },