@swarmclawai/swarmclaw 1.3.5 → 1.3.6
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 +5 -0
- package/package.json +3 -2
- package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
- package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
- package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +15 -34
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
- package/src/lib/server/knowledge-sources.test.ts +46 -0
- package/src/lib/server/knowledge-sources.ts +34 -16
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
package/README.md
CHANGED
|
@@ -204,6 +204,11 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
|
|
|
204
204
|
|
|
205
205
|
## Release Notes
|
|
206
206
|
|
|
207
|
+
### v1.3.6 Highlights
|
|
208
|
+
|
|
209
|
+
- **Knowledge hygiene visibility fix**: exact-duplicate archival now only applies when sources share the same visibility and origin fingerprint. Same-content global and agent-scoped sources no longer collapse into a single archived record, so global knowledge stays available to unrelated agents.
|
|
210
|
+
- **Release gate hardening**: the default test matrix now includes the 1.3.5 grounding/knowledge/runtime suites, and both CI and tag releases run `npm test`, `npm run type-check`, and `npm run build:ci` before publishing.
|
|
211
|
+
|
|
207
212
|
### v1.3.5 Highlights
|
|
208
213
|
|
|
209
214
|
- **Knowledge grounding & citations**: agent responses are now grounded against knowledge sources at retrieval time. Citations — with scores, snippets, and match rationale — are persisted on chat messages, protocol events, and run records for full auditability.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
|
|
63
63
|
"benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
|
|
64
64
|
"type-check": "tsc --noEmit",
|
|
65
|
-
"test": "npm run test:cli && npm run test:setup && npm run test:openclaw",
|
|
65
|
+
"test": "npm run test:cli && npm run test:setup && npm run test:openclaw && npm run test:runtime",
|
|
66
66
|
"format": "eslint --fix",
|
|
67
67
|
"lint": "eslint",
|
|
68
68
|
"lint:fix": "eslint --fix",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs",
|
|
73
73
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
|
|
74
74
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
75
|
+
"test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts",
|
|
75
76
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
76
77
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
77
78
|
"prepack": "npm run build:ci",
|
|
@@ -1,40 +1,23 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
2
|
import test from 'node:test'
|
|
3
|
+
import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cwd: repoRoot,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
5
|
+
function runWithTempDataDir<T = unknown>(script: string): T {
|
|
6
|
+
return runWithSharedTempDataDir<T>(script, {
|
|
7
|
+
prefix: 'swarmclaw-chat-disabled-',
|
|
8
|
+
dataDir: 'data',
|
|
9
|
+
browserProfilesDir: 'browser-profiles',
|
|
10
|
+
})
|
|
34
11
|
}
|
|
35
12
|
|
|
36
13
|
test('executeSessionChatTurn persists a visible error for disabled agents', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
14
|
+
const output = runWithTempDataDir<{
|
|
15
|
+
error: string | null
|
|
16
|
+
text: string | null
|
|
17
|
+
persisted: boolean
|
|
18
|
+
lastRole: string | null
|
|
19
|
+
lastText: string | null
|
|
20
|
+
}>(`
|
|
38
21
|
const storageMod = await import('@/lib/server/storage')
|
|
39
22
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
23
|
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
@@ -1,40 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
2
|
import test from 'node:test'
|
|
7
|
-
|
|
8
|
-
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
-
|
|
10
|
-
function runWithTempDataDir(script: string) {
|
|
11
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-eval-history-'))
|
|
12
|
-
try {
|
|
13
|
-
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
-
cwd: repoRoot,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
35
4
|
|
|
36
5
|
test('executeSessionChatTurn persists internal eval user turns for same-thread recall', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
recallText: string | null
|
|
8
|
+
roles: string[]
|
|
9
|
+
texts: string[]
|
|
10
|
+
}>(`
|
|
38
11
|
const storageMod = await import('@/lib/server/storage')
|
|
39
12
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
13
|
const providersMod = await import('@/lib/providers/index')
|
|
@@ -103,7 +76,11 @@ test('executeSessionChatTurn persists internal eval user turns for same-thread r
|
|
|
103
76
|
roles: storedSession.messages.map((entry) => entry.role),
|
|
104
77
|
texts: storedSession.messages.map((entry) => entry.text),
|
|
105
78
|
}))
|
|
106
|
-
|
|
79
|
+
`, {
|
|
80
|
+
prefix: 'swarmclaw-chat-eval-history-',
|
|
81
|
+
dataDir: 'data',
|
|
82
|
+
browserProfilesDir: 'browser-profiles',
|
|
83
|
+
})
|
|
107
84
|
|
|
108
85
|
assert.match(String(output.recallText || ''), /Sunbird/)
|
|
109
86
|
assert.deepEqual(output.roles, ['user', 'assistant', 'user', 'assistant'])
|
|
@@ -1,40 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
2
|
import test from 'node:test'
|
|
7
|
-
|
|
8
|
-
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
-
|
|
10
|
-
function runWithTempDataDir(script: string) {
|
|
11
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-grounding-'))
|
|
12
|
-
try {
|
|
13
|
-
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
-
cwd: repoRoot,
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
3
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
35
4
|
|
|
36
5
|
test('executeSessionChatTurn persists citations and retrieval traces on grounded assistant messages', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
6
|
+
const output = runWithTempDataDir<{
|
|
7
|
+
persisted: boolean
|
|
8
|
+
resultCitationCount: number
|
|
9
|
+
resultSelectorStatus: string | null
|
|
10
|
+
messageCitationCount: number
|
|
11
|
+
messageTraceHitCount: number
|
|
12
|
+
messageSelectorStatus: string | null
|
|
13
|
+
messageSourceTitle: string | null
|
|
14
|
+
}>(`
|
|
38
15
|
const storageMod = await import('@/lib/server/storage')
|
|
39
16
|
const providersMod = await import('@/lib/providers/index')
|
|
40
17
|
const threadMod = await import('@/lib/server/agents/agent-thread-session')
|
|
@@ -115,7 +92,11 @@ test('executeSessionChatTurn persists citations and retrieval traces on grounded
|
|
|
115
92
|
messageSelectorStatus: lastMessage?.retrievalTrace?.selectorStatus || null,
|
|
116
93
|
messageSourceTitle: lastMessage?.citations?.[0]?.sourceTitle || null,
|
|
117
94
|
}))
|
|
118
|
-
|
|
95
|
+
`, {
|
|
96
|
+
prefix: 'swarmclaw-chat-grounding-',
|
|
97
|
+
dataDir: 'data',
|
|
98
|
+
browserProfilesDir: 'browser-profiles',
|
|
99
|
+
})
|
|
119
100
|
|
|
120
101
|
assert.equal(output.persisted, true)
|
|
121
102
|
assert.equal(output.resultCitationCount >= 1, true)
|
|
@@ -1,40 +1,24 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
2
|
import test from 'node:test'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
env: {
|
|
16
|
-
...process.env,
|
|
17
|
-
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
-
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
-
BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
|
|
20
|
-
},
|
|
21
|
-
encoding: 'utf-8',
|
|
22
|
-
})
|
|
23
|
-
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
24
|
-
const lines = (result.stdout || '')
|
|
25
|
-
.trim()
|
|
26
|
-
.split('\n')
|
|
27
|
-
.map((line) => line.trim())
|
|
28
|
-
.filter(Boolean)
|
|
29
|
-
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
30
|
-
return JSON.parse(jsonLine || '{}')
|
|
31
|
-
} finally {
|
|
32
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
33
|
-
}
|
|
3
|
+
import { runWithTempDataDir as runWithSharedTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
4
|
+
|
|
5
|
+
function runWithTempDataDir<T = unknown>(script: string): T {
|
|
6
|
+
return runWithSharedTempDataDir<T>(script, {
|
|
7
|
+
prefix: 'swarmclaw-chat-session-sync-',
|
|
8
|
+
dataDir: 'data',
|
|
9
|
+
browserProfilesDir: 'browser-profiles',
|
|
10
|
+
})
|
|
34
11
|
}
|
|
35
12
|
|
|
36
13
|
test('executeSessionChatTurn syncs updated agent runtime fields onto its thread session', () => {
|
|
37
|
-
const output = runWithTempDataDir
|
|
14
|
+
const output = runWithTempDataDir<{
|
|
15
|
+
provider: string | null
|
|
16
|
+
model: string | null
|
|
17
|
+
extensions: string[]
|
|
18
|
+
heartbeatEnabled: boolean | null
|
|
19
|
+
heartbeatIntervalSec: number | null
|
|
20
|
+
connectorContext: Record<string, unknown> | null
|
|
21
|
+
}>(`
|
|
38
22
|
const storageMod = await import('@/lib/server/storage')
|
|
39
23
|
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
40
24
|
const providersMod = await import('@/lib/providers/index')
|
|
@@ -126,7 +110,12 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
|
|
|
126
110
|
})
|
|
127
111
|
|
|
128
112
|
test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thread history and clears stale connector state', () => {
|
|
129
|
-
const output = runWithTempDataDir
|
|
113
|
+
const output = runWithTempDataDir<{
|
|
114
|
+
connectorContext: Record<string, unknown> | null
|
|
115
|
+
messageCount: number
|
|
116
|
+
lastMessageText: string | null
|
|
117
|
+
heartbeatKinds: number
|
|
118
|
+
}>(`
|
|
130
119
|
const storageMod = await import('@/lib/server/storage')
|
|
131
120
|
const providersMod = await import('@/lib/providers/index')
|
|
132
121
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -232,7 +221,11 @@ test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thr
|
|
|
232
221
|
})
|
|
233
222
|
|
|
234
223
|
test('executeSessionChatTurn hides internal main-loop followup output from the visible transcript', () => {
|
|
235
|
-
const output = runWithTempDataDir
|
|
224
|
+
const output = runWithTempDataDir<{
|
|
225
|
+
messageCount: number
|
|
226
|
+
lastMessageText: string | null
|
|
227
|
+
hasStreamingArtifacts: boolean
|
|
228
|
+
}>(`
|
|
236
229
|
const storageMod = await import('@/lib/server/storage')
|
|
237
230
|
const providersMod = await import('@/lib/providers/index')
|
|
238
231
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -324,7 +317,10 @@ test('executeSessionChatTurn hides internal main-loop followup output from the v
|
|
|
324
317
|
})
|
|
325
318
|
|
|
326
319
|
test('executeSessionChatTurn forces external connector sessions onto session-scoped memory', () => {
|
|
327
|
-
const output = runWithTempDataDir
|
|
320
|
+
const output = runWithTempDataDir<{
|
|
321
|
+
memoryScopeMode: string | null
|
|
322
|
+
connectorContext: { isOwnerConversation?: boolean } | null
|
|
323
|
+
}>(`
|
|
328
324
|
const storageMod = await import('@/lib/server/storage')
|
|
329
325
|
const providersMod = await import('@/lib/providers/index')
|
|
330
326
|
const execMod = await import('@/lib/server/chat-execution/chat-execution')
|
|
@@ -418,7 +414,10 @@ test('executeSessionChatTurn forces external connector sessions onto session-sco
|
|
|
418
414
|
})
|
|
419
415
|
|
|
420
416
|
test('executeSessionChatTurn applies lifecycle hooks for model resolution and message persistence', () => {
|
|
421
|
-
const output = runWithTempDataDir
|
|
417
|
+
const output = runWithTempDataDir<{
|
|
418
|
+
lastMessageText: string
|
|
419
|
+
marks: string[]
|
|
420
|
+
}>(`
|
|
422
421
|
const storageMod = await import('@/lib/server/storage')
|
|
423
422
|
const providersMod = await import('@/lib/providers/index')
|
|
424
423
|
const extMod = await import('@/lib/server/extensions')
|
|
@@ -213,3 +213,49 @@ test('runKnowledgeHygieneMaintenance reindexes stale file sources and archives e
|
|
|
213
213
|
assert.ok(output.recentActionKinds.includes('archive'))
|
|
214
214
|
assert.ok(output.recentActionKinds.includes('reindex') || output.recentActionKinds.includes('sync'))
|
|
215
215
|
})
|
|
216
|
+
|
|
217
|
+
test('runKnowledgeHygieneMaintenance keeps same-content sources separate when visibility differs', () => {
|
|
218
|
+
const output = runWithTempDataDir<{
|
|
219
|
+
globalArchived: boolean
|
|
220
|
+
agentArchived: boolean
|
|
221
|
+
agent1Hits: number
|
|
222
|
+
agent2Hits: number
|
|
223
|
+
}>(`
|
|
224
|
+
const knowledgeMod = await import('./src/lib/server/knowledge-sources.ts')
|
|
225
|
+
const knowledge = knowledgeMod.default || knowledgeMod
|
|
226
|
+
|
|
227
|
+
const globalSource = await knowledge.createKnowledgeSource({
|
|
228
|
+
kind: 'manual',
|
|
229
|
+
title: 'Global Policy',
|
|
230
|
+
content: 'identical content',
|
|
231
|
+
scope: 'global',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const agentSource = await knowledge.createKnowledgeSource({
|
|
235
|
+
kind: 'manual',
|
|
236
|
+
title: 'Scoped Policy',
|
|
237
|
+
content: 'identical content',
|
|
238
|
+
scope: 'agent',
|
|
239
|
+
agentIds: ['agent-1'],
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
await knowledge.runKnowledgeHygieneMaintenance()
|
|
243
|
+
|
|
244
|
+
const globalDetail = await knowledge.getKnowledgeSourceDetail(globalSource.source.id)
|
|
245
|
+
const agentDetail = await knowledge.getKnowledgeSourceDetail(agentSource.source.id)
|
|
246
|
+
const agent1Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-1' })
|
|
247
|
+
const agent2Hits = await knowledge.searchKnowledgeHits({ query: 'identical', viewerAgentId: 'agent-2' })
|
|
248
|
+
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
globalArchived: !!globalDetail?.source?.archivedAt,
|
|
251
|
+
agentArchived: !!agentDetail?.source?.archivedAt,
|
|
252
|
+
agent1Hits: agent1Hits.length,
|
|
253
|
+
agent2Hits: agent2Hits.length,
|
|
254
|
+
}))
|
|
255
|
+
`, { prefix: 'swarmclaw-knowledge-visibility-' })
|
|
256
|
+
|
|
257
|
+
assert.equal(output.globalArchived, false)
|
|
258
|
+
assert.equal(output.agentArchived, false)
|
|
259
|
+
assert.equal(output.agent1Hits, 2)
|
|
260
|
+
assert.equal(output.agent2Hits, 1)
|
|
261
|
+
})
|
|
@@ -1056,6 +1056,38 @@ function sameSourceOrigin(left: KnowledgeSource, right: KnowledgeSource): boolea
|
|
|
1056
1056
|
return false
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
1059
|
+
function duplicateOriginFingerprint(source: KnowledgeSource): string {
|
|
1060
|
+
if (source.sourceUrl) return `url:${source.sourceUrl}`
|
|
1061
|
+
if (source.sourcePath) return `path:${source.sourcePath}`
|
|
1062
|
+
return `kind:${source.kind}`
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function duplicateGroupKey(source: KnowledgeSource): string | null {
|
|
1066
|
+
if (!source.sourceHash) return null
|
|
1067
|
+
const sortedAgentIds = [...source.agentIds].sort()
|
|
1068
|
+
const sortedTags = [...source.tags].map((tag) => tag.toLowerCase()).sort()
|
|
1069
|
+
return [
|
|
1070
|
+
source.sourceHash,
|
|
1071
|
+
source.kind,
|
|
1072
|
+
source.scope,
|
|
1073
|
+
sortedAgentIds.join(','),
|
|
1074
|
+
sortedTags.join(','),
|
|
1075
|
+
duplicateOriginFingerprint(source),
|
|
1076
|
+
].join('|')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function collectDuplicateGroups(sources: KnowledgeSource[]): Map<string, KnowledgeSource[]> {
|
|
1080
|
+
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1081
|
+
for (const source of sources) {
|
|
1082
|
+
const groupKey = duplicateGroupKey(source)
|
|
1083
|
+
if (!groupKey) continue
|
|
1084
|
+
const group = duplicateGroups.get(groupKey) || []
|
|
1085
|
+
group.push(source)
|
|
1086
|
+
duplicateGroups.set(groupKey, group)
|
|
1087
|
+
}
|
|
1088
|
+
return duplicateGroups
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1059
1091
|
function canonicalSourceForGroup(group: KnowledgeSource[]): KnowledgeSource {
|
|
1060
1092
|
return [...group].sort((left, right) => {
|
|
1061
1093
|
const archiveDelta = Number(sourceIsExcludedByDefault(left)) - Number(sourceIsExcludedByDefault(right))
|
|
@@ -1072,14 +1104,7 @@ function buildHygieneSummary(sources: KnowledgeSource[]): KnowledgeHygieneSummar
|
|
|
1072
1104
|
const pushFinding = (finding: KnowledgeHygieneFinding) => {
|
|
1073
1105
|
if (findings.length < MAX_HYGIENE_FINDINGS) findings.push(finding)
|
|
1074
1106
|
}
|
|
1075
|
-
|
|
1076
|
-
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1077
|
-
for (const source of sources) {
|
|
1078
|
-
if (!source.sourceHash) continue
|
|
1079
|
-
const group = duplicateGroups.get(source.sourceHash) || []
|
|
1080
|
-
group.push(source)
|
|
1081
|
-
duplicateGroups.set(source.sourceHash, group)
|
|
1082
|
-
}
|
|
1107
|
+
const duplicateGroups = collectDuplicateGroups(sources)
|
|
1083
1108
|
|
|
1084
1109
|
for (const source of sources) {
|
|
1085
1110
|
if (sourceIsArchived(source)) {
|
|
@@ -1185,14 +1210,7 @@ export async function getKnowledgeHygieneSummary(): Promise<KnowledgeHygieneSumm
|
|
|
1185
1210
|
export async function runKnowledgeHygieneMaintenance(): Promise<KnowledgeHygieneSummary> {
|
|
1186
1211
|
await ensureLegacyKnowledgeBackfill()
|
|
1187
1212
|
const sources = listStoredSources()
|
|
1188
|
-
|
|
1189
|
-
const duplicateGroups = new Map<string, KnowledgeSource[]>()
|
|
1190
|
-
for (const source of sources) {
|
|
1191
|
-
if (!source.sourceHash) continue
|
|
1192
|
-
const group = duplicateGroups.get(source.sourceHash) || []
|
|
1193
|
-
group.push(source)
|
|
1194
|
-
duplicateGroups.set(source.sourceHash, group)
|
|
1195
|
-
}
|
|
1213
|
+
const duplicateGroups = collectDuplicateGroups(sources)
|
|
1196
1214
|
|
|
1197
1215
|
for (const source of sources) {
|
|
1198
1216
|
if (sourceIsExcludedByDefault(source)) continue
|
|
@@ -10,19 +10,32 @@ export function runWithTempDataDir<T = unknown>(
|
|
|
10
10
|
script: string,
|
|
11
11
|
options: {
|
|
12
12
|
prefix?: string
|
|
13
|
+
dataDir?: string
|
|
14
|
+
workspaceDir?: string
|
|
15
|
+
browserProfilesDir?: string
|
|
13
16
|
} = {},
|
|
14
17
|
): T {
|
|
15
18
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), options.prefix || 'swarmclaw-test-'))
|
|
16
|
-
const
|
|
19
|
+
const resolveTempPath = (value: string | undefined, fallback: string): string =>
|
|
20
|
+
path.isAbsolute(value || '') ? String(value) : path.join(tempDir, value || fallback)
|
|
21
|
+
const dataDir = resolveTempPath(options.dataDir, '')
|
|
22
|
+
const workspaceDir = resolveTempPath(options.workspaceDir, 'workspace')
|
|
23
|
+
const browserProfilesDir = options.browserProfilesDir
|
|
24
|
+
? resolveTempPath(options.browserProfilesDir, 'browser-profiles')
|
|
25
|
+
: null
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
17
28
|
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
29
|
+
if (browserProfilesDir) fs.mkdirSync(browserProfilesDir, { recursive: true })
|
|
18
30
|
|
|
19
31
|
try {
|
|
20
32
|
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
21
33
|
cwd: repoRoot,
|
|
22
34
|
env: {
|
|
23
35
|
...process.env,
|
|
24
|
-
DATA_DIR:
|
|
36
|
+
DATA_DIR: dataDir,
|
|
25
37
|
WORKSPACE_DIR: workspaceDir,
|
|
38
|
+
...(browserProfilesDir ? { BROWSER_PROFILES_DIR: browserProfilesDir } : {}),
|
|
26
39
|
},
|
|
27
40
|
encoding: 'utf-8',
|
|
28
41
|
})
|