@swarmclawai/swarmclaw 0.3.1 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { BaseCheckpointSaver } from '@langchain/langgraph'
|
|
2
|
+
import type { RunnableConfig } from '@langchain/core/runnables'
|
|
3
|
+
import type {
|
|
4
|
+
Checkpoint,
|
|
5
|
+
CheckpointTuple,
|
|
6
|
+
CheckpointListOptions,
|
|
7
|
+
} from '@langchain/langgraph-checkpoint'
|
|
8
|
+
import type {
|
|
9
|
+
CheckpointMetadata,
|
|
10
|
+
PendingWrite,
|
|
11
|
+
CheckpointPendingWrite,
|
|
12
|
+
} from '@langchain/langgraph-checkpoint'
|
|
13
|
+
import Database from 'better-sqlite3'
|
|
14
|
+
import path from 'path'
|
|
15
|
+
import { DATA_DIR } from './data-dir'
|
|
16
|
+
|
|
17
|
+
const DB_PATH = path.join(DATA_DIR, 'swarmclaw.db')
|
|
18
|
+
|
|
19
|
+
function getDb(): Database.Database {
|
|
20
|
+
const db = new Database(DB_PATH)
|
|
21
|
+
db.pragma('journal_mode = WAL')
|
|
22
|
+
return db
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Ensure tables exist
|
|
26
|
+
const initDb = getDb()
|
|
27
|
+
initDb.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS langgraph_checkpoints (
|
|
29
|
+
thread_id TEXT NOT NULL,
|
|
30
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
31
|
+
checkpoint_id TEXT NOT NULL,
|
|
32
|
+
parent_checkpoint_id TEXT,
|
|
33
|
+
type TEXT NOT NULL DEFAULT 'json',
|
|
34
|
+
checkpoint BLOB NOT NULL,
|
|
35
|
+
metadata BLOB NOT NULL DEFAULT '{}',
|
|
36
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
37
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
|
38
|
+
)
|
|
39
|
+
`)
|
|
40
|
+
initDb.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS langgraph_writes (
|
|
42
|
+
thread_id TEXT NOT NULL,
|
|
43
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
44
|
+
checkpoint_id TEXT NOT NULL,
|
|
45
|
+
task_id TEXT NOT NULL,
|
|
46
|
+
idx INTEGER NOT NULL,
|
|
47
|
+
channel TEXT NOT NULL,
|
|
48
|
+
type TEXT NOT NULL DEFAULT 'json',
|
|
49
|
+
value BLOB,
|
|
50
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
51
|
+
)
|
|
52
|
+
`)
|
|
53
|
+
initDb.close()
|
|
54
|
+
|
|
55
|
+
function getThreadId(config: RunnableConfig): string {
|
|
56
|
+
return (config.configurable?.thread_id as string) || ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getCheckpointNs(config: RunnableConfig): string {
|
|
60
|
+
return (config.configurable?.checkpoint_ns as string) || ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getCheckpointId(config: RunnableConfig): string | undefined {
|
|
64
|
+
return config.configurable?.checkpoint_id as string | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class SqliteCheckpointSaver extends BaseCheckpointSaver {
|
|
68
|
+
private db: Database.Database
|
|
69
|
+
|
|
70
|
+
constructor() {
|
|
71
|
+
super()
|
|
72
|
+
this.db = getDb()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
|
|
76
|
+
const threadId = getThreadId(config)
|
|
77
|
+
if (!threadId) return undefined
|
|
78
|
+
|
|
79
|
+
const ns = getCheckpointNs(config)
|
|
80
|
+
const checkpointId = getCheckpointId(config)
|
|
81
|
+
|
|
82
|
+
let row: any
|
|
83
|
+
if (checkpointId) {
|
|
84
|
+
row = this.db.prepare(
|
|
85
|
+
`SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?`
|
|
86
|
+
).get(threadId, ns, checkpointId)
|
|
87
|
+
} else {
|
|
88
|
+
row = this.db.prepare(
|
|
89
|
+
`SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ? ORDER BY created_at DESC LIMIT 1`
|
|
90
|
+
).get(threadId, ns)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!row) return undefined
|
|
94
|
+
|
|
95
|
+
const checkpoint = JSON.parse(
|
|
96
|
+
typeof row.checkpoint === 'string' ? row.checkpoint : Buffer.from(row.checkpoint).toString()
|
|
97
|
+
) as Checkpoint
|
|
98
|
+
const metadata = JSON.parse(
|
|
99
|
+
typeof row.metadata === 'string' ? row.metadata : Buffer.from(row.metadata).toString()
|
|
100
|
+
) as CheckpointMetadata
|
|
101
|
+
|
|
102
|
+
const resultConfig: RunnableConfig = {
|
|
103
|
+
configurable: {
|
|
104
|
+
thread_id: threadId,
|
|
105
|
+
checkpoint_ns: ns,
|
|
106
|
+
checkpoint_id: row.checkpoint_id,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parentConfig = row.parent_checkpoint_id
|
|
111
|
+
? {
|
|
112
|
+
configurable: {
|
|
113
|
+
thread_id: threadId,
|
|
114
|
+
checkpoint_ns: ns,
|
|
115
|
+
checkpoint_id: row.parent_checkpoint_id,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: undefined
|
|
119
|
+
|
|
120
|
+
// Load pending writes
|
|
121
|
+
const writeRows = this.db.prepare(
|
|
122
|
+
`SELECT task_id, channel, value FROM langgraph_writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY idx`
|
|
123
|
+
).all(threadId, ns, row.checkpoint_id) as any[]
|
|
124
|
+
|
|
125
|
+
const pendingWrites: CheckpointPendingWrite[] = writeRows.map((w) => [
|
|
126
|
+
w.task_id,
|
|
127
|
+
w.channel,
|
|
128
|
+
w.value ? JSON.parse(typeof w.value === 'string' ? w.value : Buffer.from(w.value).toString()) : undefined,
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
config: resultConfig,
|
|
133
|
+
checkpoint,
|
|
134
|
+
metadata,
|
|
135
|
+
parentConfig,
|
|
136
|
+
pendingWrites,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async *list(
|
|
141
|
+
config: RunnableConfig,
|
|
142
|
+
options?: CheckpointListOptions,
|
|
143
|
+
): AsyncGenerator<CheckpointTuple> {
|
|
144
|
+
const threadId = getThreadId(config)
|
|
145
|
+
if (!threadId) return
|
|
146
|
+
|
|
147
|
+
const ns = getCheckpointNs(config)
|
|
148
|
+
const limit = options?.limit ?? 100
|
|
149
|
+
|
|
150
|
+
let query = `SELECT * FROM langgraph_checkpoints WHERE thread_id = ? AND checkpoint_ns = ?`
|
|
151
|
+
const params: any[] = [threadId, ns]
|
|
152
|
+
|
|
153
|
+
if (options?.before?.configurable?.checkpoint_id) {
|
|
154
|
+
query += ` AND checkpoint_id < ?`
|
|
155
|
+
params.push(options.before.configurable.checkpoint_id)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
query += ` ORDER BY created_at DESC LIMIT ?`
|
|
159
|
+
params.push(limit)
|
|
160
|
+
|
|
161
|
+
const rows = this.db.prepare(query).all(...params) as any[]
|
|
162
|
+
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
const checkpoint = JSON.parse(
|
|
165
|
+
typeof row.checkpoint === 'string' ? row.checkpoint : Buffer.from(row.checkpoint).toString()
|
|
166
|
+
) as Checkpoint
|
|
167
|
+
const metadata = JSON.parse(
|
|
168
|
+
typeof row.metadata === 'string' ? row.metadata : Buffer.from(row.metadata).toString()
|
|
169
|
+
) as CheckpointMetadata
|
|
170
|
+
|
|
171
|
+
const resultConfig: RunnableConfig = {
|
|
172
|
+
configurable: {
|
|
173
|
+
thread_id: threadId,
|
|
174
|
+
checkpoint_ns: ns,
|
|
175
|
+
checkpoint_id: row.checkpoint_id,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parentConfig = row.parent_checkpoint_id
|
|
180
|
+
? {
|
|
181
|
+
configurable: {
|
|
182
|
+
thread_id: threadId,
|
|
183
|
+
checkpoint_ns: ns,
|
|
184
|
+
checkpoint_id: row.parent_checkpoint_id,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
: undefined
|
|
188
|
+
|
|
189
|
+
yield {
|
|
190
|
+
config: resultConfig,
|
|
191
|
+
checkpoint,
|
|
192
|
+
metadata,
|
|
193
|
+
parentConfig,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async put(
|
|
199
|
+
config: RunnableConfig,
|
|
200
|
+
checkpoint: Checkpoint,
|
|
201
|
+
metadata: CheckpointMetadata,
|
|
202
|
+
_newVersions: Record<string, number | string>,
|
|
203
|
+
): Promise<RunnableConfig> {
|
|
204
|
+
const threadId = getThreadId(config)
|
|
205
|
+
const ns = getCheckpointNs(config)
|
|
206
|
+
const parentId = getCheckpointId(config)
|
|
207
|
+
|
|
208
|
+
this.db.prepare(`
|
|
209
|
+
INSERT OR REPLACE INTO langgraph_checkpoints
|
|
210
|
+
(thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata, created_at)
|
|
211
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
212
|
+
`).run(
|
|
213
|
+
threadId,
|
|
214
|
+
ns,
|
|
215
|
+
checkpoint.id,
|
|
216
|
+
parentId || null,
|
|
217
|
+
JSON.stringify(checkpoint),
|
|
218
|
+
JSON.stringify(metadata),
|
|
219
|
+
Date.now(),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
configurable: {
|
|
224
|
+
thread_id: threadId,
|
|
225
|
+
checkpoint_ns: ns,
|
|
226
|
+
checkpoint_id: checkpoint.id,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async putWrites(
|
|
232
|
+
config: RunnableConfig,
|
|
233
|
+
writes: PendingWrite[],
|
|
234
|
+
taskId: string,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const threadId = getThreadId(config)
|
|
237
|
+
const ns = getCheckpointNs(config)
|
|
238
|
+
const checkpointId = getCheckpointId(config)
|
|
239
|
+
if (!checkpointId) return
|
|
240
|
+
|
|
241
|
+
const stmt = this.db.prepare(`
|
|
242
|
+
INSERT OR REPLACE INTO langgraph_writes
|
|
243
|
+
(thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, value)
|
|
244
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
245
|
+
`)
|
|
246
|
+
|
|
247
|
+
const insertMany = this.db.transaction((items: PendingWrite[]) => {
|
|
248
|
+
items.forEach(([channel, value], idx) => {
|
|
249
|
+
stmt.run(
|
|
250
|
+
threadId,
|
|
251
|
+
ns,
|
|
252
|
+
checkpointId,
|
|
253
|
+
taskId,
|
|
254
|
+
idx,
|
|
255
|
+
channel as string,
|
|
256
|
+
value !== undefined ? JSON.stringify(value) : null,
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
insertMany(writes)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async deleteThread(threadId: string): Promise<void> {
|
|
265
|
+
this.db.prepare(`DELETE FROM langgraph_checkpoints WHERE thread_id = ?`).run(threadId)
|
|
266
|
+
this.db.prepare(`DELETE FROM langgraph_writes WHERE thread_id = ?`).run(threadId)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let _saver: SqliteCheckpointSaver | undefined
|
|
271
|
+
export function getCheckpointSaver(): SqliteCheckpointSaver {
|
|
272
|
+
if (!_saver) _saver = new SqliteCheckpointSaver()
|
|
273
|
+
return _saver
|
|
274
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import { z } from 'zod'
|
|
2
3
|
import type { GoalContract, MessageToolEvent } from '@/types'
|
|
3
|
-
import { loadSessions, saveSessions, loadTasks, saveTasks } from './storage'
|
|
4
|
+
import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
|
|
4
5
|
import { log } from './logger'
|
|
5
6
|
import { getMemoryDb } from './memory-db'
|
|
6
7
|
import {
|
|
@@ -18,6 +19,7 @@ const MEMORY_NOTE_MIN_INTERVAL_MS = 90 * 60 * 1000
|
|
|
18
19
|
const DEFAULT_FOLLOWUP_DELAY_SEC = 45
|
|
19
20
|
const MAX_FOLLOWUP_CHAIN = 6
|
|
20
21
|
const META_LINE_RE = /\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i
|
|
22
|
+
const AGENT_HEARTBEAT_META_RE = /\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i
|
|
21
23
|
const SCREENSHOT_GOAL_HINT = /\b(screenshot|screen shot|snapshot|capture)\b/i
|
|
22
24
|
const DELIVERY_GOAL_HINT = /\b(send|deliver|return|share|upload|post|message)\b/i
|
|
23
25
|
const SCHEDULE_GOAL_HINT = /\b(schedule|scheduled|every\s+\w+|interval|cron|recurr)\b/i
|
|
@@ -149,7 +151,7 @@ function appendTimeline(
|
|
|
149
151
|
const recent = state.timeline.at(-1)
|
|
150
152
|
if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
|
|
151
153
|
state.timeline.push({
|
|
152
|
-
id: `tl_${
|
|
154
|
+
id: `tl_${genId()}`,
|
|
153
155
|
at: now,
|
|
154
156
|
source,
|
|
155
157
|
note: normalizedNote,
|
|
@@ -224,7 +226,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
|
224
226
|
const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
|
|
225
227
|
if (!text) return null
|
|
226
228
|
return {
|
|
227
|
-
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${
|
|
229
|
+
id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${genId(3)}`,
|
|
228
230
|
type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
|
|
229
231
|
text,
|
|
230
232
|
createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
|
|
@@ -244,7 +246,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
|
|
|
244
246
|
? entry.status
|
|
245
247
|
: undefined
|
|
246
248
|
return {
|
|
247
|
-
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${
|
|
249
|
+
id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${genId(3)}`,
|
|
248
250
|
at: typeof entry?.at === 'number' ? entry.at : now,
|
|
249
251
|
source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
|
|
250
252
|
note,
|
|
@@ -301,7 +303,7 @@ function appendEvent(state: MainLoopState, type: string, text: string, now = Dat
|
|
|
301
303
|
return false
|
|
302
304
|
}
|
|
303
305
|
state.pendingEvents.push({
|
|
304
|
-
id: `evt_${
|
|
306
|
+
id: `evt_${genId()}`,
|
|
305
307
|
type,
|
|
306
308
|
text: normalizedText,
|
|
307
309
|
createdAt: now,
|
|
@@ -515,7 +517,7 @@ function upsertMissionTask(session: any, state: MainLoopState, now: number): str
|
|
|
515
517
|
].filter(Boolean).join('\n')
|
|
516
518
|
|
|
517
519
|
if (!task) {
|
|
518
|
-
const id =
|
|
520
|
+
const id = genId()
|
|
519
521
|
task = {
|
|
520
522
|
id,
|
|
521
523
|
title,
|
|
@@ -822,10 +824,67 @@ export function pushMainLoopEventToMainSessions(input: PushMainLoopEventInput):
|
|
|
822
824
|
return changed
|
|
823
825
|
}
|
|
824
826
|
|
|
827
|
+
const AgentHeartbeatMetaSchema = z.object({
|
|
828
|
+
goal: z.string().trim().optional(),
|
|
829
|
+
status: z.enum(['progress', 'ok', 'idle', 'blocked']).optional(),
|
|
830
|
+
next_action: z.string().trim().optional(),
|
|
831
|
+
}).passthrough()
|
|
832
|
+
|
|
833
|
+
type AgentHeartbeatMeta = z.infer<typeof AgentHeartbeatMetaSchema>
|
|
834
|
+
|
|
835
|
+
function parseAgentHeartbeatMeta(text: string): AgentHeartbeatMeta | null {
|
|
836
|
+
const raw = (text || '').trim()
|
|
837
|
+
if (!raw) return null
|
|
838
|
+
const match = raw.match(AGENT_HEARTBEAT_META_RE)
|
|
839
|
+
if (!match?.[1]) return null
|
|
840
|
+
try {
|
|
841
|
+
const parsed = JSON.parse(match[1])
|
|
842
|
+
return AgentHeartbeatMetaSchema.parse(parsed)
|
|
843
|
+
} catch {
|
|
844
|
+
return null
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function handleAgentHeartbeatResult(session: any, input: HandleMainLoopRunResultInput): null {
|
|
849
|
+
if (!input.internal || input.source !== 'heartbeat') return null
|
|
850
|
+
if (!session.agentId) return null
|
|
851
|
+
const text = input.resultText || ''
|
|
852
|
+
if (!text.trim()) return null
|
|
853
|
+
|
|
854
|
+
const meta = parseAgentHeartbeatMeta(text)
|
|
855
|
+
if (!meta) return null
|
|
856
|
+
|
|
857
|
+
const agents = loadAgents()
|
|
858
|
+
const agent = agents[session.agentId]
|
|
859
|
+
if (!agent) return null
|
|
860
|
+
|
|
861
|
+
let changed = false
|
|
862
|
+
if (meta.goal && meta.goal !== agent.heartbeatGoal) {
|
|
863
|
+
agent.heartbeatGoal = meta.goal
|
|
864
|
+
changed = true
|
|
865
|
+
log.info('agent-heartbeat', `Goal updated for agent ${agent.name}: ${meta.goal.slice(0, 120)}`)
|
|
866
|
+
}
|
|
867
|
+
if (meta.next_action) {
|
|
868
|
+
agent.heartbeatNextAction = meta.next_action
|
|
869
|
+
changed = true
|
|
870
|
+
}
|
|
871
|
+
if (meta.status) {
|
|
872
|
+
agent.heartbeatStatus = meta.status
|
|
873
|
+
changed = true
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (changed) {
|
|
877
|
+
agents[session.agentId] = agent
|
|
878
|
+
saveAgents(agents)
|
|
879
|
+
}
|
|
880
|
+
return null
|
|
881
|
+
}
|
|
882
|
+
|
|
825
883
|
export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): MainLoopFollowupRequest | null {
|
|
826
884
|
const sessions = loadSessions()
|
|
827
885
|
const session = sessions[input.sessionId]
|
|
828
|
-
if (!session
|
|
886
|
+
if (!session) return null
|
|
887
|
+
if (!isMainSession(session)) return handleAgentHeartbeatResult(session, input)
|
|
829
888
|
|
|
830
889
|
const now = Date.now()
|
|
831
890
|
const state = normalizeState(session.mainLoopState, now)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import crypto from 'crypto'
|
|
4
3
|
import fs from 'fs'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
5
|
import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
|
|
6
6
|
import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
|
|
7
7
|
import { loadSettings } from './storage'
|
|
@@ -20,12 +20,12 @@ const IMAGES_DIR = path.join(DATA_DIR, 'memory-images')
|
|
|
20
20
|
|
|
21
21
|
const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
|
|
22
22
|
const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
|
|
23
|
-
const MAX_FTS_QUERY_TERMS = 6
|
|
24
|
-
const MAX_FTS_TERM_LENGTH = 48
|
|
23
|
+
export const MAX_FTS_QUERY_TERMS = 6
|
|
24
|
+
export const MAX_FTS_TERM_LENGTH = 48
|
|
25
25
|
const MAX_FTS_RESULT_ROWS = 30
|
|
26
26
|
const MAX_MERGED_RESULTS = 50
|
|
27
27
|
|
|
28
|
-
const MEMORY_FTS_STOP_WORDS = new Set([
|
|
28
|
+
export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
29
29
|
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
|
|
30
30
|
'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
|
|
31
31
|
'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
|
|
@@ -184,7 +184,7 @@ function canonicalText(value: unknown): string {
|
|
|
184
184
|
.trim()
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function buildFtsQuery(input: string): string {
|
|
187
|
+
export function buildFtsQuery(input: string): string {
|
|
188
188
|
const tokens = String(input || '')
|
|
189
189
|
.toLowerCase()
|
|
190
190
|
.match(/[a-z0-9][a-z0-9._:/-]*/g) || []
|
|
@@ -546,7 +546,7 @@ function initDb() {
|
|
|
546
546
|
|
|
547
547
|
return {
|
|
548
548
|
add(data: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): MemoryEntry {
|
|
549
|
-
const id =
|
|
549
|
+
const id = genId(6)
|
|
550
550
|
const now = Date.now()
|
|
551
551
|
const references = normalizeReferences(data.references, data.filePaths)
|
|
552
552
|
const legacyFilePaths = referencesToLegacyFilePaths(references)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import net from 'node:net'
|
|
4
|
+
import { resolveOpenClawWorkspace } from './openclaw-sync'
|
|
5
|
+
|
|
6
|
+
const APPROVAL_TIMEOUT_MS = 30_000
|
|
7
|
+
|
|
8
|
+
interface ApprovalRequest {
|
|
9
|
+
toolName: string
|
|
10
|
+
args: Record<string, unknown>
|
|
11
|
+
socketPath?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ApprovalResponse {
|
|
15
|
+
approved: boolean
|
|
16
|
+
reason?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveSocketPath(): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const workspace = resolveOpenClawWorkspace()
|
|
22
|
+
const socketPath = path.join(workspace, 'exec-approvals.sock')
|
|
23
|
+
if (fs.existsSync(socketPath)) return socketPath
|
|
24
|
+
} catch { /* workspace not found */ }
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveApprovalToken(): string | null {
|
|
29
|
+
try {
|
|
30
|
+
const workspace = resolveOpenClawWorkspace()
|
|
31
|
+
const tokenPath = path.join(workspace, 'exec-approvals.json')
|
|
32
|
+
if (!fs.existsSync(tokenPath)) return null
|
|
33
|
+
const raw = JSON.parse(fs.readFileSync(tokenPath, 'utf8'))
|
|
34
|
+
return typeof raw?.token === 'string' ? raw.token : null
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Forward a tool approval request to OpenClaw's exec-approvals Unix socket.
|
|
42
|
+
* Returns the approval decision, or null if the socket is unavailable.
|
|
43
|
+
*/
|
|
44
|
+
export async function forwardApprovalToOpenClaw(request: ApprovalRequest): Promise<ApprovalResponse | null> {
|
|
45
|
+
const socketPath = request.socketPath || resolveSocketPath()
|
|
46
|
+
if (!socketPath) return null
|
|
47
|
+
|
|
48
|
+
const token = resolveApprovalToken()
|
|
49
|
+
|
|
50
|
+
return new Promise<ApprovalResponse | null>((resolve) => {
|
|
51
|
+
const socket = net.createConnection({ path: socketPath }, () => {
|
|
52
|
+
const payload = JSON.stringify({
|
|
53
|
+
type: 'approval_request',
|
|
54
|
+
toolName: request.toolName,
|
|
55
|
+
args: request.args,
|
|
56
|
+
token,
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
})
|
|
59
|
+
socket.write(payload + '\n')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
let data = ''
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
socket.destroy()
|
|
65
|
+
resolve(null) // Timeout — fall through to SwarmClaw UI
|
|
66
|
+
}, APPROVAL_TIMEOUT_MS)
|
|
67
|
+
|
|
68
|
+
socket.on('data', (chunk) => {
|
|
69
|
+
data += chunk.toString()
|
|
70
|
+
// Try to parse complete JSON response
|
|
71
|
+
try {
|
|
72
|
+
const response = JSON.parse(data.trim())
|
|
73
|
+
clearTimeout(timer)
|
|
74
|
+
socket.destroy()
|
|
75
|
+
resolve({
|
|
76
|
+
approved: response.approved === true,
|
|
77
|
+
reason: typeof response.reason === 'string' ? response.reason : undefined,
|
|
78
|
+
})
|
|
79
|
+
} catch {
|
|
80
|
+
// Incomplete data, wait for more
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
socket.on('error', () => {
|
|
85
|
+
clearTimeout(timer)
|
|
86
|
+
resolve(null) // Socket error — fall through
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
socket.on('close', () => {
|
|
90
|
+
clearTimeout(timer)
|
|
91
|
+
// If we haven't resolved yet, try to parse what we have
|
|
92
|
+
if (data.trim()) {
|
|
93
|
+
try {
|
|
94
|
+
const response = JSON.parse(data.trim())
|
|
95
|
+
resolve({
|
|
96
|
+
approved: response.approved === true,
|
|
97
|
+
reason: typeof response.reason === 'string' ? response.reason : undefined,
|
|
98
|
+
})
|
|
99
|
+
return
|
|
100
|
+
} catch { /* fall through */ }
|
|
101
|
+
}
|
|
102
|
+
resolve(null)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}
|