@swarmclawai/swarmclaw 0.6.4 → 0.6.7
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 +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { AgentWallet, WalletChain } from '@/types'
|
|
6
|
+
|
|
7
|
+
interface WalletSectionProps {
|
|
8
|
+
agentId: string
|
|
9
|
+
wallet: (Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }) | null
|
|
10
|
+
onWalletCreated: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function WalletSection({ agentId, wallet, onWalletCreated }: WalletSectionProps) {
|
|
14
|
+
const [creating, setCreating] = useState(false)
|
|
15
|
+
const [error, setError] = useState<string | null>(null)
|
|
16
|
+
const [copied, setCopied] = useState(false)
|
|
17
|
+
|
|
18
|
+
const createWallet = useCallback(async () => {
|
|
19
|
+
setCreating(true)
|
|
20
|
+
setError(null)
|
|
21
|
+
try {
|
|
22
|
+
await api('POST', '/wallets', { agentId, chain: 'solana' as WalletChain })
|
|
23
|
+
onWalletCreated()
|
|
24
|
+
} catch (err: unknown) {
|
|
25
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
26
|
+
} finally {
|
|
27
|
+
setCreating(false)
|
|
28
|
+
}
|
|
29
|
+
}, [agentId, onWalletCreated])
|
|
30
|
+
|
|
31
|
+
const copyAddress = useCallback(() => {
|
|
32
|
+
if (!wallet) return
|
|
33
|
+
navigator.clipboard.writeText(wallet.publicKey)
|
|
34
|
+
setCopied(true)
|
|
35
|
+
setTimeout(() => setCopied(false), 2000)
|
|
36
|
+
}, [wallet])
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="mb-8">
|
|
40
|
+
<div className="flex items-center gap-2 mb-3">
|
|
41
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
42
|
+
Wallet
|
|
43
|
+
</label>
|
|
44
|
+
<span className="px-1.5 py-0.5 rounded-[4px] bg-amber-500/15 text-amber-400 text-[9px] font-600 uppercase tracking-wide">
|
|
45
|
+
Experimental
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{!wallet ? (
|
|
50
|
+
<div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50">
|
|
51
|
+
<p className="text-[12px] text-text-3/70 mb-3">
|
|
52
|
+
Create a Solana wallet for this agent to hold funds, pay for services, and trade autonomously.
|
|
53
|
+
</p>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={createWallet}
|
|
57
|
+
disabled={creating}
|
|
58
|
+
className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[11px] font-600 hover:bg-accent-bright/15 transition-all cursor-pointer disabled:opacity-50 border border-accent-bright/20"
|
|
59
|
+
style={{ fontFamily: 'inherit' }}
|
|
60
|
+
>
|
|
61
|
+
{creating ? 'Creating...' : 'Create Wallet'}
|
|
62
|
+
</button>
|
|
63
|
+
{error && <p className="text-[11px] text-red-400 mt-2">{error}</p>}
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className="p-4 rounded-[12px] border border-white/[0.06] bg-surface-2/50 space-y-3">
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<span className="text-[10px] text-text-3/60 uppercase tracking-wide font-600">
|
|
69
|
+
{wallet.chain}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="flex-1" />
|
|
72
|
+
{typeof wallet.balanceSol === 'number' && (
|
|
73
|
+
<span className="text-[13px] font-600 text-text-1">
|
|
74
|
+
{wallet.balanceSol.toFixed(4)} SOL
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-center gap-2">
|
|
79
|
+
<code className="text-[11px] text-text-3 bg-black/20 px-2 py-1 rounded-[6px] font-mono truncate flex-1">
|
|
80
|
+
{wallet.publicKey}
|
|
81
|
+
</code>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={copyAddress}
|
|
85
|
+
className="shrink-0 px-2 py-1 rounded-[6px] text-[10px] text-text-3 hover:text-text-2 border border-white/[0.08] bg-surface transition-colors cursor-pointer"
|
|
86
|
+
style={{ fontFamily: 'inherit' }}
|
|
87
|
+
>
|
|
88
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex items-center gap-3 text-[10px] text-text-3/60">
|
|
92
|
+
<span>Limit: {((wallet.spendingLimitLamports ?? 100_000_000) / 1e9).toFixed(2)} SOL/tx</span>
|
|
93
|
+
<span>Daily: {((wallet.dailyLimitLamports ?? 1_000_000_000) / 1e9).toFixed(1)} SOL</span>
|
|
94
|
+
<span>{wallet.requireApproval ? 'Approval required' : 'Auto-send'}</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
interface UseSwipeOptions {
|
|
4
|
+
onSwipe: (direction: 'left' | 'right') => void
|
|
5
|
+
/** Only trigger right-swipe from this many pixels from the left edge */
|
|
6
|
+
edgeWidth?: number
|
|
7
|
+
/** Minimum horizontal distance to count as a swipe */
|
|
8
|
+
threshold?: number
|
|
9
|
+
/** Whether left-swipe is currently allowed (e.g. sidebar is open) */
|
|
10
|
+
leftSwipeEnabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSwipe({
|
|
14
|
+
onSwipe,
|
|
15
|
+
edgeWidth = 40,
|
|
16
|
+
threshold = 50,
|
|
17
|
+
leftSwipeEnabled = false,
|
|
18
|
+
}: UseSwipeOptions) {
|
|
19
|
+
const startX = useRef(0)
|
|
20
|
+
const startY = useRef(0)
|
|
21
|
+
const isEdge = useRef(false)
|
|
22
|
+
|
|
23
|
+
const onTouchStart = useCallback((e: React.TouchEvent) => {
|
|
24
|
+
const touch = e.touches[0]
|
|
25
|
+
startX.current = touch.clientX
|
|
26
|
+
startY.current = touch.clientY
|
|
27
|
+
isEdge.current = touch.clientX <= edgeWidth
|
|
28
|
+
}, [edgeWidth])
|
|
29
|
+
|
|
30
|
+
// No-op — we only evaluate on touchend, but callers may wire this for consistency
|
|
31
|
+
const onTouchMove = useCallback(() => {}, [])
|
|
32
|
+
|
|
33
|
+
const onTouchEnd = useCallback((e: React.TouchEvent) => {
|
|
34
|
+
const touch = e.changedTouches[0]
|
|
35
|
+
const dx = touch.clientX - startX.current
|
|
36
|
+
const dy = touch.clientY - startY.current
|
|
37
|
+
// Ignore if vertical movement dominates
|
|
38
|
+
if (Math.abs(dy) > Math.abs(dx)) return
|
|
39
|
+
if (Math.abs(dx) < threshold) return
|
|
40
|
+
|
|
41
|
+
if (dx > 0 && isEdge.current) {
|
|
42
|
+
onSwipe('right')
|
|
43
|
+
} else if (dx < 0 && leftSwipeEnabled) {
|
|
44
|
+
onSwipe('left')
|
|
45
|
+
}
|
|
46
|
+
}, [threshold, leftSwipeEnabled, onSwipe])
|
|
47
|
+
|
|
48
|
+
return { onTouchStart, onTouchMove, onTouchEnd }
|
|
49
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import https from 'https'
|
|
3
3
|
import type { StreamChatOptions } from './index'
|
|
4
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
4
5
|
|
|
5
6
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
6
7
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
@@ -23,7 +24,7 @@ function fileToContentBlocks(filePath: string): any[] {
|
|
|
23
24
|
return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
27
|
+
export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
27
28
|
return new Promise((resolve) => {
|
|
28
29
|
const messages = buildMessages(session, message, imagePath, loadHistory, imageUrl)
|
|
29
30
|
const model = session.model || 'claude-sonnet-4-6'
|
|
@@ -43,9 +44,21 @@ export function streamAnthropicChat({ session, message, imagePath, imageUrl, api
|
|
|
43
44
|
const payload = JSON.stringify(body)
|
|
44
45
|
const abortController = { aborted: false }
|
|
45
46
|
let fullResponse = ''
|
|
47
|
+
let apiReqRef: ReturnType<typeof https.request> | null = null
|
|
48
|
+
|
|
49
|
+
if (signal) {
|
|
50
|
+
if (signal.aborted) {
|
|
51
|
+
abortController.aborted = true
|
|
52
|
+
} else {
|
|
53
|
+
signal.addEventListener('abort', () => {
|
|
54
|
+
abortController.aborted = true
|
|
55
|
+
apiReqRef?.destroy()
|
|
56
|
+
}, { once: true })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
46
59
|
|
|
47
60
|
const apiReq = https.request({
|
|
48
|
-
hostname:
|
|
61
|
+
hostname: PROVIDER_DEFAULTS.anthropic,
|
|
49
62
|
path: '/v1/messages',
|
|
50
63
|
method: 'POST',
|
|
51
64
|
headers: {
|
|
@@ -109,6 +122,7 @@ export function streamAnthropicChat({ session, message, imagePath, imageUrl, api
|
|
|
109
122
|
})
|
|
110
123
|
})
|
|
111
124
|
|
|
125
|
+
apiReqRef = apiReq
|
|
112
126
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
113
127
|
|
|
114
128
|
apiReq.on('error', (e) => {
|
|
@@ -24,7 +24,7 @@ function findClaude(): string {
|
|
|
24
24
|
|
|
25
25
|
const CLAUDE = findClaude()
|
|
26
26
|
|
|
27
|
-
export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active }: StreamChatOptions): Promise<string> {
|
|
27
|
+
export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
28
28
|
const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
|
|
29
29
|
let prompt = message
|
|
30
30
|
if (imagePath) {
|
|
@@ -108,6 +108,12 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
|
|
|
108
108
|
proc.stdin!.end()
|
|
109
109
|
|
|
110
110
|
active.set(session.id, proc)
|
|
111
|
+
|
|
112
|
+
if (signal) {
|
|
113
|
+
if (signal.aborted) { proc.kill(); }
|
|
114
|
+
else signal.addEventListener('abort', () => { proc.kill() }, { once: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
let fullResponse = ''
|
|
112
118
|
let buf = ''
|
|
113
119
|
let eventCount = 0
|
|
@@ -29,6 +29,8 @@ export interface StreamChatOptions {
|
|
|
29
29
|
active: Map<string, any>
|
|
30
30
|
loadHistory: (sessionId: string) => any[]
|
|
31
31
|
onUsage?: (usage: StreamChatUsage) => void
|
|
32
|
+
/** Abort signal from the caller — providers should use this to cancel in-flight requests. */
|
|
33
|
+
signal?: AbortSignal
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface BuiltinProviderConfig extends ProviderInfo {
|
|
@@ -351,6 +353,11 @@ export async function streamChatWithFailover(
|
|
|
351
353
|
t: 'md',
|
|
352
354
|
text: JSON.stringify({ failover: { from: credId, reason: err.message?.slice(0, 100) } }),
|
|
353
355
|
})}\n\n`)
|
|
356
|
+
// Exponential backoff for rate-limit / server errors (skip for auth rotation)
|
|
357
|
+
if (statusCode !== 401) {
|
|
358
|
+
const delay = Math.min(500 * Math.pow(2, i), 8000)
|
|
359
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
360
|
+
}
|
|
354
361
|
continue
|
|
355
362
|
}
|
|
356
363
|
throw err
|
|
@@ -2,16 +2,17 @@ import fs from 'fs'
|
|
|
2
2
|
import http from 'http'
|
|
3
3
|
import https from 'https'
|
|
4
4
|
import type { StreamChatOptions } from './index'
|
|
5
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
5
6
|
|
|
6
7
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
7
8
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
8
9
|
|
|
9
|
-
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
10
|
+
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
10
11
|
return new Promise((resolve) => {
|
|
11
12
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
12
13
|
const model = session.model || 'llama3'
|
|
13
14
|
// Cloud: no endpoint but API key present → use Ollama cloud
|
|
14
|
-
const endpoint = session.apiEndpoint || (apiKey ?
|
|
15
|
+
const endpoint = session.apiEndpoint || (apiKey ? PROVIDER_DEFAULTS.ollamaCloud : PROVIDER_DEFAULTS.ollama)
|
|
15
16
|
|
|
16
17
|
const parsed = new URL(endpoint)
|
|
17
18
|
const isHttps = parsed.protocol === 'https:'
|
|
@@ -26,6 +27,18 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
26
27
|
|
|
27
28
|
const abortController = { aborted: false }
|
|
28
29
|
let fullResponse = ''
|
|
30
|
+
let apiReqRef: ReturnType<typeof http.request> | null = null
|
|
31
|
+
|
|
32
|
+
if (signal) {
|
|
33
|
+
if (signal.aborted) {
|
|
34
|
+
abortController.aborted = true
|
|
35
|
+
} else {
|
|
36
|
+
signal.addEventListener('abort', () => {
|
|
37
|
+
abortController.aborted = true
|
|
38
|
+
apiReqRef?.destroy()
|
|
39
|
+
}, { once: true })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
const headers: Record<string, string> = {
|
|
31
44
|
'Content-Type': 'application/json',
|
|
@@ -87,6 +100,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
87
100
|
})
|
|
88
101
|
})
|
|
89
102
|
|
|
103
|
+
apiReqRef = apiReq
|
|
90
104
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
91
105
|
|
|
92
106
|
apiReq.on('error', (e: NodeJS.ErrnoException) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
|
+
import { PROVIDER_DEFAULTS } from './provider-defaults'
|
|
3
4
|
|
|
4
5
|
const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
|
|
5
6
|
const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
|
|
@@ -43,7 +44,7 @@ async function fileToContentParts(filePath: string): Promise<any[]> {
|
|
|
43
44
|
return [{ type: 'text', text: `[Attached file: ${name}]` }]
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
47
|
+
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
47
48
|
return new Promise(async (resolve) => {
|
|
48
49
|
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
|
|
49
50
|
const model = session.model || 'gpt-4o'
|
|
@@ -58,7 +59,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
58
59
|
let fullResponse = ''
|
|
59
60
|
|
|
60
61
|
// Support custom base URLs for custom providers
|
|
61
|
-
const baseUrl = session.apiEndpoint ||
|
|
62
|
+
const baseUrl = session.apiEndpoint || PROVIDER_DEFAULTS.openai
|
|
62
63
|
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
63
64
|
|
|
64
65
|
// OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
|
|
@@ -67,6 +68,10 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
67
68
|
const contentType = session.contentType || 'application/json'
|
|
68
69
|
|
|
69
70
|
const abortController = new AbortController()
|
|
71
|
+
if (signal) {
|
|
72
|
+
if (signal.aborted) abortController.abort()
|
|
73
|
+
else signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
|
74
|
+
}
|
|
70
75
|
active.set(session.id, { kill: () => abortController.abort() })
|
|
71
76
|
|
|
72
77
|
try {
|
|
@@ -279,7 +279,7 @@ async function connectToGateway(
|
|
|
279
279
|
|
|
280
280
|
// --- Provider ---
|
|
281
281
|
|
|
282
|
-
export function streamOpenClawChat({ session, message, imagePath, apiKey, write, active }: StreamChatOptions): Promise<string> {
|
|
282
|
+
export function streamOpenClawChat({ session, message, imagePath, apiKey, write, active, signal }: StreamChatOptions): Promise<string> {
|
|
283
283
|
let prompt = message
|
|
284
284
|
if (imagePath) {
|
|
285
285
|
prompt = `[The user has shared an image at: ${imagePath}]\n\n${message}`
|
|
@@ -316,6 +316,11 @@ export function streamOpenClawChat({ session, message, imagePath, apiKey, write,
|
|
|
316
316
|
|
|
317
317
|
active.set(session.id, { kill: () => { ws.close(); clearTimeout(timeout); finish('Aborted.') } })
|
|
318
318
|
|
|
319
|
+
if (signal) {
|
|
320
|
+
if (signal.aborted) { ws.close(); clearTimeout(timeout); finish('Aborted.'); return }
|
|
321
|
+
signal.addEventListener('abort', () => { ws.close(); clearTimeout(timeout); finish('Aborted.') }, { once: true })
|
|
322
|
+
}
|
|
323
|
+
|
|
319
324
|
const agentReqId = randomUUID()
|
|
320
325
|
ws.send(JSON.stringify({
|
|
321
326
|
type: 'req',
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface ScheduleTemplate {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
description: string
|
|
5
|
+
icon: string
|
|
6
|
+
category: 'monitoring' | 'reporting' | 'maintenance' | 'content'
|
|
7
|
+
defaults: {
|
|
8
|
+
taskPrompt: string
|
|
9
|
+
scheduleType: 'cron' | 'interval'
|
|
10
|
+
cron?: string
|
|
11
|
+
intervalMs?: number
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SCHEDULE_TEMPLATES: ScheduleTemplate[] = [
|
|
16
|
+
{
|
|
17
|
+
id: 'daily-digest',
|
|
18
|
+
name: 'Daily Digest',
|
|
19
|
+
description: 'Summarize activity from the past 24 hours each morning',
|
|
20
|
+
icon: 'Newspaper',
|
|
21
|
+
category: 'reporting',
|
|
22
|
+
defaults: {
|
|
23
|
+
taskPrompt: 'Summarize all notable activity, events, and updates from the past 24 hours. Highlight key metrics, completed tasks, and anything that needs attention.',
|
|
24
|
+
scheduleType: 'cron',
|
|
25
|
+
cron: '0 9 * * *',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'weekly-report',
|
|
30
|
+
name: 'Weekly Report',
|
|
31
|
+
description: 'Generate a weekly metrics and progress report every Monday',
|
|
32
|
+
icon: 'BarChart3',
|
|
33
|
+
category: 'reporting',
|
|
34
|
+
defaults: {
|
|
35
|
+
taskPrompt: 'Generate a comprehensive weekly report covering key metrics, completed tasks, ongoing work, blockers, and recommendations for the coming week.',
|
|
36
|
+
scheduleType: 'cron',
|
|
37
|
+
cron: '0 10 * * 1',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'health-monitor',
|
|
42
|
+
name: 'Health Monitor',
|
|
43
|
+
description: 'Check system health and service status every 5 minutes',
|
|
44
|
+
icon: 'HeartPulse',
|
|
45
|
+
category: 'monitoring',
|
|
46
|
+
defaults: {
|
|
47
|
+
taskPrompt: 'Perform a system health check. Verify all services are running, check resource usage (CPU, memory, disk), and report any anomalies or degraded performance.',
|
|
48
|
+
scheduleType: 'interval',
|
|
49
|
+
intervalMs: 300000,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'content-generation',
|
|
54
|
+
name: 'Content Generation',
|
|
55
|
+
description: 'Generate daily content such as posts, summaries, or briefs',
|
|
56
|
+
icon: 'PenLine',
|
|
57
|
+
category: 'content',
|
|
58
|
+
defaults: {
|
|
59
|
+
taskPrompt: 'Generate fresh content based on current trends and recent activity. Create a well-structured draft ready for review and publishing.',
|
|
60
|
+
scheduleType: 'cron',
|
|
61
|
+
cron: '0 8 * * *',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'data-cleanup',
|
|
66
|
+
name: 'Data Cleanup',
|
|
67
|
+
description: 'Run weekly cleanup of stale data and temporary files',
|
|
68
|
+
icon: 'Trash2',
|
|
69
|
+
category: 'maintenance',
|
|
70
|
+
defaults: {
|
|
71
|
+
taskPrompt: 'Identify and clean up stale data, expired records, orphaned files, and temporary resources. Log what was removed and any issues encountered.',
|
|
72
|
+
scheduleType: 'cron',
|
|
73
|
+
cron: '0 2 * * 0',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'metric-snapshot',
|
|
78
|
+
name: 'Metric Snapshot',
|
|
79
|
+
description: 'Capture an hourly snapshot of key metrics and KPIs',
|
|
80
|
+
icon: 'Activity',
|
|
81
|
+
category: 'monitoring',
|
|
82
|
+
defaults: {
|
|
83
|
+
taskPrompt: 'Capture a snapshot of all key metrics and KPIs. Record current values, compare against previous snapshots, and flag any significant changes or threshold breaches.',
|
|
84
|
+
scheduleType: 'interval',
|
|
85
|
+
intervalMs: 3600000,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'security-audit',
|
|
90
|
+
name: 'Security Audit',
|
|
91
|
+
description: 'Run a daily security scan and vulnerability check',
|
|
92
|
+
icon: 'ShieldCheck',
|
|
93
|
+
category: 'monitoring',
|
|
94
|
+
defaults: {
|
|
95
|
+
taskPrompt: 'Perform a security audit. Check for unusual access patterns, review authentication logs, scan for known vulnerabilities, and report any security concerns.',
|
|
96
|
+
scheduleType: 'cron',
|
|
97
|
+
cron: '0 0 * * *',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'backup-check',
|
|
102
|
+
name: 'Backup Check',
|
|
103
|
+
description: 'Verify backup integrity and completeness daily',
|
|
104
|
+
icon: 'DatabaseBackup',
|
|
105
|
+
category: 'maintenance',
|
|
106
|
+
defaults: {
|
|
107
|
+
taskPrompt: 'Verify that all scheduled backups completed successfully. Check backup integrity, storage usage, and retention policy compliance. Alert on any failures.',
|
|
108
|
+
scheduleType: 'cron',
|
|
109
|
+
cron: '0 3 * * *',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
/** Subset of templates to feature in the empty state */
|
|
115
|
+
export const FEATURED_TEMPLATE_IDS = ['daily-digest', 'health-monitor', 'content-generation'] as const
|
|
@@ -63,8 +63,8 @@ export function buildAgentAwarenessBlock(excludeId: string): string {
|
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
return [
|
|
66
|
-
'##
|
|
66
|
+
'## My Colleagues',
|
|
67
|
+
'These are the other agents I work alongside. I can hand off tasks to any of them if their skills are a better fit:',
|
|
67
68
|
...lines,
|
|
68
|
-
'You can delegate tasks to any agent using the delegate_to_agent tool.',
|
|
69
69
|
].join('\n')
|
|
70
70
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { loadSettings } from './storage'
|
|
2
|
+
import type { AppNotification } from '@/types'
|
|
3
|
+
|
|
4
|
+
/** In-memory rate limiter: dedupKey → last dispatch timestamp */
|
|
5
|
+
const recentDispatches = new Map<string, number>()
|
|
6
|
+
const DEDUP_WINDOW_MS = 60_000
|
|
7
|
+
|
|
8
|
+
export async function dispatchAlert(notification: AppNotification): Promise<void> {
|
|
9
|
+
const settings = loadSettings()
|
|
10
|
+
const url = typeof settings.alertWebhookUrl === 'string' ? settings.alertWebhookUrl.trim() : ''
|
|
11
|
+
if (!url) return
|
|
12
|
+
|
|
13
|
+
const allowedEvents: string[] = Array.isArray(settings.alertWebhookEvents)
|
|
14
|
+
? settings.alertWebhookEvents
|
|
15
|
+
: ['error']
|
|
16
|
+
if (!allowedEvents.includes(notification.type)) return
|
|
17
|
+
|
|
18
|
+
// Rate limit by dedupKey (or notification id as fallback)
|
|
19
|
+
const dedupKey = notification.dedupKey || notification.id
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
const lastSent = recentDispatches.get(dedupKey)
|
|
22
|
+
if (lastSent && now - lastSent < DEDUP_WINDOW_MS) return
|
|
23
|
+
recentDispatches.set(dedupKey, now)
|
|
24
|
+
|
|
25
|
+
// Prune stale entries periodically
|
|
26
|
+
if (recentDispatches.size > 200) {
|
|
27
|
+
for (const [key, ts] of recentDispatches) {
|
|
28
|
+
if (now - ts > DEDUP_WINDOW_MS) recentDispatches.delete(key)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const webhookType = settings.alertWebhookType || 'custom'
|
|
33
|
+
let body: string
|
|
34
|
+
|
|
35
|
+
if (webhookType === 'discord') {
|
|
36
|
+
body = JSON.stringify({
|
|
37
|
+
content: `⚠️ **${notification.title}**${notification.message ? `\n${notification.message}` : ''}`,
|
|
38
|
+
})
|
|
39
|
+
} else if (webhookType === 'slack') {
|
|
40
|
+
body = JSON.stringify({
|
|
41
|
+
text: `⚠️ *${notification.title}*${notification.message ? `\n${notification.message}` : ''}`,
|
|
42
|
+
})
|
|
43
|
+
} else {
|
|
44
|
+
body = JSON.stringify({
|
|
45
|
+
type: notification.type,
|
|
46
|
+
title: notification.title,
|
|
47
|
+
message: notification.message || null,
|
|
48
|
+
entityType: notification.entityType || null,
|
|
49
|
+
entityId: notification.entityId || null,
|
|
50
|
+
timestamp: notification.createdAt,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body,
|
|
59
|
+
signal: AbortSignal.timeout(5000),
|
|
60
|
+
})
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
console.warn('[alert-dispatch] Webhook delivery failed:', err instanceof Error ? err.message : String(err))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
active,
|
|
14
14
|
} from './storage'
|
|
15
15
|
import { getProvider } from '@/lib/providers'
|
|
16
|
-
import { estimateCost } from './cost'
|
|
16
|
+
import { estimateCost, checkBudget } from './cost'
|
|
17
17
|
import { log } from './logger'
|
|
18
18
|
import { logExecution } from './execution-log'
|
|
19
19
|
import { streamAgentChat } from './stream-agent-chat'
|
|
@@ -109,6 +109,21 @@ function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function shouldReplaceRecentAssistantMessage(params: {
|
|
113
|
+
previous: Message | null | undefined
|
|
114
|
+
nextToolEvents: MessageToolEvent[]
|
|
115
|
+
nextKind: Message['kind']
|
|
116
|
+
now: number
|
|
117
|
+
}): boolean {
|
|
118
|
+
const { previous, nextToolEvents, nextKind, now } = params
|
|
119
|
+
if (!previous || previous.role !== 'assistant') return false
|
|
120
|
+
if (nextToolEvents.length === 0) return false
|
|
121
|
+
if (previous.kind && nextKind && previous.kind !== nextKind) return false
|
|
122
|
+
if (typeof previous.time === 'number' && now - previous.time > 45_000) return false
|
|
123
|
+
const prevTools = Array.isArray(previous.toolEvents) ? previous.toolEvents.length : 0
|
|
124
|
+
return prevTools === 0
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
function requestedToolNamesFromMessage(message: string): string[] {
|
|
113
128
|
const lower = message.toLowerCase()
|
|
114
129
|
const candidates = [
|
|
@@ -377,6 +392,11 @@ function buildAgentSystemPrompt(session: any): string | undefined {
|
|
|
377
392
|
|
|
378
393
|
const settings = loadSettings()
|
|
379
394
|
const parts: string[] = []
|
|
395
|
+
// Identity block — agent needs to know who it is
|
|
396
|
+
const identityLines = [`## My Identity`, `My name is ${agent.name}.`]
|
|
397
|
+
if (agent.description) identityLines.push(agent.description)
|
|
398
|
+
identityLines.push('I should always refer to myself by this name. I am not "Assistant" — I have my own name and identity.')
|
|
399
|
+
parts.push(identityLines.join(' '))
|
|
380
400
|
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
381
401
|
parts.push(buildCurrentDateTimePromptContext())
|
|
382
402
|
if (agent.soul) parts.push(agent.soul)
|
|
@@ -562,6 +582,45 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
562
582
|
onEvent?.({ t: 'err', text: `Capability policy blocked tools for this run: ${blockedSummary}` })
|
|
563
583
|
}
|
|
564
584
|
|
|
585
|
+
// --- Agent monthly budget enforcement ---
|
|
586
|
+
if (session.agentId) {
|
|
587
|
+
const agentsMap = loadAgents()
|
|
588
|
+
const agent = agentsMap[session.agentId]
|
|
589
|
+
if (agent?.monthlyBudget && agent.monthlyBudget > 0) {
|
|
590
|
+
const budgetResult = checkBudget(agent)
|
|
591
|
+
if (!budgetResult.ok) {
|
|
592
|
+
const action = agent.budgetAction || 'warn'
|
|
593
|
+
if (action === 'block') {
|
|
594
|
+
const budgetError = budgetResult.message || `Agent budget exceeded: $${budgetResult.spend.toFixed(4)} / $${budgetResult.budget.toFixed(2)}`
|
|
595
|
+
onEvent?.({ t: 'err', text: budgetError })
|
|
596
|
+
|
|
597
|
+
let persisted = false
|
|
598
|
+
if (!internal) {
|
|
599
|
+
session.messages.push({
|
|
600
|
+
role: 'assistant',
|
|
601
|
+
text: budgetError,
|
|
602
|
+
time: Date.now(),
|
|
603
|
+
})
|
|
604
|
+
session.lastActiveAt = Date.now()
|
|
605
|
+
saveSessions(sessions)
|
|
606
|
+
persisted = true
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
runId,
|
|
611
|
+
sessionId,
|
|
612
|
+
text: budgetError,
|
|
613
|
+
persisted,
|
|
614
|
+
toolEvents: [],
|
|
615
|
+
error: budgetError,
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// budgetAction === 'warn': emit a warning but continue
|
|
619
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ budgetWarning: budgetResult.message }) })
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
565
624
|
const dailySpendLimitUsd = parseUsdLimit(appSettings.safetyMaxDailySpendUsd)
|
|
566
625
|
if (dailySpendLimitUsd !== null) {
|
|
567
626
|
const todaySpendUsd = getTodaySpendUsd()
|
|
@@ -711,6 +770,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
711
770
|
active,
|
|
712
771
|
loadHistory: isAutoRunNoHistory ? () => getSessionMessages(sessionId).slice(-6) : (sid: string) => applyContextClearBoundary(getSessionMessages(sid)),
|
|
713
772
|
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
773
|
+
signal: abortController.signal,
|
|
714
774
|
})
|
|
715
775
|
} catch (err: any) {
|
|
716
776
|
errorMessage = err?.message || String(err)
|
|
@@ -1014,14 +1074,26 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1014
1074
|
const persistedText = heartbeatClassification === 'strip'
|
|
1015
1075
|
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1016
1076
|
: textForPersistence
|
|
1017
|
-
|
|
1077
|
+
const nowTs = Date.now()
|
|
1078
|
+
const nextAssistantMessage: Message = {
|
|
1018
1079
|
role: 'assistant',
|
|
1019
1080
|
text: persistedText,
|
|
1020
|
-
time:
|
|
1081
|
+
time: nowTs,
|
|
1021
1082
|
thinking: thinkingText || undefined,
|
|
1022
1083
|
toolEvents: toolEvents.length ? toolEvents : undefined,
|
|
1023
1084
|
kind: persistedKind,
|
|
1024
|
-
}
|
|
1085
|
+
}
|
|
1086
|
+
const previous = current.messages.at(-1)
|
|
1087
|
+
if (shouldReplaceRecentAssistantMessage({
|
|
1088
|
+
previous,
|
|
1089
|
+
nextToolEvents: toolEvents,
|
|
1090
|
+
nextKind: persistedKind,
|
|
1091
|
+
now: nowTs,
|
|
1092
|
+
})) {
|
|
1093
|
+
current.messages[current.messages.length - 1] = nextAssistantMessage
|
|
1094
|
+
} else {
|
|
1095
|
+
current.messages.push(nextAssistantMessage)
|
|
1096
|
+
}
|
|
1025
1097
|
changed = true
|
|
1026
1098
|
|
|
1027
1099
|
// Conversation tone detection
|