@swarmclawai/swarmclaw 0.7.3 → 0.7.4
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +3 -1
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +10 -4
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-session-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('chatroom synthetic session persistence', () => {
|
|
36
|
+
it('reuses stored synthetic sessions and preserves delegate resume state', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
|
|
39
|
+
const helpers = helpersMod.default || helpersMod
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const agent = {
|
|
44
|
+
id: 'default',
|
|
45
|
+
name: 'Molly',
|
|
46
|
+
description: '',
|
|
47
|
+
systemPrompt: '',
|
|
48
|
+
provider: 'openai',
|
|
49
|
+
model: 'gpt-4o',
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
plugins: ['delegate'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const first = helpers.ensureSyntheticSession(agent, 'room-1')
|
|
56
|
+
helpers.appendSyntheticSessionMessage(first.id, 'user', 'first prompt')
|
|
57
|
+
|
|
58
|
+
const sessions = storage.loadSessions()
|
|
59
|
+
sessions[first.id].delegateResumeIds = {
|
|
60
|
+
claudeCode: null,
|
|
61
|
+
codex: 'resume-123',
|
|
62
|
+
opencode: null,
|
|
63
|
+
gemini: null,
|
|
64
|
+
}
|
|
65
|
+
storage.saveSessions(sessions)
|
|
66
|
+
|
|
67
|
+
const second = helpers.ensureSyntheticSession({ ...agent, model: 'gpt-4.1' }, 'room-1')
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
sessionId: second.id,
|
|
70
|
+
cwd: second.cwd,
|
|
71
|
+
model: second.model,
|
|
72
|
+
messageCount: second.messages.length,
|
|
73
|
+
firstMessage: second.messages[0]?.text || '',
|
|
74
|
+
delegateResumeIds: second.delegateResumeIds,
|
|
75
|
+
plugins: second.plugins || [],
|
|
76
|
+
}))
|
|
77
|
+
`)
|
|
78
|
+
|
|
79
|
+
assert.equal(output.sessionId, 'chatroom-room-1-default')
|
|
80
|
+
assert.match(String(output.cwd), /chatrooms[\/\\]room-1$/)
|
|
81
|
+
assert.equal(output.model, 'gpt-4.1')
|
|
82
|
+
assert.equal(output.messageCount, 1)
|
|
83
|
+
assert.equal(output.firstMessage, 'first prompt')
|
|
84
|
+
assert.equal(output.delegateResumeIds?.codex, 'resume-123')
|
|
85
|
+
assert.deepEqual(output.plugins, ['delegate'])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
parseMentions,
|
|
21
21
|
compactChatroomMessages,
|
|
22
22
|
buildChatroomSystemPrompt,
|
|
23
|
-
|
|
23
|
+
ensureSyntheticSession,
|
|
24
24
|
buildAgentSystemPromptForChatroom,
|
|
25
25
|
buildHistoryForAgent,
|
|
26
26
|
resolveApiKey as resolveApiKeyHelper,
|
|
@@ -31,7 +31,7 @@ import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
|
31
31
|
import { syncSessionArchiveMemory } from '../session-archive-memory'
|
|
32
32
|
import { buildIdentityContinuityContext } from '../identity-continuity'
|
|
33
33
|
import { getProvider } from '@/lib/providers'
|
|
34
|
-
import type { Connector, MessageSource, Chatroom, ChatroomMessage, Session } from '@/types'
|
|
34
|
+
import type { Agent, Connector, MessageSource, Chatroom, ChatroomMessage, Session } from '@/types'
|
|
35
35
|
import type { ConnectorInstance, InboundMessage, InboundMedia } from './types'
|
|
36
36
|
import {
|
|
37
37
|
addAllowedSender,
|
|
@@ -258,30 +258,41 @@ export function isNoMessage(text: string): boolean {
|
|
|
258
258
|
* Stored on globalThis to survive HMR reloads in dev mode —
|
|
259
259
|
* prevents duplicate sockets fighting for the same WhatsApp session. */
|
|
260
260
|
const globalKey = '__swarmclaw_running_connectors__' as const
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
const g = globalThis as typeof globalThis & Record<string, unknown>
|
|
262
|
+
|
|
263
|
+
function getOrInitGlobalValue<T>(key: string, factory: () => T): T {
|
|
264
|
+
const existing = g[key]
|
|
265
|
+
if (existing !== undefined) return existing as T
|
|
266
|
+
const created = factory()
|
|
267
|
+
g[key] = created
|
|
268
|
+
return created
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type ConnectorSession = Session
|
|
272
|
+
type ConnectorAgent = Agent
|
|
273
|
+
|
|
263
274
|
const running: Map<string, ConnectorInstance> =
|
|
264
|
-
|
|
275
|
+
getOrInitGlobalValue(globalKey, () => new Map<string, ConnectorInstance>())
|
|
265
276
|
|
|
266
277
|
/** Most recent inbound channel per connector (used for proactive replies/default outbound target) */
|
|
267
278
|
const lastInboundKey = '__swarmclaw_connector_last_inbound__' as const
|
|
268
279
|
const lastInboundChannelByConnector: Map<string, string> =
|
|
269
|
-
|
|
280
|
+
getOrInitGlobalValue(lastInboundKey, () => new Map<string, string>())
|
|
270
281
|
|
|
271
282
|
/** Last inbound message timestamp per connector (for presence indicators) */
|
|
272
283
|
const lastInboundTimeKey = '__swarmclaw_connector_last_inbound_time__' as const
|
|
273
284
|
const lastInboundTimeByConnector: Map<string, number> =
|
|
274
|
-
|
|
285
|
+
getOrInitGlobalValue(lastInboundTimeKey, () => new Map<string, number>())
|
|
275
286
|
|
|
276
287
|
/** Per-connector lock to prevent concurrent start/stop operations */
|
|
277
288
|
const lockKey = '__swarmclaw_connector_locks__' as const
|
|
278
289
|
const locks: Map<string, Promise<void>> =
|
|
279
|
-
|
|
290
|
+
getOrInitGlobalValue(lockKey, () => new Map<string, Promise<void>>())
|
|
280
291
|
|
|
281
292
|
/** Generation counter per connector — used to detect stale lifecycle events after restart */
|
|
282
293
|
const genCounterKey = '__swarmclaw_connector_gen__' as const
|
|
283
294
|
const generationCounter: Map<string, number> =
|
|
284
|
-
|
|
295
|
+
getOrInitGlobalValue(genCounterKey, () => new Map<string, number>())
|
|
285
296
|
|
|
286
297
|
type ScheduledConnectorFollowup = {
|
|
287
298
|
id: string
|
|
@@ -294,11 +305,11 @@ type ScheduledConnectorFollowup = {
|
|
|
294
305
|
|
|
295
306
|
const followupKey = '__swarmclaw_connector_followups__' as const
|
|
296
307
|
const scheduledFollowups: Map<string, ScheduledConnectorFollowup> =
|
|
297
|
-
|
|
308
|
+
getOrInitGlobalValue(followupKey, () => new Map<string, ScheduledConnectorFollowup>())
|
|
298
309
|
|
|
299
310
|
const inboundDedupeKey = '__swarmclaw_connector_inbound_dedupe__' as const
|
|
300
311
|
const recentInboundByKey: Map<string, number> =
|
|
301
|
-
|
|
312
|
+
getOrInitGlobalValue(inboundDedupeKey, () => new Map<string, number>())
|
|
302
313
|
|
|
303
314
|
type DebouncedInboundEntry = {
|
|
304
315
|
connector: Connector
|
|
@@ -308,11 +319,11 @@ type DebouncedInboundEntry = {
|
|
|
308
319
|
|
|
309
320
|
const inboundDebounceKey = '__swarmclaw_connector_inbound_debounce__' as const
|
|
310
321
|
const pendingInboundDebounce: Map<string, DebouncedInboundEntry> =
|
|
311
|
-
|
|
322
|
+
getOrInitGlobalValue(inboundDebounceKey, () => new Map<string, DebouncedInboundEntry>())
|
|
312
323
|
|
|
313
324
|
const followupDedupeKey = '__swarmclaw_connector_followup_dedupe__' as const
|
|
314
325
|
const scheduledFollowupByDedupe: Map<string, { id: string; sendAt: number }> =
|
|
315
|
-
|
|
326
|
+
getOrInitGlobalValue(followupDedupeKey, () => new Map<string, { id: string; sendAt: number }>())
|
|
316
327
|
|
|
317
328
|
/** Reconnect state per connector — tracks backoff and retry attempts for crash recovery */
|
|
318
329
|
export interface ConnectorReconnectState {
|
|
@@ -325,7 +336,7 @@ export interface ConnectorReconnectState {
|
|
|
325
336
|
|
|
326
337
|
const reconnectStateKey = '__swarmclaw_connector_reconnect_state__' as const
|
|
327
338
|
const reconnectState: Map<string, ConnectorReconnectState> =
|
|
328
|
-
|
|
339
|
+
getOrInitGlobalValue(reconnectStateKey, () => new Map<string, ConnectorReconnectState>())
|
|
329
340
|
|
|
330
341
|
const RECONNECT_INITIAL_BACKOFF_MS = 1_000
|
|
331
342
|
const RECONNECT_MAX_BACKOFF_MS = 5 * 60 * 1_000
|
|
@@ -371,11 +382,10 @@ function rememberRecentInbound(key: string, now = Date.now(), ttlMs = 120_000):
|
|
|
371
382
|
return true
|
|
372
383
|
}
|
|
373
384
|
|
|
374
|
-
|
|
375
|
-
function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): Record<string, any> | null {
|
|
385
|
+
function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): ConnectorSession | null {
|
|
376
386
|
if (connector.chatroomId) return null
|
|
377
387
|
const effectiveAgentId = msg.agentIdOverride || connector.agentId
|
|
378
|
-
const sessions = Object.values(loadSessions()
|
|
388
|
+
const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
|
|
379
389
|
const candidates = sessions.filter((session) =>
|
|
380
390
|
session?.agentId === effectiveAgentId
|
|
381
391
|
&& session?.connectorContext?.connectorId === connector.id
|
|
@@ -431,7 +441,7 @@ function startConnectorTypingLoop(connector: Connector, msg: InboundMessage): ((
|
|
|
431
441
|
type RouteMessageHandler = (connector: Connector, msg: InboundMessage) => Promise<string>
|
|
432
442
|
const routeHandlerKey = '__swarmclaw_connector_route_handler__' as const
|
|
433
443
|
const routeMessageHandlerRef: { current: RouteMessageHandler } =
|
|
434
|
-
|
|
444
|
+
getOrInitGlobalValue(routeHandlerKey, () => ({ current: async () => '[Error] Connector router unavailable.' }))
|
|
435
445
|
|
|
436
446
|
async function flushDebouncedInbound(key: string): Promise<void> {
|
|
437
447
|
const entry = pendingInboundDebounce.get(key)
|
|
@@ -625,15 +635,13 @@ function parseConnectorCommand(text: string): ParsedConnectorCommand | null {
|
|
|
625
635
|
}
|
|
626
636
|
}
|
|
627
637
|
|
|
628
|
-
|
|
629
|
-
function persistSessionRecord(session: Record<string, any>): void {
|
|
638
|
+
function persistSessionRecord(session: ConnectorSession): void {
|
|
630
639
|
const sessions = loadSessions()
|
|
631
640
|
sessions[session.id] = session
|
|
632
641
|
saveSessions(sessions)
|
|
633
642
|
}
|
|
634
643
|
|
|
635
|
-
|
|
636
|
-
function updateSessionConnectorContext(session: Record<string, any>, connector: Connector, msg: InboundMessage, sessionKey: string): void {
|
|
644
|
+
function updateSessionConnectorContext(session: ConnectorSession, connector: Connector, msg: InboundMessage, sessionKey: string): void {
|
|
637
645
|
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
638
646
|
session.connectorContext = {
|
|
639
647
|
...(session.connectorContext || {}),
|
|
@@ -665,7 +673,7 @@ function updateSessionConnectorContext(session: Record<string, any>, connector:
|
|
|
665
673
|
}
|
|
666
674
|
}
|
|
667
675
|
|
|
668
|
-
function describeSessionControls(session:
|
|
676
|
+
function describeSessionControls(session: ConnectorSession, connector: Connector, msg: InboundMessage): string {
|
|
669
677
|
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
670
678
|
const context = session.connectorContext || {}
|
|
671
679
|
const sessionAgeSec = Math.max(0, Math.round((Date.now() - (session.createdAt || Date.now())) / 1000))
|
|
@@ -699,7 +707,7 @@ function normalizeSessionSettingKey(raw: string): string {
|
|
|
699
707
|
return raw.trim().toLowerCase().replace(/[_-]+/g, '')
|
|
700
708
|
}
|
|
701
709
|
|
|
702
|
-
function applySessionSetting(session:
|
|
710
|
+
function applySessionSetting(session: ConnectorSession, keyRaw: string, valueRaw: string, msg: InboundMessage): string {
|
|
703
711
|
const key = normalizeSessionSettingKey(keyRaw)
|
|
704
712
|
const value = valueRaw.trim()
|
|
705
713
|
const asInt = () => {
|
|
@@ -709,32 +717,38 @@ function applySessionSetting(session: Record<string, any>, keyRaw: string, value
|
|
|
709
717
|
}
|
|
710
718
|
return parsed
|
|
711
719
|
}
|
|
720
|
+
const asEnum = <T extends string>(allowed: readonly T[], label: string): T | null => {
|
|
721
|
+
if (!value) return null
|
|
722
|
+
const normalized = value.toLowerCase()
|
|
723
|
+
if ((allowed as readonly string[]).includes(normalized)) return normalized as T
|
|
724
|
+
throw new Error(`Invalid ${label}. Use one of: ${allowed.join(', ')}.`)
|
|
725
|
+
}
|
|
712
726
|
|
|
713
727
|
switch (key) {
|
|
714
728
|
case 'think':
|
|
715
729
|
case 'thinkinglevel':
|
|
716
|
-
session.connectorThinkLevel =
|
|
730
|
+
session.connectorThinkLevel = asEnum(['minimal', 'low', 'medium', 'high'] as const, '/think level')
|
|
717
731
|
return `Connector thinking level set to ${session.connectorThinkLevel || 'inherit'}.`
|
|
718
732
|
case 'reply':
|
|
719
733
|
case 'replymode':
|
|
720
|
-
session.connectorReplyMode =
|
|
734
|
+
session.connectorReplyMode = asEnum(['off', 'first', 'all'] as const, 'reply mode')
|
|
721
735
|
return `Reply mode set to ${session.connectorReplyMode || 'inherit'}.`
|
|
722
736
|
case 'scope':
|
|
723
737
|
case 'sessionscope':
|
|
724
|
-
session.connectorSessionScope =
|
|
738
|
+
session.connectorSessionScope = asEnum(['main', 'channel', 'peer', 'channel-peer', 'thread'] as const, 'session scope')
|
|
725
739
|
return `Session scope set to ${session.connectorSessionScope || 'inherit'}.`
|
|
726
740
|
case 'thread':
|
|
727
741
|
case 'threadbinding':
|
|
728
|
-
session.connectorThreadBinding =
|
|
742
|
+
session.connectorThreadBinding = asEnum(['off', 'prefer', 'strict'] as const, 'thread binding')
|
|
729
743
|
if (!value) {
|
|
730
744
|
session.connectorContext = { ...(session.connectorContext || {}), threadId: null }
|
|
731
|
-
} else if (
|
|
745
|
+
} else if (session.connectorThreadBinding === 'strict' && msg.threadId) {
|
|
732
746
|
session.connectorContext = { ...(session.connectorContext || {}), threadId: msg.threadId }
|
|
733
747
|
}
|
|
734
748
|
return `Thread binding set to ${session.connectorThreadBinding || 'inherit'}.`
|
|
735
749
|
case 'group':
|
|
736
750
|
case 'grouppolicy':
|
|
737
|
-
session.connectorGroupPolicy =
|
|
751
|
+
session.connectorGroupPolicy = asEnum(['open', 'mention', 'reply-or-mention', 'disabled'] as const, 'group policy')
|
|
738
752
|
return `Group policy set to ${session.connectorGroupPolicy || 'inherit'}.`
|
|
739
753
|
case 'idle':
|
|
740
754
|
case 'idletimeout':
|
|
@@ -775,10 +789,15 @@ function applySessionSetting(session: Record<string, any>, keyRaw: string, value
|
|
|
775
789
|
case 'model':
|
|
776
790
|
session.model = value
|
|
777
791
|
return `Model set to ${session.model}.`
|
|
778
|
-
case 'provider':
|
|
779
|
-
|
|
780
|
-
|
|
792
|
+
case 'provider': {
|
|
793
|
+
const provider = getProvider(value)
|
|
794
|
+
if (!provider) {
|
|
795
|
+
throw new Error(`Unknown provider "${value}".`)
|
|
796
|
+
}
|
|
797
|
+
session.provider = provider.id as Session['provider']
|
|
798
|
+
session.apiEndpoint = provider.defaultEndpoint || session.apiEndpoint || null
|
|
781
799
|
return `Provider set to ${session.provider}.`
|
|
800
|
+
}
|
|
782
801
|
default:
|
|
783
802
|
throw new Error(`Unknown session setting "${keyRaw}".`)
|
|
784
803
|
}
|
|
@@ -787,7 +806,7 @@ function applySessionSetting(session: Record<string, any>, keyRaw: string, value
|
|
|
787
806
|
function evaluateGroupPolicy(params: {
|
|
788
807
|
connector: Connector
|
|
789
808
|
msg: InboundMessage
|
|
790
|
-
session?:
|
|
809
|
+
session?: ConnectorSession | null
|
|
791
810
|
aliases: string[]
|
|
792
811
|
}): { allowed: boolean; reason: string } {
|
|
793
812
|
const { connector, msg, session, aliases } = params
|
|
@@ -804,11 +823,11 @@ function evaluateGroupPolicy(params: {
|
|
|
804
823
|
return { allowed, reason: allowed ? (mentioned ? 'mentioned' : 'reply') : 'reply_or_mention_required' }
|
|
805
824
|
}
|
|
806
825
|
|
|
807
|
-
function applyConnectorRuntimeDefaults(session:
|
|
808
|
-
provider:
|
|
826
|
+
function applyConnectorRuntimeDefaults(session: ConnectorSession, defaults: {
|
|
827
|
+
provider: Session['provider']
|
|
809
828
|
model: string
|
|
810
829
|
apiEndpoint: string | null
|
|
811
|
-
thinkingLevel:
|
|
830
|
+
thinkingLevel: Session['connectorThinkLevel']
|
|
812
831
|
}): void {
|
|
813
832
|
session.provider = defaults.provider
|
|
814
833
|
session.model = defaults.model
|
|
@@ -819,12 +838,12 @@ function applyConnectorRuntimeDefaults(session: Record<string, any>, defaults: {
|
|
|
819
838
|
function resolveDirectSession(params: {
|
|
820
839
|
connector: Connector
|
|
821
840
|
msg: InboundMessage
|
|
822
|
-
agent:
|
|
823
|
-
}): { session:
|
|
841
|
+
agent: ConnectorAgent
|
|
842
|
+
}): { session: ConnectorSession; sessionKey: string; wasCreated: boolean; staleReason?: string | null; clearedMessages?: number } {
|
|
824
843
|
const { connector, msg, agent } = params
|
|
825
844
|
const policySeed = resolveConnectorSessionPolicy(connector, msg)
|
|
826
845
|
const providerInfo = policySeed.providerOverride ? getProvider(policySeed.providerOverride) : null
|
|
827
|
-
const defaultProvider =
|
|
846
|
+
const defaultProvider: Session['provider'] = providerInfo?.id || (agent.provider === 'claude-cli' ? 'anthropic' : agent.provider)
|
|
828
847
|
const defaultModel = policySeed.modelOverride || agent.model
|
|
829
848
|
const defaultApiEndpoint = agent.apiEndpoint || providerInfo?.defaultEndpoint || null
|
|
830
849
|
const runtimeDefaults = {
|
|
@@ -840,7 +859,7 @@ function resolveDirectSession(params: {
|
|
|
840
859
|
policy: policySeed,
|
|
841
860
|
})
|
|
842
861
|
const sessions = loadSessions()
|
|
843
|
-
let session = Object.values(sessions).find((item
|
|
862
|
+
let session = Object.values(sessions as Record<string, ConnectorSession>).find((item) => item?.name === sessionKey)
|
|
844
863
|
let wasCreated = false
|
|
845
864
|
if (!session) {
|
|
846
865
|
const id = genId()
|
|
@@ -892,8 +911,8 @@ function resolveDirectSession(params: {
|
|
|
892
911
|
const staleness = getConnectorSessionStaleness(session, policy)
|
|
893
912
|
let clearedMessages = 0
|
|
894
913
|
if (staleness.stale) {
|
|
895
|
-
try { syncSessionArchiveMemory(session
|
|
896
|
-
clearedMessages = resetConnectorSessionRuntime(session
|
|
914
|
+
try { syncSessionArchiveMemory(session, { agent }) } catch { /* archive sync is best-effort */ }
|
|
915
|
+
clearedMessages = resetConnectorSessionRuntime(session, staleness.reason || 'session_refresh')
|
|
897
916
|
applyConnectorRuntimeDefaults(session, {
|
|
898
917
|
...runtimeDefaults,
|
|
899
918
|
thinkingLevel: policySeed.thinkingLevel || session.connectorThinkLevel || null,
|
|
@@ -911,16 +930,14 @@ function resolveDirectSession(params: {
|
|
|
911
930
|
}
|
|
912
931
|
}
|
|
913
932
|
|
|
914
|
-
|
|
915
|
-
function pushSessionMessage(session: Record<string, any>, role: 'user' | 'assistant', text: string): void {
|
|
933
|
+
function pushSessionMessage(session: ConnectorSession, role: 'user' | 'assistant', text: string): void {
|
|
916
934
|
if (!text.trim()) return
|
|
917
935
|
if (!Array.isArray(session.messages)) session.messages = []
|
|
918
936
|
session.messages.push({ role, text: text.trim(), time: Date.now() })
|
|
919
937
|
session.lastActiveAt = Date.now()
|
|
920
938
|
}
|
|
921
939
|
|
|
922
|
-
|
|
923
|
-
function persistSession(session: Record<string, any>): void {
|
|
940
|
+
function persistSession(session: ConnectorSession): void {
|
|
924
941
|
const sessions = loadSessions()
|
|
925
942
|
sessions[session.id] = session
|
|
926
943
|
saveSessions(sessions)
|
|
@@ -1074,8 +1091,7 @@ function enforceInboundAccessPolicy(connector: Connector, msg: InboundMessage):
|
|
|
1074
1091
|
async function handleConnectorCommand(params: {
|
|
1075
1092
|
command: ParsedConnectorCommand
|
|
1076
1093
|
connector: Connector
|
|
1077
|
-
|
|
1078
|
-
session: Record<string, any>
|
|
1094
|
+
session: ConnectorSession
|
|
1079
1095
|
msg: InboundMessage
|
|
1080
1096
|
agentName: string
|
|
1081
1097
|
}): Promise<string> {
|
|
@@ -1128,12 +1144,13 @@ async function handleConnectorCommand(params: {
|
|
|
1128
1144
|
}
|
|
1129
1145
|
|
|
1130
1146
|
if (command.name === 'new' || command.name === 'reset') {
|
|
1131
|
-
|
|
1132
|
-
|
|
1147
|
+
const agent = session.agentId ? (loadAgents() as Record<string, ConnectorAgent>)[session.agentId] : undefined
|
|
1148
|
+
try { syncSessionArchiveMemory(session, { agent }) } catch { /* best effort */ }
|
|
1149
|
+
const cleared = resetConnectorSessionRuntime(session, 'manual_reset')
|
|
1133
1150
|
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
1134
1151
|
const providerInfo = policy.providerOverride ? getProvider(policy.providerOverride) : null
|
|
1135
1152
|
applyConnectorRuntimeDefaults(session, {
|
|
1136
|
-
provider:
|
|
1153
|
+
provider: providerInfo?.id || session.provider,
|
|
1137
1154
|
model: policy.modelOverride || session.model,
|
|
1138
1155
|
apiEndpoint: providerInfo?.defaultEndpoint || session.apiEndpoint || null,
|
|
1139
1156
|
thinkingLevel: policy.thinkingLevel || session.connectorThinkLevel || null,
|
|
@@ -1173,7 +1190,7 @@ async function handleConnectorCommand(params: {
|
|
|
1173
1190
|
|
|
1174
1191
|
if (command.name === 'think') {
|
|
1175
1192
|
const requested = command.args.trim().toLowerCase()
|
|
1176
|
-
const allowed = new Set(['minimal', 'low', 'medium', 'high'])
|
|
1193
|
+
const allowed = new Set(['minimal', 'low', 'medium', 'high'] as const)
|
|
1177
1194
|
if (!requested) {
|
|
1178
1195
|
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
1179
1196
|
const current = typeof policy.thinkingLevel === 'string' && allowed.has(policy.thinkingLevel)
|
|
@@ -1185,7 +1202,12 @@ async function handleConnectorCommand(params: {
|
|
|
1185
1202
|
persistSession(session)
|
|
1186
1203
|
return text
|
|
1187
1204
|
}
|
|
1188
|
-
if (
|
|
1205
|
+
if (
|
|
1206
|
+
requested !== 'minimal'
|
|
1207
|
+
&& requested !== 'low'
|
|
1208
|
+
&& requested !== 'medium'
|
|
1209
|
+
&& requested !== 'high'
|
|
1210
|
+
) {
|
|
1189
1211
|
const text = 'Invalid /think level. Use one of: minimal, low, medium, high.'
|
|
1190
1212
|
pushSessionMessage(session, 'user', inboundText)
|
|
1191
1213
|
pushSessionMessage(session, 'assistant', text)
|
|
@@ -1222,12 +1244,13 @@ async function handleConnectorCommand(params: {
|
|
|
1222
1244
|
return text
|
|
1223
1245
|
}
|
|
1224
1246
|
if (parts[0].toLowerCase() === 'reset') {
|
|
1225
|
-
|
|
1226
|
-
|
|
1247
|
+
const agent = session.agentId ? (loadAgents() as Record<string, ConnectorAgent>)[session.agentId] : undefined
|
|
1248
|
+
try { syncSessionArchiveMemory(session, { agent }) } catch { /* best effort */ }
|
|
1249
|
+
const cleared = resetConnectorSessionRuntime(session, 'manual_reset')
|
|
1227
1250
|
const policy = resolveConnectorSessionPolicy(connector, msg, session)
|
|
1228
1251
|
const providerInfo = policy.providerOverride ? getProvider(policy.providerOverride) : null
|
|
1229
1252
|
applyConnectorRuntimeDefaults(session, {
|
|
1230
|
-
provider:
|
|
1253
|
+
provider: providerInfo?.id || session.provider,
|
|
1231
1254
|
model: policy.modelOverride || session.model,
|
|
1232
1255
|
apiEndpoint: providerInfo?.defaultEndpoint || session.apiEndpoint || null,
|
|
1233
1256
|
thinkingLevel: policy.thinkingLevel || session.connectorThinkLevel || null,
|
|
@@ -1402,7 +1425,7 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
|
|
|
1402
1425
|
continue
|
|
1403
1426
|
}
|
|
1404
1427
|
|
|
1405
|
-
const syntheticSession =
|
|
1428
|
+
const syntheticSession = ensureSyntheticSession(agent, chatroomId)
|
|
1406
1429
|
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
1407
1430
|
const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
|
|
1408
1431
|
const fullSystemPrompt = [agentSystemPrompt, chatroomContext, threadContextBlock].filter(Boolean).join('\n\n')
|
|
@@ -2255,12 +2278,17 @@ export async function recordConnectorOutboundDelivery(params: {
|
|
|
2255
2278
|
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
2256
2279
|
const entry = history[i]
|
|
2257
2280
|
if (entry?.role !== 'assistant') continue
|
|
2258
|
-
const source = entry?.source || {}
|
|
2281
|
+
const source: Partial<MessageSource> = entry?.source || {}
|
|
2259
2282
|
if (source.connectorId !== connector.id) continue
|
|
2260
2283
|
if (source.channelId !== params.inbound.channelId) continue
|
|
2261
2284
|
if (!source.messageId && params.messageId) {
|
|
2262
2285
|
entry.source = {
|
|
2263
|
-
|
|
2286
|
+
platform: source.platform || connector.platform,
|
|
2287
|
+
connectorId: source.connectorId || connector.id,
|
|
2288
|
+
connectorName: source.connectorName || connector.name,
|
|
2289
|
+
channelId: source.channelId || params.inbound.channelId,
|
|
2290
|
+
senderId: source.senderId,
|
|
2291
|
+
senderName: source.senderName,
|
|
2264
2292
|
messageId: params.messageId,
|
|
2265
2293
|
replyToMessageId: source.replyToMessageId || params.inbound.messageId,
|
|
2266
2294
|
threadId: source.threadId || params.inbound.threadId,
|
|
@@ -2456,7 +2484,7 @@ export async function sendConnectorMessage(params: {
|
|
|
2456
2484
|
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
2457
2485
|
const entry = history[i]
|
|
2458
2486
|
if (entry?.role !== 'assistant') continue
|
|
2459
|
-
const source = entry?.source || {}
|
|
2487
|
+
const source: Partial<MessageSource> = entry?.source || {}
|
|
2460
2488
|
if (source.connectorId !== connectorId) continue
|
|
2461
2489
|
if (source.channelId !== channelId) continue
|
|
2462
2490
|
if (!source.messageId && result?.messageId) {
|
|
@@ -284,7 +284,7 @@ const slack: PlatformConnector = {
|
|
|
284
284
|
})
|
|
285
285
|
|
|
286
286
|
// Handle @mentions
|
|
287
|
-
app.event('app_mention', async ({ event,
|
|
287
|
+
app.event('app_mention', async ({ event, client }) => {
|
|
288
288
|
if (allowedChannels && !allowedChannels.includes(event.channel)) return
|
|
289
289
|
|
|
290
290
|
let senderName = event.user || 'unknown'
|
|
@@ -16,6 +16,7 @@ import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus
|
|
|
16
16
|
import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
|
|
17
17
|
import { enqueueSessionRun } from './session-run-manager'
|
|
18
18
|
import { WORKSPACE_DIR } from './data-dir'
|
|
19
|
+
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
|
|
19
20
|
import { genId } from '@/lib/id'
|
|
20
21
|
import path from 'node:path'
|
|
21
22
|
import type { WebhookRetryEntry } from '@/types'
|
|
@@ -57,7 +58,7 @@ function daemonAutostartEnvEnabled(): boolean {
|
|
|
57
58
|
return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true)
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function parseHeartbeatIntervalSec(value: unknown, fallback =
|
|
61
|
+
function parseHeartbeatIntervalSec(value: unknown, fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC): number {
|
|
61
62
|
const parsed = typeof value === 'number'
|
|
62
63
|
? value
|
|
63
64
|
: typeof value === 'string'
|
|
@@ -735,7 +736,7 @@ async function runHealthChecks() {
|
|
|
735
736
|
|
|
736
737
|
const sessionId = session.id
|
|
737
738
|
const sessionLabel = String(session.name || sessionId)
|
|
738
|
-
const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec,
|
|
739
|
+
const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, DEFAULT_HEARTBEAT_INTERVAL_SEC)
|
|
739
740
|
if (intervalSec <= 0) continue
|
|
740
741
|
const staleAfter = Math.max(intervalSec * STALE_MULTIPLIER * 1000, STALE_MIN_MS)
|
|
741
742
|
const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { AGENT_REGRESSION_SCENARIOS, resolveRegressionApprovalSettings, scoreAssertions } from './agent-regression'
|
|
4
|
+
|
|
5
|
+
describe('agent regression helpers', () => {
|
|
6
|
+
it('maps approval modes onto deterministic platform settings', () => {
|
|
7
|
+
assert.deepEqual(resolveRegressionApprovalSettings('manual'), {
|
|
8
|
+
approvalsEnabled: true,
|
|
9
|
+
approvalAutoApproveCategories: [],
|
|
10
|
+
})
|
|
11
|
+
assert.deepEqual(resolveRegressionApprovalSettings('auto'), {
|
|
12
|
+
approvalsEnabled: true,
|
|
13
|
+
approvalAutoApproveCategories: ['tool_access'],
|
|
14
|
+
})
|
|
15
|
+
assert.deepEqual(resolveRegressionApprovalSettings('off'), {
|
|
16
|
+
approvalsEnabled: false,
|
|
17
|
+
approvalAutoApproveCategories: [],
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('scores scenarios from assertion weights instead of prose', () => {
|
|
22
|
+
const scored = scoreAssertions([
|
|
23
|
+
{ name: 'artifact exists', passed: true, weight: 2 },
|
|
24
|
+
{ name: 'exact token preserved', passed: false, weight: 3 },
|
|
25
|
+
{ name: 'delegate used', passed: true },
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(scored, {
|
|
29
|
+
score: 3,
|
|
30
|
+
maxScore: 6,
|
|
31
|
+
status: 'failed',
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('includes the extended signup, secrets, email, and human-verification scenarios', () => {
|
|
36
|
+
const ids = AGENT_REGRESSION_SCENARIOS.map((scenario) => scenario.id)
|
|
37
|
+
assert.deepEqual(ids, [
|
|
38
|
+
'approval-resume',
|
|
39
|
+
'delegate-literal-artifact',
|
|
40
|
+
'schedule-script',
|
|
41
|
+
'open-ended-iteration',
|
|
42
|
+
'mock-signup-secret-email',
|
|
43
|
+
'human-verified-signup',
|
|
44
|
+
'research-build-deploy',
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
})
|