@swarmclawai/swarmclaw 0.7.2 → 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 +116 -50
- 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 +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- 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]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- 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/files/open/route.ts +16 -14
- 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/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- 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 +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- 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 +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- 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/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -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-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- 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/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- 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 +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- 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/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
1
3
|
import { genId } from '@/lib/id'
|
|
2
4
|
import type { EvalScenario, EvalRun, EvalSuiteResult } from './types'
|
|
3
5
|
import { getScenario, EVAL_SCENARIOS } from './scenarios'
|
|
@@ -5,8 +7,15 @@ import { scoreCriteria } from './scorer'
|
|
|
5
7
|
import { saveEvalRun } from './store'
|
|
6
8
|
import { loadSessions, saveSessions, loadAgents, loadCredentials, decryptKey } from '../storage'
|
|
7
9
|
import { executeSessionChatTurn } from '../chat-execution'
|
|
10
|
+
import { WORKSPACE_DIR } from '../data-dir'
|
|
8
11
|
import type { Session } from '@/types'
|
|
9
12
|
|
|
13
|
+
export function resolveEvalSessionCwd(runId: string): string {
|
|
14
|
+
const dir = path.join(WORKSPACE_DIR, 'evals', runId)
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
16
|
+
return dir
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export async function runEvalScenario(scenarioId: string, agentId: string): Promise<EvalRun> {
|
|
11
20
|
const scenario = getScenario(scenarioId)
|
|
12
21
|
if (!scenario) throw new Error(`Unknown eval scenario: ${scenarioId}`)
|
|
@@ -18,6 +27,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
|
|
|
18
27
|
const runId = genId()
|
|
19
28
|
const sessionId = `eval-${runId}`
|
|
20
29
|
const now = Date.now()
|
|
30
|
+
const sessionCwd = resolveEvalSessionCwd(runId)
|
|
21
31
|
|
|
22
32
|
const run: EvalRun = {
|
|
23
33
|
id: runId,
|
|
@@ -36,7 +46,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
|
|
|
36
46
|
const evalSession: Session = {
|
|
37
47
|
id: sessionId,
|
|
38
48
|
name: `Eval: ${scenario.name}`,
|
|
39
|
-
cwd:
|
|
49
|
+
cwd: sessionCwd,
|
|
40
50
|
user: 'eval-runner',
|
|
41
51
|
provider: (agent.provider as Session['provider']) ?? 'anthropic',
|
|
42
52
|
model: (agent.model as string) ?? '',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import Database from 'better-sqlite3'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import type { EvalRun } from './types'
|
|
4
|
+
import { DATA_DIR } from '../data-dir'
|
|
4
5
|
|
|
5
|
-
const DB_PATH = path.join(
|
|
6
|
+
const DB_PATH = path.join(DATA_DIR, 'eval-runs.db')
|
|
6
7
|
|
|
7
8
|
let db: Database.Database | null = null
|
|
8
9
|
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
|
5
|
+
DEFAULT_HEARTBEAT_INTERVAL_SEC,
|
|
6
|
+
DEFAULT_HEARTBEAT_SHOW_ALERTS,
|
|
7
|
+
DEFAULT_HEARTBEAT_SHOW_OK,
|
|
8
|
+
} from '@/lib/heartbeat-defaults'
|
|
3
9
|
import { loadAgents, loadSessions, loadSettings } from './storage'
|
|
4
10
|
import { enqueueSessionRun, getSessionRunState } from './session-run-manager'
|
|
5
11
|
import { log } from './logger'
|
|
6
|
-
import { buildMainLoopHeartbeatPrompt, getMainLoopStateForSession, isMainSession } from './main-agent-loop'
|
|
7
12
|
import { WORKSPACE_DIR } from './data-dir'
|
|
8
13
|
import { drainSystemEvents } from './system-events'
|
|
14
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
9
15
|
|
|
10
16
|
const HEARTBEAT_TICK_MS = 5_000
|
|
11
17
|
|
|
@@ -188,6 +194,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
188
194
|
if (!agent) return fallbackPrompt
|
|
189
195
|
|
|
190
196
|
const identityContext = buildIdentityContext(session, agent)
|
|
197
|
+
const continuityContext = buildIdentityContinuityContext(session, agent)
|
|
191
198
|
// Drain system events accumulated since last heartbeat
|
|
192
199
|
const events = drainSystemEvents(session.id)
|
|
193
200
|
const eventBlock = events.length > 0
|
|
@@ -219,6 +226,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
219
226
|
'AGENT_HEARTBEAT_TICK',
|
|
220
227
|
`Time: ${new Date().toISOString()}`,
|
|
221
228
|
identityContext,
|
|
229
|
+
continuityContext,
|
|
222
230
|
description ? `Description: ${description}` : '',
|
|
223
231
|
eventBlock ? `Events since last heartbeat:\n${eventBlock}` : '',
|
|
224
232
|
dynamicGoal
|
|
@@ -242,14 +250,6 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
242
250
|
].filter(Boolean).join('\n')
|
|
243
251
|
}
|
|
244
252
|
|
|
245
|
-
function applyMomentumMultiplier(intervalSec: number, momentumScore: number): number {
|
|
246
|
-
let multiplier = 1.0
|
|
247
|
-
if (momentumScore >= 80) multiplier = 0.5
|
|
248
|
-
else if (momentumScore < 40) multiplier = 2.0
|
|
249
|
-
const adjusted = Math.round(intervalSec * multiplier)
|
|
250
|
-
return Math.max(30, Math.min(7200, adjusted))
|
|
251
|
-
}
|
|
252
|
-
|
|
253
253
|
function resolveInterval(obj: Record<string, any>, currentSec: number): number {
|
|
254
254
|
// Prefer heartbeatInterval (duration string) over heartbeatIntervalSec (raw number)
|
|
255
255
|
if (obj.heartbeatInterval !== undefined && obj.heartbeatInterval !== null) {
|
|
@@ -281,7 +281,7 @@ function resolveNum(obj: Record<string, any>, key: string, current: number): num
|
|
|
281
281
|
|
|
282
282
|
function heartbeatConfigForSession(session: any, settings: Record<string, any>, agents: Record<string, any>): HeartbeatConfig {
|
|
283
283
|
// Global defaults — 30 min interval (was 120s)
|
|
284
|
-
let intervalSec = resolveInterval(settings,
|
|
284
|
+
let intervalSec = resolveInterval(settings, DEFAULT_HEARTBEAT_INTERVAL_SEC)
|
|
285
285
|
const globalPrompt = (typeof settings.heartbeatPrompt === 'string' && settings.heartbeatPrompt.trim())
|
|
286
286
|
? settings.heartbeatPrompt.trim()
|
|
287
287
|
: DEFAULT_HEARTBEAT_PROMPT
|
|
@@ -289,9 +289,9 @@ function heartbeatConfigForSession(session: any, settings: Record<string, any>,
|
|
|
289
289
|
let enabled = intervalSec > 0
|
|
290
290
|
let prompt = globalPrompt
|
|
291
291
|
let model: string | null = resolveStr(settings, 'heartbeatModel', null)
|
|
292
|
-
let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars',
|
|
293
|
-
let showOk = resolveBool(settings, 'heartbeatShowOk',
|
|
294
|
-
let showAlerts = resolveBool(settings, 'heartbeatShowAlerts',
|
|
292
|
+
let ackMaxChars = resolveNum(settings, 'heartbeatAckMaxChars', DEFAULT_HEARTBEAT_ACK_MAX_CHARS)
|
|
293
|
+
let showOk = resolveBool(settings, 'heartbeatShowOk', DEFAULT_HEARTBEAT_SHOW_OK)
|
|
294
|
+
let showAlerts = resolveBool(settings, 'heartbeatShowAlerts', DEFAULT_HEARTBEAT_SHOW_ALERTS)
|
|
295
295
|
let target: string | null = resolveStr(settings, 'heartbeatTarget', null)
|
|
296
296
|
|
|
297
297
|
// Agent layer overrides
|
|
@@ -378,7 +378,7 @@ async function tickHeartbeats() {
|
|
|
378
378
|
for (const session of Object.values(sessions) as any[]) {
|
|
379
379
|
if (!session?.id) continue
|
|
380
380
|
if (!Array.isArray(session.plugins) || session.plugins.length === 0) continue
|
|
381
|
-
if (session.sessionType && session.sessionType !== 'human'
|
|
381
|
+
if (session.sessionType && session.sessionType !== 'human') continue
|
|
382
382
|
|
|
383
383
|
// Check if this session or its agent has explicit heartbeat opt-in
|
|
384
384
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
@@ -395,10 +395,6 @@ async function tickHeartbeats() {
|
|
|
395
395
|
const cfg = heartbeatConfigForSession(session, settings, agents)
|
|
396
396
|
if (!cfg.enabled) continue
|
|
397
397
|
|
|
398
|
-
// Apply momentum-based multiplier to heartbeat interval
|
|
399
|
-
const momentumScore = session.mainLoopState?.momentumScore ?? 40
|
|
400
|
-
cfg.intervalSec = applyMomentumMultiplier(cfg.intervalSec, momentumScore)
|
|
401
|
-
|
|
402
398
|
// For sessions with explicit opt-in, use a shorter idle threshold (just intervalSec * 2).
|
|
403
399
|
// For inherited/global heartbeats, keep the 180s minimum to avoid noisy auto-fire.
|
|
404
400
|
const defaultIdleSec = explicitOptIn
|
|
@@ -410,38 +406,22 @@ async function tickHeartbeats() {
|
|
|
410
406
|
const idleMs = now - lastUserAt
|
|
411
407
|
if (idleMs < userIdleThresholdSec * 1000) continue
|
|
412
408
|
|
|
413
|
-
if (isMainSession(session)) {
|
|
414
|
-
const loopState = getMainLoopStateForSession(session.id)
|
|
415
|
-
if (loopState?.paused) continue
|
|
416
|
-
// Only suppress idle main sessions when heartbeat is inherited (not explicitly enabled)
|
|
417
|
-
if (!explicitOptIn) {
|
|
418
|
-
const loopStatus = loopState?.status || 'idle'
|
|
419
|
-
const pendingEvents = loopState?.pendingEvents?.length || 0
|
|
420
|
-
if ((loopStatus === 'ok' || loopStatus === 'idle') && pendingEvents === 0) continue
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
409
|
const last = state.lastBySession.get(session.id) || 0
|
|
425
410
|
if (now - last < cfg.intervalSec * 1000) continue
|
|
426
411
|
|
|
427
412
|
const runState = getSessionRunState(session.id)
|
|
428
413
|
if (runState.runningRunId) continue
|
|
429
414
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// Skip heartbeat only if there's truly nothing to drive it:
|
|
439
|
-
// no agent goal, no HEARTBEAT.md content, AND no custom prompt configured
|
|
440
|
-
if (!hasGoal && !heartbeatFileContent && !hasCustomPrompt) {
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
443
|
-
heartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent)
|
|
415
|
+
const rawHeartbeatFileContent = readHeartbeatFile(session)
|
|
416
|
+
const heartbeatFileContent = isHeartbeatContentEffectivelyEmpty(rawHeartbeatFileContent) ? '' : rawHeartbeatFileContent
|
|
417
|
+
const hasGoal = !!(agent?.heartbeatGoal || agent?.description || agent?.systemPrompt || agent?.soul)
|
|
418
|
+
const hasCustomPrompt = cfg.prompt !== DEFAULT_HEARTBEAT_PROMPT
|
|
419
|
+
// Skip heartbeat only if there's truly nothing to drive it:
|
|
420
|
+
// no agent goal, no HEARTBEAT.md content, AND no custom prompt configured
|
|
421
|
+
if (!hasGoal && !heartbeatFileContent && !hasCustomPrompt) {
|
|
422
|
+
continue
|
|
444
423
|
}
|
|
424
|
+
const heartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent)
|
|
445
425
|
|
|
446
426
|
const enqueue = enqueueSessionRun({
|
|
447
427
|
sessionId: session.id,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
|
|
4
|
+
|
|
5
|
+
describe('heartbeat-source', () => {
|
|
6
|
+
it('treats scheduled heartbeat polls as heartbeat traffic', () => {
|
|
7
|
+
assert.equal(isHeartbeatSource('heartbeat'), true)
|
|
8
|
+
assert.equal(isInternalHeartbeatRun(true, 'heartbeat'), true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('treats wake-triggered heartbeat polls as heartbeat traffic', () => {
|
|
12
|
+
assert.equal(isHeartbeatSource('heartbeat-wake'), true)
|
|
13
|
+
assert.equal(isInternalHeartbeatRun(true, 'heartbeat-wake'), true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not classify other sources as heartbeat traffic', () => {
|
|
17
|
+
assert.equal(isHeartbeatSource('task'), false)
|
|
18
|
+
assert.equal(isHeartbeatSource('chat'), false)
|
|
19
|
+
assert.equal(isInternalHeartbeatRun(false, 'heartbeat'), false)
|
|
20
|
+
assert.equal(isInternalHeartbeatRun(true, 'task'), false)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function isHeartbeatSource(source: string | null | undefined): boolean {
|
|
2
|
+
return source === 'heartbeat' || source === 'heartbeat-wake'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isInternalHeartbeatRun(internal: boolean | null | undefined, source: string | null | undefined): boolean {
|
|
6
|
+
return internal === true && isHeartbeatSource(source)
|
|
7
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import type { Session } from '@/types'
|
|
4
|
+
import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
|
|
5
|
+
|
|
6
|
+
test('buildIdentityContinuityContext merges agent and session continuity', () => {
|
|
7
|
+
const block = buildIdentityContinuityContext(
|
|
8
|
+
{
|
|
9
|
+
name: 'Thread A',
|
|
10
|
+
conversationTone: 'technical',
|
|
11
|
+
identityState: {
|
|
12
|
+
personaLabel: 'Debugger',
|
|
13
|
+
relationshipSummary: 'Working with the user on a production issue.',
|
|
14
|
+
},
|
|
15
|
+
} as Partial<Session>,
|
|
16
|
+
{
|
|
17
|
+
name: 'Swarmy',
|
|
18
|
+
description: 'Helpful coding agent',
|
|
19
|
+
identityState: {
|
|
20
|
+
boundaries: ['Do not pretend work is complete without evidence.'],
|
|
21
|
+
continuityNotes: ['User prefers concise explanations.'],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert.match(block, /Identity Continuity/)
|
|
27
|
+
assert.match(block, /Current persona: Debugger/)
|
|
28
|
+
assert.match(block, /Observed tone: technical/)
|
|
29
|
+
assert.match(block, /User prefers concise explanations/)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('refreshSessionIdentityState derives fallback continuity fields', () => {
|
|
33
|
+
const session = {
|
|
34
|
+
id: 's1',
|
|
35
|
+
name: 'Checkout Bug',
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
user: 'Taylor',
|
|
38
|
+
provider: 'openai',
|
|
39
|
+
model: 'gpt-4.1',
|
|
40
|
+
claudeSessionId: null,
|
|
41
|
+
codexThreadId: null,
|
|
42
|
+
opencodeSessionId: null,
|
|
43
|
+
messages: [{ role: 'user', text: 'Help', time: 1 }],
|
|
44
|
+
createdAt: 1,
|
|
45
|
+
lastActiveAt: 1,
|
|
46
|
+
conversationTone: 'focused',
|
|
47
|
+
connectorContext: { threadId: 'thread-9', senderName: 'Taylor' },
|
|
48
|
+
} as Session
|
|
49
|
+
|
|
50
|
+
const state = refreshSessionIdentityState(session, {
|
|
51
|
+
name: 'Swarmy',
|
|
52
|
+
description: 'Helpful coding agent',
|
|
53
|
+
}, 100)
|
|
54
|
+
|
|
55
|
+
assert.equal(state.personaLabel, 'Swarmy thread thread-9')
|
|
56
|
+
assert.equal(state.relationshipSummary, 'Ongoing conversation with Taylor.')
|
|
57
|
+
assert.equal(state.toneStyle, 'focused')
|
|
58
|
+
assert.equal(state.updatedAt, 100)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('buildIdentityContinuityContext prefers thread persona labels from connector context', () => {
|
|
62
|
+
const block = buildIdentityContinuityContext(
|
|
63
|
+
{
|
|
64
|
+
name: 'Connector Session',
|
|
65
|
+
connectorContext: {
|
|
66
|
+
threadId: 'thread-9',
|
|
67
|
+
threadPersonaLabel: 'Checkout Incident',
|
|
68
|
+
},
|
|
69
|
+
} as Partial<Session>,
|
|
70
|
+
{
|
|
71
|
+
name: 'Swarmy',
|
|
72
|
+
description: 'Helpful coding agent',
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert.match(block, /Current persona: Checkout Incident/)
|
|
77
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Agent, IdentityContinuityState, Session } from '@/types'
|
|
2
|
+
|
|
3
|
+
function normalizeText(value: unknown, maxChars: number): string | null {
|
|
4
|
+
if (typeof value !== 'string') return null
|
|
5
|
+
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
6
|
+
return normalized ? normalized.slice(0, maxChars) : null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeList(value: unknown, maxItems: number, maxChars: number): string[] {
|
|
10
|
+
if (!Array.isArray(value)) return []
|
|
11
|
+
const seen = new Set<string>()
|
|
12
|
+
const out: string[] = []
|
|
13
|
+
for (const raw of value) {
|
|
14
|
+
const normalized = normalizeText(raw, maxChars)
|
|
15
|
+
if (!normalized) continue
|
|
16
|
+
const key = normalized.toLowerCase()
|
|
17
|
+
if (seen.has(key)) continue
|
|
18
|
+
seen.add(key)
|
|
19
|
+
out.push(normalized)
|
|
20
|
+
if (out.length >= maxItems) break
|
|
21
|
+
}
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeIdentityContinuityState(raw: unknown): IdentityContinuityState | null {
|
|
26
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
27
|
+
const record = raw as Record<string, unknown>
|
|
28
|
+
const state: IdentityContinuityState = {
|
|
29
|
+
selfSummary: normalizeText(record.selfSummary, 320),
|
|
30
|
+
relationshipSummary: normalizeText(record.relationshipSummary, 320),
|
|
31
|
+
personaLabel: normalizeText(record.personaLabel, 120),
|
|
32
|
+
toneStyle: normalizeText(record.toneStyle, 120),
|
|
33
|
+
boundaries: normalizeList(record.boundaries, 6, 180),
|
|
34
|
+
continuityNotes: normalizeList(record.continuityNotes, 8, 220),
|
|
35
|
+
updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt)
|
|
36
|
+
? Math.trunc(record.updatedAt)
|
|
37
|
+
: null,
|
|
38
|
+
}
|
|
39
|
+
return state
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fallbackSelfSummary(agent?: Partial<Agent> | null): string | null {
|
|
43
|
+
const description = normalizeText(agent?.description, 220)
|
|
44
|
+
if (description) return `${agent?.name || 'Agent'}: ${description}`
|
|
45
|
+
const soul = normalizeText(agent?.soul, 220)
|
|
46
|
+
if (soul) return `${agent?.name || 'Agent'}: ${soul}`
|
|
47
|
+
const name = normalizeText(agent?.name, 80)
|
|
48
|
+
return name ? `${name}: persistent companion agent` : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fallbackPersonaLabel(session?: Partial<Session> | null, agent?: Partial<Agent> | null): string | null {
|
|
52
|
+
const threadPersona = normalizeText(session?.connectorContext?.threadPersonaLabel, 120)
|
|
53
|
+
if (threadPersona) return threadPersona
|
|
54
|
+
const threadTitle = normalizeText(session?.connectorContext?.threadTitle, 120)
|
|
55
|
+
if (threadTitle) return threadTitle
|
|
56
|
+
const threadId = normalizeText(session?.connectorContext?.threadId, 80)
|
|
57
|
+
if (threadId) return `${agent?.name || 'Agent'} thread ${threadId}`
|
|
58
|
+
const sessionName = normalizeText(session?.name, 120)
|
|
59
|
+
if (sessionName && !/^new chat$/i.test(sessionName)) return sessionName
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fallbackRelationshipSummary(session?: Partial<Session> | null): string | null {
|
|
64
|
+
const sender = normalizeText(session?.connectorContext?.senderName, 80)
|
|
65
|
+
if (sender) return `Ongoing conversation with ${sender}.`
|
|
66
|
+
const user = normalizeText(session?.user, 80)
|
|
67
|
+
if (user && user !== 'user') return `Ongoing conversation with ${user}.`
|
|
68
|
+
return 'Ongoing conversation with the user.'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildIdentityContinuityContext(
|
|
72
|
+
session?: Partial<Session> | null,
|
|
73
|
+
agent?: Partial<Agent> | null,
|
|
74
|
+
): string {
|
|
75
|
+
const agentState = normalizeIdentityContinuityState(agent?.identityState)
|
|
76
|
+
const sessionState = normalizeIdentityContinuityState(session?.identityState)
|
|
77
|
+
const selfSummary = sessionState?.selfSummary || agentState?.selfSummary || fallbackSelfSummary(agent)
|
|
78
|
+
const relationshipSummary = sessionState?.relationshipSummary || agentState?.relationshipSummary || fallbackRelationshipSummary(session)
|
|
79
|
+
const personaLabel = sessionState?.personaLabel || fallbackPersonaLabel(session, agent)
|
|
80
|
+
const toneStyle = sessionState?.toneStyle || normalizeText(session?.conversationTone, 80) || agentState?.toneStyle
|
|
81
|
+
const boundaries = sessionState?.boundaries?.length
|
|
82
|
+
? sessionState.boundaries
|
|
83
|
+
: agentState?.boundaries?.length
|
|
84
|
+
? agentState.boundaries
|
|
85
|
+
: []
|
|
86
|
+
const continuityNotes = [
|
|
87
|
+
...(agentState?.continuityNotes || []),
|
|
88
|
+
...(sessionState?.continuityNotes || []),
|
|
89
|
+
].slice(-6)
|
|
90
|
+
|
|
91
|
+
const lines: string[] = []
|
|
92
|
+
if (selfSummary) lines.push(`Core self: ${selfSummary}`)
|
|
93
|
+
if (personaLabel) lines.push(`Current persona: ${personaLabel}`)
|
|
94
|
+
if (relationshipSummary) lines.push(`Relationship context: ${relationshipSummary}`)
|
|
95
|
+
if (toneStyle) lines.push(`Observed tone: ${toneStyle}`)
|
|
96
|
+
if (boundaries.length) lines.push(`Boundaries: ${boundaries.join(' | ')}`)
|
|
97
|
+
if (continuityNotes.length) lines.push(`Continuity notes: ${continuityNotes.join(' | ')}`)
|
|
98
|
+
if (!lines.length) return ''
|
|
99
|
+
return `## Identity Continuity\n${lines.join('\n')}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function refreshSessionIdentityState(
|
|
103
|
+
session: Session,
|
|
104
|
+
agent?: Partial<Agent> | null,
|
|
105
|
+
now = Date.now(),
|
|
106
|
+
): IdentityContinuityState {
|
|
107
|
+
const existing = normalizeIdentityContinuityState(session.identityState) || {}
|
|
108
|
+
const agentState = normalizeIdentityContinuityState(agent?.identityState) || {}
|
|
109
|
+
const boundaries = existing.boundaries?.length ? existing.boundaries : (agentState.boundaries || [])
|
|
110
|
+
const continuityNotes = [
|
|
111
|
+
...(agentState.continuityNotes || []),
|
|
112
|
+
...(existing.continuityNotes || []),
|
|
113
|
+
].slice(-8)
|
|
114
|
+
|
|
115
|
+
const next: IdentityContinuityState = {
|
|
116
|
+
selfSummary: existing.selfSummary || agentState.selfSummary || fallbackSelfSummary(agent),
|
|
117
|
+
relationshipSummary: existing.relationshipSummary || agentState.relationshipSummary || fallbackRelationshipSummary(session),
|
|
118
|
+
personaLabel: existing.personaLabel || fallbackPersonaLabel(session, agent),
|
|
119
|
+
toneStyle: normalizeText(session.conversationTone, 80) || existing.toneStyle || agentState.toneStyle,
|
|
120
|
+
boundaries,
|
|
121
|
+
continuityNotes,
|
|
122
|
+
updatedAt: now,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
session.identityState = next
|
|
126
|
+
return next
|
|
127
|
+
}
|