@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- 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/files/open/route.ts +16 -14
- 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/skills/route.ts +11 -3
- 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 +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- 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 +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- 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 +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- 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/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -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 +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- 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 +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- 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 +948 -112
- 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 +188 -9
- 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/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -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/heartbeat-service.ts +14 -40
- 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 +28 -1103
- 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 +5 -6
- 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 +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- 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 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- 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 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- 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/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { spawnSync } from 'child_process'
|
|
4
|
+
import * as cheerio from 'cheerio'
|
|
5
|
+
import { findBinaryOnPath } from './session-tools/context'
|
|
6
|
+
|
|
7
|
+
const TEXT_EXTENSIONS = new Set([
|
|
8
|
+
'.txt', '.md', '.markdown', '.json', '.jsonl', '.csv', '.tsv',
|
|
9
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs',
|
|
10
|
+
'.java', '.yaml', '.yml', '.sql', '.xml', '.css', '.scss', '.html', '.htm',
|
|
11
|
+
])
|
|
12
|
+
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tif', '.tiff'])
|
|
13
|
+
|
|
14
|
+
export interface StructuredTable {
|
|
15
|
+
name: string
|
|
16
|
+
headers: string[]
|
|
17
|
+
rows: Array<Record<string, unknown>>
|
|
18
|
+
rowCount: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DocumentArtifact {
|
|
22
|
+
filePath: string
|
|
23
|
+
fileName: string
|
|
24
|
+
ext: string
|
|
25
|
+
method: string
|
|
26
|
+
text: string
|
|
27
|
+
metadata: Record<string, unknown>
|
|
28
|
+
tables: StructuredTable[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function trimText(text: string, maxChars = 200_000): string {
|
|
32
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\u0000/g, '').trim()
|
|
33
|
+
if (normalized.length <= maxChars) return normalized
|
|
34
|
+
return `${normalized.slice(0, maxChars)}\n... [truncated]`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeScalar(value: unknown): unknown {
|
|
38
|
+
if (value === undefined) return null
|
|
39
|
+
if (value === null) return null
|
|
40
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value
|
|
41
|
+
if (value instanceof Date) return value.toISOString()
|
|
42
|
+
return String(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDelimitedText(input: string, delimiter: string): string[][] {
|
|
46
|
+
const rows: string[][] = []
|
|
47
|
+
let row: string[] = []
|
|
48
|
+
let field = ''
|
|
49
|
+
let inQuotes = false
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
52
|
+
const char = input[index]
|
|
53
|
+
const next = input[index + 1]
|
|
54
|
+
|
|
55
|
+
if (inQuotes) {
|
|
56
|
+
if (char === '"' && next === '"') {
|
|
57
|
+
field += '"'
|
|
58
|
+
index += 1
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
if (char === '"') {
|
|
62
|
+
inQuotes = false
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
field += char
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (char === '"') {
|
|
70
|
+
inQuotes = true
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (char === delimiter) {
|
|
74
|
+
row.push(field)
|
|
75
|
+
field = ''
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (char === '\n') {
|
|
79
|
+
row.push(field)
|
|
80
|
+
rows.push(row)
|
|
81
|
+
row = []
|
|
82
|
+
field = ''
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (char === '\r') continue
|
|
86
|
+
field += char
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (field.length > 0 || row.length > 0) {
|
|
90
|
+
row.push(field)
|
|
91
|
+
rows.push(row)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return rows.filter((cells) => cells.some((cell) => cell.trim().length > 0))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function matrixToTable(name: string, matrix: string[][]): StructuredTable {
|
|
98
|
+
if (matrix.length === 0) return { name, headers: [], rows: [], rowCount: 0 }
|
|
99
|
+
const headerRow = matrix[0].map((cell, index) => cell.trim() || `column_${index + 1}`)
|
|
100
|
+
const rows = matrix.slice(1).map((cells) => {
|
|
101
|
+
const row: Record<string, unknown> = {}
|
|
102
|
+
for (let index = 0; index < headerRow.length; index += 1) {
|
|
103
|
+
row[headerRow[index]] = cells[index] ?? ''
|
|
104
|
+
}
|
|
105
|
+
return row
|
|
106
|
+
})
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
headers: headerRow,
|
|
110
|
+
rows,
|
|
111
|
+
rowCount: rows.length,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function objectsToTable(name: string, rows: Array<Record<string, unknown>>): StructuredTable {
|
|
116
|
+
const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
|
|
117
|
+
const normalizedRows = rows.map((row) => {
|
|
118
|
+
const out: Record<string, unknown> = {}
|
|
119
|
+
for (const header of headers) out[header] = normalizeScalar(row[header])
|
|
120
|
+
return out
|
|
121
|
+
})
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
headers,
|
|
125
|
+
rows: normalizedRows,
|
|
126
|
+
rowCount: normalizedRows.length,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function tablesToText(tables: StructuredTable[]): string {
|
|
131
|
+
return tables
|
|
132
|
+
.map((table) => {
|
|
133
|
+
const header = table.headers.join('\t')
|
|
134
|
+
const body = table.rows.slice(0, 100).map((row) => table.headers.map((key) => String(row[key] ?? '')).join('\t')).join('\n')
|
|
135
|
+
return `${table.name}\n${header}${body ? `\n${body}` : ''}`
|
|
136
|
+
})
|
|
137
|
+
.join('\n\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function worksheetRowToArray(values: unknown): unknown[] {
|
|
141
|
+
if (Array.isArray(values)) return values.slice(1)
|
|
142
|
+
if (values && typeof values === 'object') {
|
|
143
|
+
return Object.entries(values as Record<string, unknown>)
|
|
144
|
+
.filter(([key]) => Number.isFinite(Number(key)) && Number(key) >= 1)
|
|
145
|
+
.sort((left, right) => Number(left[0]) - Number(right[0]))
|
|
146
|
+
.map(([, value]) => value)
|
|
147
|
+
}
|
|
148
|
+
return []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function listZipEntries(filePath: string): { entries: string[]; method: string } {
|
|
152
|
+
const unzip = findBinaryOnPath('unzip') || findBinaryOnPath('zipinfo')
|
|
153
|
+
if (!unzip) throw new Error('ZIP listing requires `unzip` or `zipinfo` on PATH.')
|
|
154
|
+
const args = path.basename(unzip).includes('zipinfo') ? ['-1', filePath] : ['-Z1', filePath]
|
|
155
|
+
const out = spawnSync(unzip, args, {
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
158
|
+
timeout: 20_000,
|
|
159
|
+
})
|
|
160
|
+
if ((out.status ?? 1) !== 0) {
|
|
161
|
+
throw new Error(`Failed to inspect ZIP: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
|
|
162
|
+
}
|
|
163
|
+
const entries = (out.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
164
|
+
return { entries, method: path.basename(unzip) }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function extractPdfText(filePath: string): Promise<{ text: string; method: string }> {
|
|
168
|
+
try {
|
|
169
|
+
const pdfMod = await import(/* webpackIgnore: true */ 'pdf-parse')
|
|
170
|
+
const pdfParse = ((pdfMod as Record<string, unknown>).default ?? pdfMod) as (buf: Buffer) => Promise<{ text: string }>
|
|
171
|
+
const result = await pdfParse(fs.readFileSync(filePath))
|
|
172
|
+
if ((result.text || '').trim()) {
|
|
173
|
+
return { text: result.text, method: 'pdf-parse' }
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// fall through to pdftotext
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const pdftotext = findBinaryOnPath('pdftotext')
|
|
180
|
+
if (!pdftotext) throw new Error('PDF extraction requires `pdf-parse` or `pdftotext`.')
|
|
181
|
+
const out = spawnSync(pdftotext, ['-layout', '-nopgbrk', '-q', filePath, '-'], {
|
|
182
|
+
encoding: 'utf-8',
|
|
183
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
184
|
+
timeout: 20_000,
|
|
185
|
+
})
|
|
186
|
+
if ((out.status ?? 1) !== 0) {
|
|
187
|
+
throw new Error(`pdftotext failed: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
|
|
188
|
+
}
|
|
189
|
+
return { text: out.stdout || '', method: 'pdftotext' }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractImageText(filePath: string): { text: string; method: string } {
|
|
193
|
+
const tesseract = findBinaryOnPath('tesseract')
|
|
194
|
+
if (!tesseract) {
|
|
195
|
+
throw new Error('Image OCR requires `tesseract` on PATH.')
|
|
196
|
+
}
|
|
197
|
+
const out = spawnSync(tesseract, [filePath, 'stdout', '--psm', '6'], {
|
|
198
|
+
encoding: 'utf-8',
|
|
199
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
200
|
+
timeout: 30_000,
|
|
201
|
+
})
|
|
202
|
+
if ((out.status ?? 1) !== 0) {
|
|
203
|
+
throw new Error(`tesseract failed: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
|
|
204
|
+
}
|
|
205
|
+
return { text: out.stdout || '', method: 'tesseract' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function extractRichText(filePath: string): { text: string; method: string } {
|
|
209
|
+
const textutil = findBinaryOnPath('textutil')
|
|
210
|
+
if (!textutil) throw new Error('DOC/DOCX/RTF extraction requires `textutil` on PATH.')
|
|
211
|
+
const out = spawnSync(textutil, ['-convert', 'txt', '-stdout', filePath], {
|
|
212
|
+
encoding: 'utf-8',
|
|
213
|
+
maxBuffer: 25 * 1024 * 1024,
|
|
214
|
+
timeout: 20_000,
|
|
215
|
+
})
|
|
216
|
+
if ((out.status ?? 1) !== 0 || !(out.stdout || '').trim()) {
|
|
217
|
+
throw new Error(`textutil failed: ${(out.stderr || out.stdout || '').trim() || 'unknown error'}`)
|
|
218
|
+
}
|
|
219
|
+
return { text: out.stdout || '', method: 'textutil' }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function extractDocumentArtifact(filePath: string, options?: { maxChars?: number; preferOcr?: boolean }): Promise<DocumentArtifact> {
|
|
223
|
+
const resolved = path.resolve(filePath)
|
|
224
|
+
if (!fs.existsSync(resolved)) throw new Error(`File not found: ${filePath}`)
|
|
225
|
+
const stat = fs.statSync(resolved)
|
|
226
|
+
if (!stat.isFile()) throw new Error(`Expected a file: ${filePath}`)
|
|
227
|
+
|
|
228
|
+
const ext = path.extname(resolved).toLowerCase()
|
|
229
|
+
const metadata: Record<string, unknown> = {
|
|
230
|
+
sizeBytes: stat.size,
|
|
231
|
+
modifiedAt: stat.mtimeMs,
|
|
232
|
+
}
|
|
233
|
+
const maxChars = options?.maxChars || 200_000
|
|
234
|
+
let text = ''
|
|
235
|
+
let method = 'utf8'
|
|
236
|
+
let tables: StructuredTable[] = []
|
|
237
|
+
|
|
238
|
+
if (ext === '.pdf') {
|
|
239
|
+
const pdf = await extractPdfText(resolved)
|
|
240
|
+
text = pdf.text
|
|
241
|
+
method = pdf.method
|
|
242
|
+
} else if (ext === '.csv' || ext === '.tsv') {
|
|
243
|
+
const delimiter = ext === '.tsv' ? '\t' : ','
|
|
244
|
+
const raw = fs.readFileSync(resolved, 'utf-8')
|
|
245
|
+
const table = matrixToTable(path.basename(resolved), parseDelimitedText(raw, delimiter))
|
|
246
|
+
tables = [table]
|
|
247
|
+
text = tablesToText(tables)
|
|
248
|
+
method = ext === '.tsv' ? 'tsv' : 'csv'
|
|
249
|
+
} else if (ext === '.xlsx' || ext === '.xlsm') {
|
|
250
|
+
const ExcelJS = await import('exceljs')
|
|
251
|
+
const workbook = new ExcelJS.Workbook()
|
|
252
|
+
await workbook.xlsx.readFile(resolved)
|
|
253
|
+
tables = workbook.worksheets.map((worksheet) => {
|
|
254
|
+
const matrix: string[][] = []
|
|
255
|
+
worksheet.eachRow((row) => {
|
|
256
|
+
matrix.push(worksheetRowToArray(row.values).map((cell) => String(normalizeScalar(cell) ?? '')))
|
|
257
|
+
})
|
|
258
|
+
return matrixToTable(worksheet.name, matrix)
|
|
259
|
+
}).filter((table) => table.headers.length > 0 || table.rowCount > 0)
|
|
260
|
+
text = tablesToText(tables)
|
|
261
|
+
method = 'exceljs'
|
|
262
|
+
metadata.sheetNames = workbook.worksheets.map((sheet) => sheet.name)
|
|
263
|
+
} else if (ext === '.json') {
|
|
264
|
+
const raw = fs.readFileSync(resolved, 'utf-8')
|
|
265
|
+
text = raw
|
|
266
|
+
method = 'json'
|
|
267
|
+
try {
|
|
268
|
+
const parsed = JSON.parse(raw)
|
|
269
|
+
if (Array.isArray(parsed) && parsed.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
|
|
270
|
+
tables = [objectsToTable(path.basename(resolved), parsed as Array<Record<string, unknown>>)]
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// keep raw json text only
|
|
274
|
+
}
|
|
275
|
+
} else if (ext === '.html' || ext === '.htm') {
|
|
276
|
+
const html = fs.readFileSync(resolved, 'utf-8')
|
|
277
|
+
const $ = cheerio.load(html)
|
|
278
|
+
$('script, style, noscript').remove()
|
|
279
|
+
text = $('body').text() || $.text()
|
|
280
|
+
method = 'html-strip'
|
|
281
|
+
} else if (ext === '.zip') {
|
|
282
|
+
const zip = listZipEntries(resolved)
|
|
283
|
+
text = zip.entries.join('\n')
|
|
284
|
+
method = zip.method
|
|
285
|
+
metadata.entries = zip.entries
|
|
286
|
+
} else if (ext === '.doc' || ext === '.docx' || ext === '.rtf') {
|
|
287
|
+
const rich = extractRichText(resolved)
|
|
288
|
+
text = rich.text
|
|
289
|
+
method = rich.method
|
|
290
|
+
} else if (IMAGE_EXTENSIONS.has(ext) || options?.preferOcr === true) {
|
|
291
|
+
const image = extractImageText(resolved)
|
|
292
|
+
text = image.text
|
|
293
|
+
method = image.method
|
|
294
|
+
} else if (TEXT_EXTENSIONS.has(ext) || !ext) {
|
|
295
|
+
text = fs.readFileSync(resolved, 'utf-8')
|
|
296
|
+
method = 'utf8'
|
|
297
|
+
} else {
|
|
298
|
+
text = fs.readFileSync(resolved, 'utf-8')
|
|
299
|
+
method = 'utf8-fallback'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
filePath: resolved,
|
|
304
|
+
fileName: path.basename(resolved),
|
|
305
|
+
ext,
|
|
306
|
+
method,
|
|
307
|
+
text: trimText(text, maxChars),
|
|
308
|
+
metadata,
|
|
309
|
+
tables,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function loadTabularFile(filePath: string, options?: { sheetName?: string }): Promise<StructuredTable> {
|
|
314
|
+
const resolved = path.resolve(filePath)
|
|
315
|
+
const ext = path.extname(resolved).toLowerCase()
|
|
316
|
+
if (ext === '.csv' || ext === '.tsv') {
|
|
317
|
+
const delimiter = ext === '.tsv' ? '\t' : ','
|
|
318
|
+
return matrixToTable(path.basename(resolved), parseDelimitedText(fs.readFileSync(resolved, 'utf-8'), delimiter))
|
|
319
|
+
}
|
|
320
|
+
if (ext === '.json') {
|
|
321
|
+
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf-8'))
|
|
322
|
+
if (!Array.isArray(parsed) || !parsed.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
|
|
323
|
+
throw new Error('JSON table inputs must be an array of objects.')
|
|
324
|
+
}
|
|
325
|
+
return objectsToTable(path.basename(resolved), parsed as Array<Record<string, unknown>>)
|
|
326
|
+
}
|
|
327
|
+
if (ext === '.xlsx' || ext === '.xlsm') {
|
|
328
|
+
const ExcelJS = await import('exceljs')
|
|
329
|
+
const workbook = new ExcelJS.Workbook()
|
|
330
|
+
await workbook.xlsx.readFile(resolved)
|
|
331
|
+
const target = options?.sheetName
|
|
332
|
+
? workbook.getWorksheet(options.sheetName)
|
|
333
|
+
: workbook.worksheets[0]
|
|
334
|
+
if (!target) throw new Error(`Worksheet not found: ${options?.sheetName || '(first worksheet)'}`)
|
|
335
|
+
const matrix: string[][] = []
|
|
336
|
+
target.eachRow((row) => {
|
|
337
|
+
matrix.push(worksheetRowToArray(row.values).map((cell) => String(normalizeScalar(cell) ?? '')))
|
|
338
|
+
})
|
|
339
|
+
return matrixToTable(target.name, matrix)
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`Unsupported tabular file: ${ext || '(no extension)'}`)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function normalizeInlineRows(value: unknown): StructuredTable {
|
|
345
|
+
if (!Array.isArray(value)) throw new Error('rows must be an array.')
|
|
346
|
+
if (value.length === 0) return { name: 'rows', headers: [], rows: [], rowCount: 0 }
|
|
347
|
+
if (value.every((row) => Array.isArray(row))) {
|
|
348
|
+
return matrixToTable('rows', value.map((row) => (row as unknown[]).map((cell) => String(normalizeScalar(cell) ?? ''))))
|
|
349
|
+
}
|
|
350
|
+
if (value.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
|
|
351
|
+
return objectsToTable('rows', value as Array<Record<string, unknown>>)
|
|
352
|
+
}
|
|
353
|
+
throw new Error('rows must be an array of objects or arrays.')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function escapeDelimitedCell(value: unknown, delimiter: string): string {
|
|
357
|
+
const raw = String(normalizeScalar(value) ?? '')
|
|
358
|
+
if (raw.includes('"') || raw.includes('\n') || raw.includes(delimiter)) {
|
|
359
|
+
return `"${raw.replace(/"/g, '""')}"`
|
|
360
|
+
}
|
|
361
|
+
return raw
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function serializeTable(table: StructuredTable, delimiter = ','): string {
|
|
365
|
+
const header = table.headers.map((cell) => escapeDelimitedCell(cell, delimiter)).join(delimiter)
|
|
366
|
+
const rows = table.rows.map((row) => table.headers.map((headerCell) => escapeDelimitedCell(row[headerCell], delimiter)).join(delimiter))
|
|
367
|
+
return [header, ...rows].join('\n')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function writeStructuredTable(filePath: string, table: StructuredTable): Promise<{ filePath: string; format: string }> {
|
|
371
|
+
const resolved = path.resolve(filePath)
|
|
372
|
+
const ext = path.extname(resolved).toLowerCase()
|
|
373
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
374
|
+
|
|
375
|
+
if (ext === '.json') {
|
|
376
|
+
fs.writeFileSync(resolved, JSON.stringify(table.rows, null, 2), 'utf-8')
|
|
377
|
+
return { filePath: resolved, format: 'json' }
|
|
378
|
+
}
|
|
379
|
+
if (ext === '.tsv') {
|
|
380
|
+
fs.writeFileSync(resolved, serializeTable(table, '\t'), 'utf-8')
|
|
381
|
+
return { filePath: resolved, format: 'tsv' }
|
|
382
|
+
}
|
|
383
|
+
if (ext === '.xlsx') {
|
|
384
|
+
const ExcelJS = await import('exceljs')
|
|
385
|
+
const workbook = new ExcelJS.Workbook()
|
|
386
|
+
const worksheet = workbook.addWorksheet(table.name || 'Sheet1')
|
|
387
|
+
worksheet.addRow(table.headers)
|
|
388
|
+
for (const row of table.rows) {
|
|
389
|
+
worksheet.addRow(table.headers.map((header) => row[header] ?? null))
|
|
390
|
+
}
|
|
391
|
+
await workbook.xlsx.writeFile(resolved)
|
|
392
|
+
return { filePath: resolved, format: 'xlsx' }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fs.writeFileSync(resolved, serializeTable(table, ','), 'utf-8')
|
|
396
|
+
return { filePath: resolved, format: 'csv' }
|
|
397
|
+
}
|
|
@@ -3,9 +3,9 @@ import path from 'path'
|
|
|
3
3
|
import { loadAgents, loadSessions, loadSettings } from './storage'
|
|
4
4
|
import { enqueueSessionRun, getSessionRunState } from './session-run-manager'
|
|
5
5
|
import { log } from './logger'
|
|
6
|
-
import { buildMainLoopHeartbeatPrompt, getMainLoopStateForSession, isMainSession } from './main-agent-loop'
|
|
7
6
|
import { WORKSPACE_DIR } from './data-dir'
|
|
8
7
|
import { drainSystemEvents } from './system-events'
|
|
8
|
+
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
9
9
|
|
|
10
10
|
const HEARTBEAT_TICK_MS = 5_000
|
|
11
11
|
|
|
@@ -188,6 +188,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
188
188
|
if (!agent) return fallbackPrompt
|
|
189
189
|
|
|
190
190
|
const identityContext = buildIdentityContext(session, agent)
|
|
191
|
+
const continuityContext = buildIdentityContinuityContext(session, agent)
|
|
191
192
|
// Drain system events accumulated since last heartbeat
|
|
192
193
|
const events = drainSystemEvents(session.id)
|
|
193
194
|
const eventBlock = events.length > 0
|
|
@@ -219,6 +220,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
219
220
|
'AGENT_HEARTBEAT_TICK',
|
|
220
221
|
`Time: ${new Date().toISOString()}`,
|
|
221
222
|
identityContext,
|
|
223
|
+
continuityContext,
|
|
222
224
|
description ? `Description: ${description}` : '',
|
|
223
225
|
eventBlock ? `Events since last heartbeat:\n${eventBlock}` : '',
|
|
224
226
|
dynamicGoal
|
|
@@ -242,14 +244,6 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
|
|
|
242
244
|
].filter(Boolean).join('\n')
|
|
243
245
|
}
|
|
244
246
|
|
|
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
247
|
function resolveInterval(obj: Record<string, any>, currentSec: number): number {
|
|
254
248
|
// Prefer heartbeatInterval (duration string) over heartbeatIntervalSec (raw number)
|
|
255
249
|
if (obj.heartbeatInterval !== undefined && obj.heartbeatInterval !== null) {
|
|
@@ -377,8 +371,8 @@ async function tickHeartbeats() {
|
|
|
377
371
|
|
|
378
372
|
for (const session of Object.values(sessions) as any[]) {
|
|
379
373
|
if (!session?.id) continue
|
|
380
|
-
if (!Array.isArray(session.
|
|
381
|
-
if (session.sessionType && session.sessionType !== 'human'
|
|
374
|
+
if (!Array.isArray(session.plugins) || session.plugins.length === 0) continue
|
|
375
|
+
if (session.sessionType && session.sessionType !== 'human') continue
|
|
382
376
|
|
|
383
377
|
// Check if this session or its agent has explicit heartbeat opt-in
|
|
384
378
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
@@ -395,10 +389,6 @@ async function tickHeartbeats() {
|
|
|
395
389
|
const cfg = heartbeatConfigForSession(session, settings, agents)
|
|
396
390
|
if (!cfg.enabled) continue
|
|
397
391
|
|
|
398
|
-
// Apply momentum-based multiplier to heartbeat interval
|
|
399
|
-
const momentumScore = session.mainLoopState?.momentumScore ?? 40
|
|
400
|
-
cfg.intervalSec = applyMomentumMultiplier(cfg.intervalSec, momentumScore)
|
|
401
|
-
|
|
402
392
|
// For sessions with explicit opt-in, use a shorter idle threshold (just intervalSec * 2).
|
|
403
393
|
// For inherited/global heartbeats, keep the 180s minimum to avoid noisy auto-fire.
|
|
404
394
|
const defaultIdleSec = explicitOptIn
|
|
@@ -410,38 +400,22 @@ async function tickHeartbeats() {
|
|
|
410
400
|
const idleMs = now - lastUserAt
|
|
411
401
|
if (idleMs < userIdleThresholdSec * 1000) continue
|
|
412
402
|
|
|
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
403
|
const last = state.lastBySession.get(session.id) || 0
|
|
425
404
|
if (now - last < cfg.intervalSec * 1000) continue
|
|
426
405
|
|
|
427
406
|
const runState = getSessionRunState(session.id)
|
|
428
407
|
if (runState.runningRunId) continue
|
|
429
408
|
|
|
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)
|
|
409
|
+
const rawHeartbeatFileContent = readHeartbeatFile(session)
|
|
410
|
+
const heartbeatFileContent = isHeartbeatContentEffectivelyEmpty(rawHeartbeatFileContent) ? '' : rawHeartbeatFileContent
|
|
411
|
+
const hasGoal = !!(agent?.heartbeatGoal || agent?.description || agent?.systemPrompt || agent?.soul)
|
|
412
|
+
const hasCustomPrompt = cfg.prompt !== DEFAULT_HEARTBEAT_PROMPT
|
|
413
|
+
// Skip heartbeat only if there's truly nothing to drive it:
|
|
414
|
+
// no agent goal, no HEARTBEAT.md content, AND no custom prompt configured
|
|
415
|
+
if (!hasGoal && !heartbeatFileContent && !hasCustomPrompt) {
|
|
416
|
+
continue
|
|
444
417
|
}
|
|
418
|
+
const heartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent)
|
|
445
419
|
|
|
446
420
|
const enqueue = enqueueSessionRun({
|
|
447
421
|
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
|
+
})
|