@swarmclawai/swarmclaw 0.9.2 → 0.9.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 +12 -10
- package/bundled-skills/google-workspace/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/app/agents/page.tsx +2 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
- package/src/app/api/clawhub/install/route.ts +2 -0
- package/src/app/api/skills/[id]/route.ts +4 -0
- package/src/app/api/skills/route.ts +4 -0
- package/src/app/globals.css +28 -0
- package/src/app/home/page.tsx +11 -0
- package/src/app/settings/page.tsx +12 -5
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/connectors/connector-list.tsx +2 -5
- package/src/components/logs/log-list.tsx +2 -5
- package/src/components/providers/provider-list.tsx +2 -5
- package/src/components/runs/run-list.tsx +2 -6
- package/src/components/schedules/schedule-list.tsx +7 -1
- package/src/components/ui/full-screen-loader.tsx +0 -29
- package/src/components/ui/page-loader.tsx +69 -0
- package/src/lib/runtime/runtime-loop.ts +21 -1
- package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
- package/src/lib/server/agents/agent-thread-session.ts +1 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
- package/src/lib/server/agents/main-agent-loop.ts +259 -0
- package/src/lib/server/agents/orchestrator-lg.ts +12 -8
- package/src/lib/server/agents/orchestrator.ts +11 -7
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
- package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
- package/src/lib/server/chat-execution/chat-execution.ts +116 -29
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
- package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
- package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
- package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
- package/src/lib/server/connectors/contact-boundaries.ts +101 -0
- package/src/lib/server/connectors/manager.test.ts +504 -73
- package/src/lib/server/connectors/manager.ts +41 -10
- package/src/lib/server/connectors/session-consolidation.ts +2 -0
- package/src/lib/server/connectors/session-kind.ts +7 -0
- package/src/lib/server/connectors/session.test.ts +104 -0
- package/src/lib/server/connectors/session.ts +5 -2
- package/src/lib/server/identity-continuity.test.ts +4 -3
- package/src/lib/server/identity-continuity.ts +8 -4
- package/src/lib/server/memory/memory-policy.test.ts +5 -15
- package/src/lib/server/memory/memory-policy.ts +11 -41
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
- package/src/lib/server/runtime/heartbeat-service.ts +5 -1
- package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime/runtime-settings.ts +4 -0
- package/src/lib/server/runtime/session-run-manager.ts +2 -0
- package/src/lib/server/session-reset-policy.test.ts +17 -3
- package/src/lib/server/session-reset-policy.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +11 -10
- package/src/lib/server/session-tools/crud.ts +41 -7
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
- package/src/lib/server/session-tools/memory.ts +209 -48
- package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
- package/src/lib/server/session-tools/skill-runtime.ts +382 -0
- package/src/lib/server/session-tools/skills.ts +575 -0
- package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
- package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
- package/src/lib/server/skills/skill-discovery.ts +4 -0
- package/src/lib/server/skills/skills-normalize.test.ts +28 -0
- package/src/lib/server/skills/skills-normalize.ts +93 -1
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/tasks/task-followups.test.ts +124 -0
- package/src/lib/server/tasks/task-followups.ts +88 -13
- package/src/types/index.ts +30 -2
- package/src/views/settings/section-runtime-loop.tsx +38 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getToolEventsSnapshotKey,
|
|
11
11
|
hasPersistableAssistantPayload,
|
|
12
12
|
parseUsdLimit,
|
|
13
|
+
pruneOldHeartbeatMessages,
|
|
13
14
|
shouldAutoRouteHeartbeatAlerts,
|
|
14
15
|
shouldPersistInboundUserMessage,
|
|
15
16
|
shouldReplaceRecentAssistantMessage,
|
|
@@ -478,3 +479,58 @@ describe('estimateConversationTone', () => {
|
|
|
478
479
|
assert.equal(estimateConversationTone(''), 'neutral')
|
|
479
480
|
})
|
|
480
481
|
})
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// pruneOldHeartbeatMessages
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
describe('pruneOldHeartbeatMessages', () => {
|
|
487
|
+
it('removes old heartbeat messages keeping only the most recent 2', () => {
|
|
488
|
+
const messages: Message[] = [
|
|
489
|
+
{ role: 'user', text: 'hi', time: 1 },
|
|
490
|
+
{ role: 'assistant', text: 'alert 1', time: 2, kind: 'heartbeat' },
|
|
491
|
+
{ role: 'assistant', text: 'alert 2', time: 3, kind: 'heartbeat' },
|
|
492
|
+
{ role: 'assistant', text: 'real reply', time: 4, kind: 'chat' },
|
|
493
|
+
{ role: 'assistant', text: 'alert 3', time: 5, kind: 'heartbeat' },
|
|
494
|
+
{ role: 'assistant', text: 'alert 4', time: 6, kind: 'heartbeat' },
|
|
495
|
+
]
|
|
496
|
+
const removed = pruneOldHeartbeatMessages(messages)
|
|
497
|
+
assert.equal(removed, 2)
|
|
498
|
+
assert.equal(messages.length, 4)
|
|
499
|
+
// Only the last 2 heartbeat messages remain
|
|
500
|
+
const heartbeats = messages.filter((m) => m.kind === 'heartbeat')
|
|
501
|
+
assert.equal(heartbeats.length, 2)
|
|
502
|
+
assert.equal(heartbeats[0].text, 'alert 3')
|
|
503
|
+
assert.equal(heartbeats[1].text, 'alert 4')
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('does not remove anything when count is at or below maxKeep', () => {
|
|
507
|
+
const messages: Message[] = [
|
|
508
|
+
{ role: 'assistant', text: 'alert 1', time: 1, kind: 'heartbeat' },
|
|
509
|
+
{ role: 'user', text: 'hi', time: 2 },
|
|
510
|
+
{ role: 'assistant', text: 'alert 2', time: 3, kind: 'heartbeat' },
|
|
511
|
+
]
|
|
512
|
+
assert.equal(pruneOldHeartbeatMessages(messages), 0)
|
|
513
|
+
assert.equal(messages.length, 3)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('respects custom maxKeep value', () => {
|
|
517
|
+
const messages: Message[] = [
|
|
518
|
+
{ role: 'assistant', text: 'hb1', time: 1, kind: 'heartbeat' },
|
|
519
|
+
{ role: 'assistant', text: 'hb2', time: 2, kind: 'heartbeat' },
|
|
520
|
+
{ role: 'assistant', text: 'hb3', time: 3, kind: 'heartbeat' },
|
|
521
|
+
]
|
|
522
|
+
assert.equal(pruneOldHeartbeatMessages(messages, 1), 2)
|
|
523
|
+
assert.equal(messages.length, 1)
|
|
524
|
+
assert.equal(messages[0].text, 'hb3')
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('does not touch non-heartbeat messages', () => {
|
|
528
|
+
const messages: Message[] = [
|
|
529
|
+
{ role: 'user', text: 'a', time: 1 },
|
|
530
|
+
{ role: 'assistant', text: 'b', time: 2, kind: 'chat' },
|
|
531
|
+
{ role: 'assistant', text: 'c', time: 3 },
|
|
532
|
+
]
|
|
533
|
+
assert.equal(pruneOldHeartbeatMessages(messages), 0)
|
|
534
|
+
assert.equal(messages.length, 3)
|
|
535
|
+
})
|
|
536
|
+
})
|
|
@@ -522,6 +522,30 @@ export function classifyHeartbeatResponse(text: string, ackMaxChars: number, had
|
|
|
522
522
|
return stripped.length < cleaned.length ? 'strip' : 'keep'
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Prune old heartbeat messages from the transcript to prevent context bloat.
|
|
527
|
+
* Keeps only the most recent `maxKeep` heartbeat assistant messages.
|
|
528
|
+
* Returns the number of messages removed.
|
|
529
|
+
*/
|
|
530
|
+
export function pruneOldHeartbeatMessages(messages: Message[], maxKeep = 2): number {
|
|
531
|
+
const heartbeatIndices: number[] = []
|
|
532
|
+
for (let i = 0; i < messages.length; i++) {
|
|
533
|
+
if (messages[i].role === 'assistant' && messages[i].kind === 'heartbeat') {
|
|
534
|
+
heartbeatIndices.push(i)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (heartbeatIndices.length <= maxKeep) return 0
|
|
538
|
+
const toRemove = new Set(heartbeatIndices.slice(0, heartbeatIndices.length - maxKeep))
|
|
539
|
+
let removed = 0
|
|
540
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
541
|
+
if (toRemove.has(i)) {
|
|
542
|
+
messages.splice(i, 1)
|
|
543
|
+
removed++
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return removed
|
|
547
|
+
}
|
|
548
|
+
|
|
525
549
|
export function estimateConversationTone(text: string): string {
|
|
526
550
|
const t = text || ''
|
|
527
551
|
if (/```/.test(t) || /\b(function|const|let|var|import|export|class|interface|async|await|return)\b/.test(t)) return 'technical'
|
|
@@ -29,6 +29,7 @@ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agent
|
|
|
29
29
|
import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
|
|
30
30
|
import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
|
|
31
31
|
import { buildWorkspaceContext } from '@/lib/server/workspace-context'
|
|
32
|
+
import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
|
|
32
33
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
33
34
|
import {
|
|
34
35
|
applyContextClearBoundary,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
getTodaySpendUsd,
|
|
49
50
|
classifyHeartbeatResponse,
|
|
50
51
|
estimateConversationTone,
|
|
52
|
+
pruneOldHeartbeatMessages,
|
|
51
53
|
} from '@/lib/server/chat-execution/chat-execution-utils'
|
|
52
54
|
import { runPostLlmToolRouting } from '@/lib/server/chat-execution/chat-turn-tool-routing'
|
|
53
55
|
import {
|
|
@@ -66,6 +68,7 @@ import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolic
|
|
|
66
68
|
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat/chat-streaming-state'
|
|
67
69
|
import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/agents/assistant-control'
|
|
68
70
|
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
|
|
71
|
+
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
69
72
|
import { errorMessage as toErrorMessage } from '@/lib/shared-utils'
|
|
70
73
|
import { listUniversalToolAccessPluginIds } from '@/lib/server/universal-tool-access'
|
|
71
74
|
|
|
@@ -108,6 +111,24 @@ export function buildEnabledToolsAutonomyGuidance(): string[] {
|
|
|
108
111
|
]
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function resolveHeartbeatLastConnectorTarget(session: Session | null | undefined): {
|
|
115
|
+
connectorId?: string
|
|
116
|
+
channelId: string
|
|
117
|
+
} | null {
|
|
118
|
+
if (!isDirectConnectorSession(session)) return null
|
|
119
|
+
const connectorId = typeof session?.connectorContext?.connectorId === 'string'
|
|
120
|
+
? session.connectorContext.connectorId.trim()
|
|
121
|
+
: ''
|
|
122
|
+
const channelId = typeof session?.connectorContext?.channelId === 'string'
|
|
123
|
+
? session.connectorContext.channelId.trim()
|
|
124
|
+
: ''
|
|
125
|
+
if (!channelId) return null
|
|
126
|
+
return {
|
|
127
|
+
connectorId: connectorId || undefined,
|
|
128
|
+
channelId,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
interface SessionWithCredentials {
|
|
112
133
|
credentialId?: string | null
|
|
113
134
|
}
|
|
@@ -129,7 +150,14 @@ export interface ExecuteChatTurnInput {
|
|
|
129
150
|
signal?: AbortSignal
|
|
130
151
|
onEvent?: (event: SSEEvent) => void
|
|
131
152
|
modelOverride?: string
|
|
132
|
-
heartbeatConfig?: {
|
|
153
|
+
heartbeatConfig?: {
|
|
154
|
+
ackMaxChars: number
|
|
155
|
+
showOk: boolean
|
|
156
|
+
showAlerts: boolean
|
|
157
|
+
target: string | null
|
|
158
|
+
lightContext?: boolean
|
|
159
|
+
deliveryMode?: 'default' | 'tool_only'
|
|
160
|
+
}
|
|
133
161
|
replyToId?: string
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -421,6 +449,10 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
421
449
|
session.openclawAgentId = desiredOpenClawAgentId
|
|
422
450
|
changed = true
|
|
423
451
|
}
|
|
452
|
+
if (session.connectorContext) {
|
|
453
|
+
session.connectorContext = undefined
|
|
454
|
+
changed = true
|
|
455
|
+
}
|
|
424
456
|
}
|
|
425
457
|
|
|
426
458
|
if (changed) {
|
|
@@ -429,6 +461,29 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
429
461
|
}
|
|
430
462
|
}
|
|
431
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Build a minimal system prompt for lightweight heartbeat context.
|
|
466
|
+
* Strips conversation history, skills, tool discipline, and workspace context.
|
|
467
|
+
* Keeps identity, datetime, and heartbeat guidance for correct routing.
|
|
468
|
+
*/
|
|
469
|
+
function buildLightHeartbeatSystemPrompt(session: Session): string | undefined {
|
|
470
|
+
if (!session.agentId) return undefined
|
|
471
|
+
const agents = loadAgents()
|
|
472
|
+
const agent = agents[session.agentId]
|
|
473
|
+
if (!agent) return undefined
|
|
474
|
+
|
|
475
|
+
const parts: string[] = []
|
|
476
|
+
parts.push(`## Identity\nName: ${agent.name}`)
|
|
477
|
+
if (agent.description) parts.push(`Description: ${agent.description}`)
|
|
478
|
+
parts.push(buildCurrentDateTimePromptContext())
|
|
479
|
+
if (agent.soul) parts.push(`## Soul\n${agent.soul.slice(0, 300)}`)
|
|
480
|
+
parts.push([
|
|
481
|
+
'## Heartbeats',
|
|
482
|
+
'You run on an autonomous heartbeat. If you receive a heartbeat poll and nothing needs attention, reply exactly: HEARTBEAT_OK',
|
|
483
|
+
].join('\n'))
|
|
484
|
+
return parts.join('\n\n')
|
|
485
|
+
}
|
|
486
|
+
|
|
432
487
|
function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
433
488
|
if (!session.agentId) return undefined
|
|
434
489
|
const agents = loadAgents()
|
|
@@ -472,13 +527,16 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
472
527
|
if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
|
|
473
528
|
|
|
474
529
|
// 5. Skills (SwarmClaw Core)
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
530
|
+
try {
|
|
531
|
+
const runtimeSkills = resolveRuntimeSkills({
|
|
532
|
+
cwd: session.cwd,
|
|
533
|
+
enabledPlugins,
|
|
534
|
+
agentSkillIds: agent.skillIds || [],
|
|
535
|
+
storedSkills: loadSkills(),
|
|
536
|
+
selectedSkillId: session.skillRuntimeState?.selectedSkillId || null,
|
|
537
|
+
})
|
|
538
|
+
parts.push(...buildRuntimeSkillPromptBlocks(runtimeSkills))
|
|
539
|
+
} catch { /* non-critical */ }
|
|
482
540
|
|
|
483
541
|
// 5b. Workspace context files (HEARTBEAT.md, IDENTITY.md, AGENTS.md, etc.)
|
|
484
542
|
try {
|
|
@@ -601,6 +659,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
601
659
|
)
|
|
602
660
|
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
603
661
|
const isAutonomousInternalRun = internal && source !== 'chat'
|
|
662
|
+
const heartbeatLightContext = isHeartbeatRun && !!input.heartbeatConfig?.lightContext
|
|
604
663
|
const isAutoRunNoHistory = isHeartbeatRun
|
|
605
664
|
const heartbeatStatusOnly = false
|
|
606
665
|
if (shouldApplySessionFreshnessReset(source)) {
|
|
@@ -803,7 +862,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
803
862
|
// including identity, soul, skills, tool discipline, and execution policy.
|
|
804
863
|
// Only build the standalone system prompt for the direct-provider (no LangGraph) path
|
|
805
864
|
// to avoid duplicating tool discipline, operating guidance, and capability sections.
|
|
806
|
-
|
|
865
|
+
// lightContext mode uses a minimal prompt for both paths to reduce token cost.
|
|
866
|
+
const systemPrompt = heartbeatLightContext
|
|
867
|
+
? buildLightHeartbeatSystemPrompt(session)
|
|
868
|
+
: (hasPlugins ? undefined : buildAgentSystemPrompt(session))
|
|
807
869
|
const toolEvents: MessageToolEvent[] = []
|
|
808
870
|
const streamErrors: string[] = []
|
|
809
871
|
const accumulatedUsage = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 }
|
|
@@ -967,8 +1029,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
967
1029
|
// Heartbeat runs get a small tail of recent messages so the agent can see
|
|
968
1030
|
// prior findings and avoid repeating the same searches. Full history is
|
|
969
1031
|
// skipped to avoid blowing the context window on long-lived sessions.
|
|
1032
|
+
// lightContext mode skips history entirely for maximum token savings.
|
|
970
1033
|
const heartbeatHistory = isAutoRunNoHistory
|
|
971
|
-
? getSessionMessages(sessionId).slice(-6)
|
|
1034
|
+
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
972
1035
|
: undefined
|
|
973
1036
|
|
|
974
1037
|
console.log(`[chat-execution] provider=${providerType}, hasPlugins=${hasPlugins}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, plugins=${enabledSessionPlugins.length}`)
|
|
@@ -988,7 +1051,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
988
1051
|
fullResponse = result.finalResponse || result.fullText
|
|
989
1052
|
} else {
|
|
990
1053
|
const directHistorySnapshot = isAutoRunNoHistory
|
|
991
|
-
? getSessionMessages(sessionId).slice(-6)
|
|
1054
|
+
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
992
1055
|
: applyContextClearBoundary(getSessionMessages(sessionId))
|
|
993
1056
|
responseCacheInput = {
|
|
994
1057
|
provider: providerType,
|
|
@@ -1186,6 +1249,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1186
1249
|
&& (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
|
|
1187
1250
|
if (isDuplicate) {
|
|
1188
1251
|
heartbeatClassification = 'suppress'
|
|
1252
|
+
log.info('heartbeat', `Duplicate heartbeat suppressed for session ${sessionId} (same text within 24h)`)
|
|
1189
1253
|
}
|
|
1190
1254
|
}
|
|
1191
1255
|
}
|
|
@@ -1198,6 +1262,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1198
1262
|
const shouldPersistAssistant = !hiddenControlOnly
|
|
1199
1263
|
&& hasPersistableAssistantPayload(persistedText, thinkingText, persistedToolEvents)
|
|
1200
1264
|
&& heartbeatClassification !== 'suppress'
|
|
1265
|
+
&& !(isHeartbeatRun && heartbeatConfig?.deliveryMode === 'tool_only' && !isDirectConnectorSession(session))
|
|
1201
1266
|
|
|
1202
1267
|
const normalizeResumeId = (value: unknown): string | null =>
|
|
1203
1268
|
typeof value === 'string' && value.trim() ? value.trim() : null
|
|
@@ -1206,6 +1271,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1206
1271
|
const current = fresh[sessionId]
|
|
1207
1272
|
if (current) {
|
|
1208
1273
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1274
|
+
if (!isDirectConnectorSession(current) && current.connectorContext) {
|
|
1275
|
+
current.connectorContext = undefined
|
|
1276
|
+
}
|
|
1209
1277
|
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1210
1278
|
pruneStreamingAssistantArtifacts(current.messages, {
|
|
1211
1279
|
minIndex: runMessageStartIndex,
|
|
@@ -1279,18 +1347,22 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1279
1347
|
}
|
|
1280
1348
|
|
|
1281
1349
|
// Target routing for non-suppressed heartbeat alerts
|
|
1282
|
-
if (
|
|
1350
|
+
if (
|
|
1351
|
+
isHeartbeatRun
|
|
1352
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1353
|
+
&& heartbeatConfig?.target
|
|
1354
|
+
&& heartbeatConfig.target !== 'none'
|
|
1355
|
+
) {
|
|
1283
1356
|
try {
|
|
1284
1357
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1285
|
-
const {
|
|
1358
|
+
const { sendConnectorMessage } = require('../connectors/manager')
|
|
1286
1359
|
let connectorId: string | undefined
|
|
1287
1360
|
let channelId: string | undefined
|
|
1288
1361
|
if (heartbeatConfig.target === 'last') {
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
channelId = first.recentChannelId
|
|
1362
|
+
const lastTarget = resolveHeartbeatLastConnectorTarget(current)
|
|
1363
|
+
if (lastTarget) {
|
|
1364
|
+
connectorId = lastTarget.connectorId
|
|
1365
|
+
channelId = lastTarget.channelId
|
|
1294
1366
|
}
|
|
1295
1367
|
} else if (heartbeatConfig.target.includes(':')) {
|
|
1296
1368
|
const [cId, chId] = heartbeatConfig.target.split(':', 2)
|
|
@@ -1309,19 +1381,25 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1309
1381
|
|
|
1310
1382
|
// Auto-discover connectors linked to this agent when no explicit target is set
|
|
1311
1383
|
// Skip if a real inbound message was handled recently — the agent just responded to it
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1384
|
+
if (
|
|
1385
|
+
isHeartbeatRun
|
|
1386
|
+
&& shouldAutoRouteHeartbeatAlerts(heartbeatConfig)
|
|
1387
|
+
&& !heartbeatConfig?.target
|
|
1388
|
+
&& isDirectConnectorSession(current)
|
|
1389
|
+
) {
|
|
1390
|
+
const recentInbound = current.connectorContext?.lastInboundAt
|
|
1391
|
+
&& (Date.now() - current.connectorContext.lastInboundAt) < 60_000
|
|
1392
|
+
const connectorId = typeof current.connectorContext?.connectorId === 'string'
|
|
1393
|
+
? current.connectorContext.connectorId.trim()
|
|
1394
|
+
: ''
|
|
1395
|
+
const channelId = typeof current.connectorContext?.channelId === 'string'
|
|
1396
|
+
? current.connectorContext.channelId.trim()
|
|
1397
|
+
: ''
|
|
1398
|
+
if (!recentInbound && channelId) {
|
|
1316
1399
|
try {
|
|
1317
1400
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1318
|
-
const {
|
|
1319
|
-
|
|
1320
|
-
c.agentId === session.agentId && c.recentChannelId && c.supportsSend
|
|
1321
|
-
)
|
|
1322
|
-
for (const conn of agentConnectors) {
|
|
1323
|
-
sendMsg({ connectorId: conn.id, channelId: conn.recentChannelId, text: persistedText }).catch(() => {})
|
|
1324
|
-
}
|
|
1401
|
+
const { sendConnectorMessage: sendMsg } = require('../connectors/manager')
|
|
1402
|
+
sendMsg({ connectorId: connectorId || undefined, channelId, text: persistedText }).catch(() => {})
|
|
1325
1403
|
} catch {
|
|
1326
1404
|
// Best effort — connector manager may not be loaded
|
|
1327
1405
|
}
|
|
@@ -1332,6 +1410,15 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1332
1410
|
pruneSuppressedHeartbeatStreamMessage(current.messages)
|
|
1333
1411
|
}
|
|
1334
1412
|
|
|
1413
|
+
// P1: Prune old heartbeat messages to prevent context bloat.
|
|
1414
|
+
// Long-running agents accumulate ~48 no-op messages/day; keep only the most recent 2.
|
|
1415
|
+
if (isHeartbeatRun) {
|
|
1416
|
+
const pruned = pruneOldHeartbeatMessages(current.messages)
|
|
1417
|
+
if (pruned > 0) {
|
|
1418
|
+
log.info('heartbeat', `Pruned ${pruned} old heartbeat message(s) from session ${sessionId}`)
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1335
1422
|
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
1336
1423
|
try {
|
|
1337
1424
|
await getPluginManager().runHook('afterChatTurn', {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { MessageToolEvent } from '@/types'
|
|
2
2
|
import { canonicalizePluginId } from '@/lib/server/tool-aliases'
|
|
3
3
|
import { extractSuggestions } from '@/lib/server/suggestions'
|
|
4
|
-
import { isDirectMemoryWriteRequest } from '@/lib/server/memory/memory-policy'
|
|
5
4
|
import {
|
|
6
|
-
isBroadGoal,
|
|
7
5
|
looksLikeExternalWalletTask,
|
|
8
|
-
looksLikeOpenEndedDeliverableTask,
|
|
9
6
|
} from '@/lib/server/chat-execution/stream-continuation'
|
|
10
7
|
|
|
11
|
-
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
8
|
+
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export|create|generate)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
12
9
|
|
|
13
10
|
export function isLikelyToolErrorOutput(output: string): boolean {
|
|
14
11
|
const trimmed = String(output || '').trim()
|
|
@@ -168,37 +165,3 @@ export function compactThreadRecallText(text: string, maxChars = 180): string {
|
|
|
168
165
|
return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
|
|
172
|
-
const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
|
|
173
|
-
|
|
174
|
-
export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
|
|
175
|
-
const trimmed = String(message || '').trim()
|
|
176
|
-
if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
|
|
177
|
-
if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
|
|
178
|
-
if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
|
|
185
|
-
'memory',
|
|
186
|
-
'manage_sessions',
|
|
187
|
-
'web',
|
|
188
|
-
'context_mgmt',
|
|
189
|
-
])
|
|
190
|
-
|
|
191
|
-
export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
|
|
192
|
-
const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
|
|
193
|
-
return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
|
|
197
|
-
'memory_store',
|
|
198
|
-
'memory_update',
|
|
199
|
-
])
|
|
200
|
-
|
|
201
|
-
export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
|
|
202
|
-
const rawToolName = toolName.trim().toLowerCase()
|
|
203
|
-
return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
|
|
204
|
-
}
|