@swarmclawai/swarmclaw 1.2.4 → 1.2.5
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 +14 -0
- package/bin/daemon-cmd.js +169 -0
- package/bin/server-cmd.js +3 -0
- package/bin/swarmclaw.js +11 -0
- package/package.json +17 -16
- package/src/app/api/agents/[id]/clone/route.ts +3 -32
- package/src/app/api/agents/[id]/route.ts +6 -158
- package/src/app/api/agents/[id]/status/route.ts +2 -3
- package/src/app/api/agents/[id]/thread/route.ts +4 -17
- package/src/app/api/agents/bulk/route.ts +5 -47
- package/src/app/api/agents/route.ts +5 -119
- package/src/app/api/agents/trash/route.ts +13 -24
- package/src/app/api/auth/route.ts +3 -9
- package/src/app/api/autonomy/estop/route.ts +5 -5
- package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
- package/src/app/api/chatrooms/[id]/route.ts +23 -2
- package/src/app/api/chatrooms/route.ts +13 -2
- package/src/app/api/chats/[id]/clear/route.ts +2 -13
- package/src/app/api/chats/[id]/deploy/route.ts +2 -3
- package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
- package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
- package/src/app/api/chats/[id]/queue/route.ts +17 -64
- package/src/app/api/chats/[id]/retry/route.ts +4 -22
- package/src/app/api/chats/[id]/route.ts +10 -138
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/migrate-messages/route.ts +7 -0
- package/src/app/api/chats/route.ts +13 -134
- package/src/app/api/connectors/[id]/access/route.ts +12 -229
- package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
- package/src/app/api/connectors/[id]/health/route.ts +12 -39
- package/src/app/api/connectors/[id]/route.ts +14 -122
- package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
- package/src/app/api/connectors/doctor/route.ts +1 -1
- package/src/app/api/connectors/route.ts +12 -70
- package/src/app/api/credentials/[id]/route.ts +2 -4
- package/src/app/api/credentials/route.ts +10 -19
- package/src/app/api/daemon/health-check/route.ts +3 -4
- package/src/app/api/daemon/route.ts +10 -8
- package/src/app/api/documents/route.ts +11 -10
- package/src/app/api/external-agents/route.ts +3 -3
- package/src/app/api/gateways/[id]/health/route.ts +2 -3
- package/src/app/api/gateways/[id]/route.ts +7 -122
- package/src/app/api/gateways/route.ts +3 -103
- package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
- package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
- package/src/app/api/openclaw/directory/route.ts +2 -2
- package/src/app/api/openclaw/history/route.ts +3 -5
- package/src/app/api/providers/[id]/route.test.ts +49 -0
- package/src/app/api/providers/ollama/route.ts +6 -5
- package/src/app/api/schedules/[id]/route.ts +14 -108
- package/src/app/api/schedules/[id]/run/route.ts +6 -67
- package/src/app/api/schedules/route.ts +9 -51
- package/src/app/api/settings/route.ts +4 -3
- package/src/app/api/setup/check-provider/route.ts +15 -1
- package/src/app/api/setup/openclaw-device/route.ts +2 -2
- package/src/app/api/system/status/route.ts +2 -2
- package/src/app/api/tasks/[id]/route.ts +16 -202
- package/src/app/api/tasks/bulk/route.ts +5 -86
- package/src/app/api/tasks/metrics/route.ts +2 -1
- package/src/app/api/tasks/route.ts +11 -171
- package/src/app/api/upload/route.ts +1 -1
- package/src/app/api/uploads/[filename]/route.ts +1 -1
- package/src/app/api/uploads/route.ts +1 -1
- package/src/app/api/webhooks/[id]/history/route.ts +2 -2
- package/src/app/layout.tsx +9 -6
- package/src/app/protocols/page.tsx +71 -89
- package/src/app/tasks/page.tsx +32 -32
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/auth/setup-wizard/index.tsx +4 -4
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
- package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
- package/src/components/auth/setup-wizard/utils.ts +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
- package/src/components/connectors/connector-list.tsx +26 -40
- package/src/components/connectors/connector-sheet.tsx +95 -149
- package/src/components/gateways/gateway-sheet.tsx +61 -110
- package/src/components/layout/live-query-sync.tsx +121 -0
- package/src/components/protocols/structured-session-launcher.tsx +24 -45
- package/src/components/providers/app-query-provider.tsx +17 -0
- package/src/components/providers/provider-list.tsx +60 -61
- package/src/components/providers/provider-sheet.tsx +74 -56
- package/src/components/skills/skill-list.tsx +5 -18
- package/src/components/skills/skill-sheet.tsx +21 -20
- package/src/components/skills/skills-workspace.tsx +48 -87
- package/src/components/tasks/task-card.tsx +20 -13
- package/src/components/tasks/task-column.tsx +22 -7
- package/src/components/tasks/task-list.tsx +8 -11
- package/src/components/tasks/task-sheet.tsx +111 -103
- package/src/features/agents/queries.ts +20 -0
- package/src/features/chatrooms/queries.ts +20 -0
- package/src/features/chats/queries.ts +27 -0
- package/src/features/connectors/queries.ts +145 -0
- package/src/features/credentials/queries.ts +37 -0
- package/src/features/extensions/queries.ts +26 -0
- package/src/features/external-agents/queries.ts +36 -0
- package/src/features/gateways/queries.ts +274 -0
- package/src/features/missions/queries.ts +23 -0
- package/src/features/projects/queries.ts +20 -0
- package/src/features/protocols/queries.ts +149 -0
- package/src/features/providers/queries.ts +142 -0
- package/src/features/settings/queries.ts +20 -0
- package/src/features/skills/queries.ts +182 -0
- package/src/features/tasks/queries.ts +189 -0
- package/src/hooks/use-ws.ts +3 -2
- package/src/lib/app/api-client.ts +2 -2
- package/src/lib/query/client.ts +17 -0
- package/src/lib/server/agents/agent-runtime-config.ts +1 -1
- package/src/lib/server/agents/agent-service.ts +429 -0
- package/src/lib/server/agents/agent-thread-session.ts +6 -5
- package/src/lib/server/agents/autonomy-contract.ts +1 -4
- package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
- package/src/lib/server/agents/delegation-advisory.ts +251 -0
- package/src/lib/server/agents/main-agent-loop.ts +98 -40
- package/src/lib/server/agents/subagent-runtime.ts +12 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
- package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
- package/src/lib/server/build-llm.ts +7 -15
- package/src/lib/server/capability-router.test.ts +70 -1
- package/src/lib/server/capability-router.ts +24 -99
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
- package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
- package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
- package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
- package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
- package/src/lib/server/chat-execution/message-classifier.ts +74 -32
- package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
- package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
- package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
- package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
- package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
- package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
- package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
- package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
- package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
- package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
- package/src/lib/server/chats/chat-session-service.ts +410 -0
- package/src/lib/server/connectors/access.ts +1 -1
- package/src/lib/server/connectors/commands.ts +7 -6
- package/src/lib/server/connectors/connector-inbound.ts +14 -7
- package/src/lib/server/connectors/connector-outbound.ts +16 -11
- package/src/lib/server/connectors/connector-service.ts +453 -0
- package/src/lib/server/connectors/delivery.ts +17 -12
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
- package/src/lib/server/connectors/media.ts +1 -1
- package/src/lib/server/connectors/response-media.ts +1 -1
- package/src/lib/server/connectors/session-consolidation.ts +11 -7
- package/src/lib/server/connectors/session.ts +9 -7
- package/src/lib/server/connectors/voice-note.ts +2 -1
- package/src/lib/server/context-manager.ts +20 -1
- package/src/lib/server/cost.ts +2 -3
- package/src/lib/server/credentials/credential-repository.ts +43 -4
- package/src/lib/server/credentials/credential-service.ts +112 -0
- package/src/lib/server/daemon/admin-metadata.ts +64 -0
- package/src/lib/server/daemon/controller.ts +577 -0
- package/src/lib/server/daemon/daemon-runtime.ts +352 -0
- package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
- package/src/lib/server/daemon/types.ts +101 -0
- package/src/lib/server/embeddings.ts +3 -9
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -2
- package/src/lib/server/execution-brief.test.ts +167 -0
- package/src/lib/server/execution-brief.ts +295 -0
- package/src/lib/server/execution-engine/chat-turn.ts +9 -0
- package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
- package/src/lib/server/execution-engine/index.ts +35 -0
- package/src/lib/server/execution-engine/task-attempt.ts +303 -0
- package/src/lib/server/execution-engine/types.ts +33 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
- package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
- package/src/lib/server/memory/session-archive-memory.ts +12 -10
- package/src/lib/server/messages/message-repository.ts +330 -0
- package/src/lib/server/missions/mission-service/core.ts +8 -6
- package/src/lib/server/openclaw/agent-resolver.ts +2 -3
- package/src/lib/server/openclaw/doctor.ts +1 -1
- package/src/lib/server/openclaw/gateway.test.ts +10 -1
- package/src/lib/server/openclaw/gateway.ts +5 -14
- package/src/lib/server/openclaw/health.ts +3 -11
- package/src/lib/server/openclaw/sync.ts +8 -6
- package/src/lib/server/persistence/storage-context.ts +3 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
- package/src/lib/server/protocols/protocol-normalization.ts +1 -1
- package/src/lib/server/protocols/protocol-queries.ts +13 -7
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
- package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
- package/src/lib/server/protocols/protocol-swarm.ts +8 -8
- package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
- package/src/lib/server/protocols/protocol-templates.ts +4 -2
- package/src/lib/server/protocols/protocol-types.ts +10 -7
- package/src/lib/server/provider-endpoint.ts +7 -12
- package/src/lib/server/provider-model-discovery.ts +2 -11
- package/src/lib/server/query-expansion.ts +5 -6
- package/src/lib/server/run-context.test.ts +365 -0
- package/src/lib/server/run-context.ts +367 -0
- package/src/lib/server/runtime/heartbeat-service.ts +7 -5
- package/src/lib/server/runtime/queue/core.ts +61 -190
- package/src/lib/server/runtime/run-ledger.ts +8 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
- package/src/lib/server/schedules/schedule-route-service.ts +230 -0
- package/src/lib/server/service-result.ts +16 -0
- package/src/lib/server/session-note.ts +2 -3
- package/src/lib/server/session-reset-policy.ts +4 -3
- package/src/lib/server/session-tools/connector.ts +9 -6
- package/src/lib/server/session-tools/context-mgmt.ts +58 -9
- package/src/lib/server/session-tools/crud.ts +162 -10
- package/src/lib/server/session-tools/delegate.ts +1 -1
- package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
- package/src/lib/server/session-tools/memory.ts +6 -4
- package/src/lib/server/session-tools/session-info.test.ts +56 -0
- package/src/lib/server/session-tools/session-info.ts +119 -12
- package/src/lib/server/session-tools/skill-runtime.ts +3 -1
- package/src/lib/server/session-tools/skills.ts +15 -15
- package/src/lib/server/session-tools/subagent.test.ts +115 -1
- package/src/lib/server/session-tools/subagent.ts +125 -7
- package/src/lib/server/session-tools/team-context.ts +4 -3
- package/src/lib/server/session-tools/wallet.ts +0 -58
- package/src/lib/server/sessions/session-lineage.ts +55 -0
- package/src/lib/server/sessions/session-repository.ts +2 -2
- package/src/lib/server/skills/learned-skills.ts +24 -23
- package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
- package/src/lib/server/skills/skill-repository.ts +136 -13
- package/src/lib/server/skills/skill-suggestions.ts +25 -28
- package/src/lib/server/storage-normalization.test.ts +44 -267
- package/src/lib/server/storage-normalization.ts +75 -0
- package/src/lib/server/storage.ts +19 -0
- package/src/lib/server/structured-extract.ts +3 -14
- package/src/lib/server/tasks/task-followups.ts +16 -11
- package/src/lib/server/tasks/task-result.test.ts +25 -29
- package/src/lib/server/tasks/task-result.ts +5 -9
- package/src/lib/server/tasks/task-route-service.ts +449 -0
- package/src/lib/server/text-normalization.ts +41 -0
- package/src/lib/server/tool-planning.ts +6 -42
- package/src/lib/server/upload-path.ts +5 -0
- package/src/lib/server/working-state/extraction.ts +614 -0
- package/src/lib/server/working-state/normalization.ts +866 -0
- package/src/lib/server/working-state/prompt.ts +60 -0
- package/src/lib/server/working-state/repository.ts +38 -0
- package/src/lib/server/working-state/service.test.ts +253 -0
- package/src/lib/server/working-state/service.ts +293 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/ws-client.ts +3 -3
- package/src/stores/slices/task-slice.ts +1 -4
- package/src/stores/use-chatroom-store.ts +2 -2
- package/src/types/index.ts +277 -12
|
@@ -3,11 +3,14 @@ import path from 'node:path'
|
|
|
3
3
|
import type { BoardTask, Connector, MessageToolEvent } from '@/types'
|
|
4
4
|
import { normalizeWhatsappTarget } from '@/lib/server/connectors/response-media'
|
|
5
5
|
import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
|
|
6
|
+
import { loadConnectors } from '@/lib/server/connectors/connector-repository'
|
|
6
7
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
7
|
-
import {
|
|
8
|
+
import { loadSessions } from '@/lib/server/sessions/session-repository'
|
|
9
|
+
import { UPLOAD_DIR } from '@/lib/server/upload-path'
|
|
8
10
|
import { errorMessage } from '@/lib/shared-utils'
|
|
9
11
|
import { isMainSession } from '@/lib/server/agents/main-agent-loop'
|
|
10
12
|
import { log } from '@/lib/server/logger'
|
|
13
|
+
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
11
14
|
|
|
12
15
|
const TAG = 'task-followups'
|
|
13
16
|
|
|
@@ -291,9 +294,10 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
291
294
|
const ownerSessionTarget = resolveMainSessionOwnerTarget()
|
|
292
295
|
if (ownerSessionTarget) return ownerSessionTarget
|
|
293
296
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
const sourceMessages = typeof sourceSession.id === 'string' ? getMessages(sourceSession.id) : []
|
|
298
|
+
if (!isMainSession(sourceSession) && sourceMessages.length > 0) {
|
|
299
|
+
for (let index = sourceMessages.length - 1; index >= 0; index -= 1) {
|
|
300
|
+
const message = sourceMessages[index]
|
|
297
301
|
if (!message || message.role !== 'user') continue
|
|
298
302
|
if (message.historyExcluded === true) continue
|
|
299
303
|
|
|
@@ -408,14 +412,15 @@ export function taskAlreadyDeliveredToConnectorTarget(params: {
|
|
|
408
412
|
: ''
|
|
409
413
|
if (!taskSessionId) return false
|
|
410
414
|
const session = params.sessions[taskSessionId]
|
|
411
|
-
if (!session
|
|
415
|
+
if (!session) return false
|
|
412
416
|
|
|
417
|
+
const sessionMessages = typeof session.id === 'string' ? getMessages(session.id) : []
|
|
413
418
|
const connector = params.connectors[params.target.connectorId]
|
|
414
419
|
const normalizedTargetChannel = normalizeFollowupChannelForConnector(connector, params.target.channelId)
|
|
415
420
|
if (!normalizedTargetChannel) return false
|
|
416
421
|
|
|
417
|
-
for (let index =
|
|
418
|
-
const message =
|
|
422
|
+
for (let index = sessionMessages.length - 1; index >= 0; index -= 1) {
|
|
423
|
+
const message = sessionMessages[index]
|
|
419
424
|
if (!message || message.role !== 'assistant' || !Array.isArray(message.toolEvents)) continue
|
|
420
425
|
for (const event of message.toolEvents) {
|
|
421
426
|
const delivered = extractDeliveredConnectorTarget(event as MessageToolEvent)
|
|
@@ -446,14 +451,14 @@ export async function notifyConnectorTaskFollowups(params: {
|
|
|
446
451
|
const targets = collectTaskConnectorFollowupTargets({
|
|
447
452
|
task,
|
|
448
453
|
sessions: sessions as Record<string, SessionLike>,
|
|
449
|
-
connectors
|
|
454
|
+
connectors,
|
|
450
455
|
running: running as RunningConnectorLike[],
|
|
451
456
|
})
|
|
452
457
|
if (!targets.length) return
|
|
453
458
|
const originTarget = resolveTaskOriginConnectorFollowupTarget({
|
|
454
459
|
task,
|
|
455
460
|
sessions: sessions as Record<string, SessionLike>,
|
|
456
|
-
connectors
|
|
461
|
+
connectors,
|
|
457
462
|
running: running as RunningConnectorLike[],
|
|
458
463
|
})
|
|
459
464
|
const preferredTargetKey = originTarget
|
|
@@ -473,8 +478,8 @@ export async function notifyConnectorTaskFollowups(params: {
|
|
|
473
478
|
continue
|
|
474
479
|
}
|
|
475
480
|
|
|
476
|
-
const template = typeof
|
|
477
|
-
?
|
|
481
|
+
const template = typeof connector.config?.taskFollowupTemplate === 'string'
|
|
482
|
+
? connector.config.taskFollowupTemplate.trim()
|
|
478
483
|
: ''
|
|
479
484
|
const message = template
|
|
480
485
|
? fillTaskFollowupTemplate(template, {
|
|
@@ -4,41 +4,37 @@ import { extractTaskResult } from '@/lib/server/tasks/task-result'
|
|
|
4
4
|
|
|
5
5
|
describe('extractTaskResult', () => {
|
|
6
6
|
it('limits artifact extraction to messages from the current run window', () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
],
|
|
20
|
-
}
|
|
7
|
+
const messages = [
|
|
8
|
+
{
|
|
9
|
+
role: 'assistant',
|
|
10
|
+
time: 1_000,
|
|
11
|
+
text: 'old run artifact: /api/uploads/wiki-old.png',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
role: 'assistant',
|
|
15
|
+
time: 2_000,
|
|
16
|
+
text: 'new run artifact: /api/uploads/wiki-new.png',
|
|
17
|
+
},
|
|
18
|
+
]
|
|
21
19
|
|
|
22
|
-
const result = extractTaskResult(
|
|
20
|
+
const result = extractTaskResult(messages, 'done', { sinceTime: 1_500 })
|
|
23
21
|
assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/wiki-new.png'])
|
|
24
22
|
})
|
|
25
23
|
|
|
26
24
|
it('excludes messages without timestamps when sinceTime is provided', () => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
],
|
|
39
|
-
}
|
|
25
|
+
const messages = [
|
|
26
|
+
{
|
|
27
|
+
role: 'assistant',
|
|
28
|
+
text: 'undated artifact: /api/uploads/undated.png',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
role: 'assistant',
|
|
32
|
+
time: 5_000,
|
|
33
|
+
text: 'dated artifact: /api/uploads/dated.png',
|
|
34
|
+
},
|
|
35
|
+
]
|
|
40
36
|
|
|
41
|
-
const result = extractTaskResult(
|
|
37
|
+
const result = extractTaskResult(messages, 'done', { sinceTime: 4_000 })
|
|
42
38
|
assert.deepEqual(result.artifacts.map((a) => a.url), ['/api/uploads/dated.png'])
|
|
43
39
|
})
|
|
44
40
|
})
|
|
@@ -49,10 +49,6 @@ interface MessageLike {
|
|
|
49
49
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
interface SessionLike {
|
|
53
|
-
messages?: MessageLike[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
interface ExtractTaskResultOptions {
|
|
57
53
|
sinceTime?: number | null
|
|
58
54
|
}
|
|
@@ -62,12 +58,12 @@ interface ExtractTaskResultOptions {
|
|
|
62
58
|
// ---------------------------------------------------------------------------
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
|
-
* Walk
|
|
61
|
+
* Walk messages and extract all artifacts + a clean summary.
|
|
66
62
|
* Replaces the old regex-based `extractLatestUploadUrl` and
|
|
67
63
|
* `summarizeScheduleTaskResult` with a single Zod-validated pass.
|
|
68
64
|
*/
|
|
69
65
|
export function extractTaskResult(
|
|
70
|
-
|
|
66
|
+
messages: MessageLike[] | null | undefined,
|
|
71
67
|
rawResultText: string | null | undefined,
|
|
72
68
|
options?: ExtractTaskResultOptions,
|
|
73
69
|
): TaskResult {
|
|
@@ -85,9 +81,9 @@ export function extractTaskResult(
|
|
|
85
81
|
artifacts.push({ url, type: classifyArtifact(filename), filename })
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
// Walk
|
|
89
|
-
if (Array.isArray(
|
|
90
|
-
for (const msg of
|
|
84
|
+
// Walk messages to collect all artifact URLs
|
|
85
|
+
if (Array.isArray(messages)) {
|
|
86
|
+
for (const msg of messages) {
|
|
91
87
|
if (sinceTime !== null) {
|
|
92
88
|
const msgTime = typeof msg.time === 'number' && Number.isFinite(msg.time) ? msg.time : null
|
|
93
89
|
if (msgTime === null || msgTime < sinceTime) continue
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import { getEnabledCapabilityIds } from '@/lib/capability-selection'
|
|
3
|
+
import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
|
|
4
|
+
import { loadAgents } from '@/lib/server/agents/agent-repository'
|
|
5
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
6
|
+
import { createNotification } from '@/lib/server/create-notification'
|
|
7
|
+
import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
|
|
8
|
+
import { getExtensionManager } from '@/lib/server/extensions'
|
|
9
|
+
import {
|
|
10
|
+
enrichTaskWithMissionSummary,
|
|
11
|
+
ensureMissionForTask,
|
|
12
|
+
noteMissionTaskFinished,
|
|
13
|
+
} from '@/lib/server/missions/mission-service'
|
|
14
|
+
import {
|
|
15
|
+
disableSessionHeartbeat,
|
|
16
|
+
enqueueTask,
|
|
17
|
+
recoverStalledRunningTasks,
|
|
18
|
+
validateCompletedTasksQueue,
|
|
19
|
+
} from '@/lib/server/runtime/queue'
|
|
20
|
+
import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
|
|
21
|
+
import { serviceFail, serviceOk } from '@/lib/server/service-result'
|
|
22
|
+
import { loadSettings } from '@/lib/server/settings/settings-repository'
|
|
23
|
+
import {
|
|
24
|
+
deleteTask,
|
|
25
|
+
loadTask,
|
|
26
|
+
loadTasks,
|
|
27
|
+
saveTask,
|
|
28
|
+
saveTaskMany,
|
|
29
|
+
} from '@/lib/server/tasks/task-repository'
|
|
30
|
+
import { resolveTaskAgentFromDescription } from '@/lib/server/tasks/task-mention'
|
|
31
|
+
import { applyTaskPatch, prepareTaskCreation } from '@/lib/server/tasks/task-service'
|
|
32
|
+
import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
33
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
34
|
+
import type { BoardTask, BoardTaskStatus, TaskComment } from '@/types'
|
|
35
|
+
import type { ServiceResult } from '@/lib/server/service-result'
|
|
36
|
+
|
|
37
|
+
import '@/lib/server/builtin-extensions'
|
|
38
|
+
|
|
39
|
+
const VALID_BULK_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
|
|
40
|
+
|
|
41
|
+
function normalizeTaskCommentInput(value: unknown): TaskComment | null {
|
|
42
|
+
if (typeof value === 'string' && value.trim()) {
|
|
43
|
+
return {
|
|
44
|
+
id: genId(),
|
|
45
|
+
author: 'user',
|
|
46
|
+
text: value.trim(),
|
|
47
|
+
createdAt: Date.now(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
51
|
+
const row = value as Record<string, unknown>
|
|
52
|
+
const text = typeof row.text === 'string' ? row.text.trim() : ''
|
|
53
|
+
if (!text) return null
|
|
54
|
+
return {
|
|
55
|
+
id: typeof row.id === 'string' && row.id.trim() ? row.id.trim() : genId(),
|
|
56
|
+
author: typeof row.author === 'string' && row.author.trim() ? row.author.trim() : 'user',
|
|
57
|
+
agentId: typeof row.agentId === 'string' && row.agentId.trim() ? row.agentId.trim() : undefined,
|
|
58
|
+
text,
|
|
59
|
+
createdAt: typeof row.createdAt === 'number' && Number.isFinite(row.createdAt) ? row.createdAt : Date.now(),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function prepareTasksForListing() {
|
|
64
|
+
validateCompletedTasksQueue()
|
|
65
|
+
recoverStalledRunningTasks()
|
|
66
|
+
const allTasks = loadTasks()
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
Object.entries(allTasks).map(([id, task]) => [id, enrichTaskWithMissionSummary(task)]),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateTaskFromRoute(id: string, body: Record<string, unknown>): ServiceResult<BoardTask> {
|
|
73
|
+
const settings = loadSettings()
|
|
74
|
+
const tasks = loadTasks()
|
|
75
|
+
if (!tasks[id]) return serviceFail(404, 'Task not found')
|
|
76
|
+
|
|
77
|
+
const prevStatus = tasks[id].status
|
|
78
|
+
if (Array.isArray(body.blockedBy)) {
|
|
79
|
+
const dagResult = validateDag(tasks, id, body.blockedBy)
|
|
80
|
+
if (!dagResult.valid) {
|
|
81
|
+
return serviceFail(400, 'Dependency cycle detected')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (body.appendComment) {
|
|
86
|
+
const appendedComment = normalizeTaskCommentInput(body.appendComment)
|
|
87
|
+
if (!appendedComment) {
|
|
88
|
+
return serviceFail(400, 'Invalid task comment payload')
|
|
89
|
+
}
|
|
90
|
+
if (!tasks[id].comments) tasks[id].comments = []
|
|
91
|
+
tasks[id].comments.push(appendedComment)
|
|
92
|
+
tasks[id].updatedAt = Date.now()
|
|
93
|
+
} else {
|
|
94
|
+
applyTaskPatch({
|
|
95
|
+
task: tasks[id],
|
|
96
|
+
patch: body,
|
|
97
|
+
now: Date.now(),
|
|
98
|
+
settings,
|
|
99
|
+
preserveCompletedAt: true,
|
|
100
|
+
clearProjectIdWhenNull: true,
|
|
101
|
+
invalidCompletionCommentAuthor: 'System',
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
tasks[id].id = id
|
|
105
|
+
|
|
106
|
+
if (typeof body.parentTaskId === 'string' || body.parentTaskId === null) {
|
|
107
|
+
const oldParentId = tasks[id].parentTaskId
|
|
108
|
+
const newParentId = typeof body.parentTaskId === 'string' && body.parentTaskId.trim() ? body.parentTaskId.trim() : null
|
|
109
|
+
if (oldParentId && oldParentId !== newParentId && tasks[oldParentId]) {
|
|
110
|
+
const oldSubs = Array.isArray(tasks[oldParentId].subtaskIds) ? tasks[oldParentId].subtaskIds : []
|
|
111
|
+
tasks[oldParentId].subtaskIds = oldSubs.filter((s: string) => s !== id)
|
|
112
|
+
tasks[oldParentId].updatedAt = Date.now()
|
|
113
|
+
saveTask(oldParentId, tasks[oldParentId])
|
|
114
|
+
}
|
|
115
|
+
if (newParentId && tasks[newParentId]) {
|
|
116
|
+
const newSubs = Array.isArray(tasks[newParentId].subtaskIds) ? tasks[newParentId].subtaskIds : []
|
|
117
|
+
if (!newSubs.includes(id)) {
|
|
118
|
+
tasks[newParentId].subtaskIds = [...newSubs, id]
|
|
119
|
+
tasks[newParentId].updatedAt = Date.now()
|
|
120
|
+
saveTask(newParentId, tasks[newParentId])
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
tasks[id].parentTaskId = newParentId
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (prevStatus !== 'archived' && tasks[id].status === 'archived') {
|
|
127
|
+
tasks[id].archivedAt = Date.now()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
saveTask(id, tasks[id])
|
|
131
|
+
const mission = ensureMissionForTask(tasks[id], { source: 'manual' })
|
|
132
|
+
if (tasks[id].status === 'completed' || tasks[id].status === 'failed' || tasks[id].status === 'cancelled') {
|
|
133
|
+
noteMissionTaskFinished(tasks[id], tasks[id].status, tasks[id].id)
|
|
134
|
+
}
|
|
135
|
+
logActivity({ entityType: 'task', entityId: id, action: 'updated', actor: 'user', summary: `Task updated: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})` })
|
|
136
|
+
if (prevStatus !== tasks[id].status) {
|
|
137
|
+
pushMainLoopEventToMainSessions({
|
|
138
|
+
type: 'task_status_changed',
|
|
139
|
+
text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (prevStatus !== tasks[id].status && tasks[id].status === 'cancelled') {
|
|
144
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
145
|
+
notify('tasks')
|
|
146
|
+
return serviceOk(enrichTaskWithMissionSummary({
|
|
147
|
+
...tasks[id],
|
|
148
|
+
missionId: mission?.id || tasks[id].missionId || null,
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (prevStatus !== tasks[id].status && (tasks[id].status === 'completed' || tasks[id].status === 'failed')) {
|
|
153
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
154
|
+
createNotification({
|
|
155
|
+
type: tasks[id].status === 'completed' ? 'success' : 'error',
|
|
156
|
+
title: `Task ${tasks[id].status}: "${tasks[id].title}"`,
|
|
157
|
+
message: tasks[id].status === 'failed' ? tasks[id].error?.slice(0, 200) : undefined,
|
|
158
|
+
entityType: 'task',
|
|
159
|
+
entityId: id,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (tasks[id].status === 'completed') {
|
|
163
|
+
const agentExtensions = tasks[id].agentId ? getEnabledCapabilityIds(loadAgents()[tasks[id].agentId]) : []
|
|
164
|
+
getExtensionManager().runHook(
|
|
165
|
+
'onTaskComplete',
|
|
166
|
+
{ taskId: id, result: tasks[id].result },
|
|
167
|
+
{ enabledIds: agentExtensions },
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (tasks[id].sessionId) {
|
|
172
|
+
enqueueSystemEvent(tasks[id].sessionId, `Task ${tasks[id].status}: ${tasks[id].title}`)
|
|
173
|
+
}
|
|
174
|
+
if (tasks[id].agentId) {
|
|
175
|
+
dispatchWake({
|
|
176
|
+
mode: 'immediate',
|
|
177
|
+
agentId: tasks[id].agentId,
|
|
178
|
+
sessionId: tasks[id].sessionId || undefined,
|
|
179
|
+
eventId: `task:${id}:${tasks[id].status}`,
|
|
180
|
+
reason: 'task-completed',
|
|
181
|
+
source: `task:${id}`,
|
|
182
|
+
resumeMessage: `Task ${tasks[id].status}: ${tasks[id].title}`,
|
|
183
|
+
detail: tasks[id].status === 'failed'
|
|
184
|
+
? String(tasks[id].error || '').slice(0, 400)
|
|
185
|
+
: JSON.stringify(tasks[id].result || '').slice(0, 400),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (tasks[id].status === 'queued') {
|
|
191
|
+
const blockers = Array.isArray(tasks[id].blockedBy) ? tasks[id].blockedBy : []
|
|
192
|
+
const incompleteBlocker = blockers.find((bid: string) => tasks[bid] && tasks[bid].status !== 'completed')
|
|
193
|
+
if (incompleteBlocker) {
|
|
194
|
+
tasks[id].status = prevStatus
|
|
195
|
+
tasks[id].updatedAt = Date.now()
|
|
196
|
+
saveTask(id, tasks[id])
|
|
197
|
+
return serviceFail(409, 'Cannot queue: blocked by incomplete tasks')
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (tasks[id].status === 'completed') {
|
|
202
|
+
const unblockedIds = cascadeUnblock(tasks, id)
|
|
203
|
+
if (unblockedIds.length > 0) {
|
|
204
|
+
saveTaskMany([
|
|
205
|
+
[id, tasks[id]],
|
|
206
|
+
...unblockedIds.map((uid) => [uid, tasks[uid]] as [string, BoardTask]),
|
|
207
|
+
])
|
|
208
|
+
for (const uid of unblockedIds) {
|
|
209
|
+
enqueueTask(uid)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
|
|
215
|
+
enqueueTask(id)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
notify('tasks')
|
|
219
|
+
return serviceOk(enrichTaskWithMissionSummary({
|
|
220
|
+
...tasks[id],
|
|
221
|
+
missionId: mission?.id || tasks[id].missionId || null,
|
|
222
|
+
}))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function archiveTaskFromRoute(id: string): ServiceResult<BoardTask> {
|
|
226
|
+
const task = loadTask(id)
|
|
227
|
+
if (!task) return serviceFail(404, 'Task not found')
|
|
228
|
+
task.status = 'archived'
|
|
229
|
+
task.archivedAt = Date.now()
|
|
230
|
+
task.updatedAt = Date.now()
|
|
231
|
+
saveTask(id, task)
|
|
232
|
+
logActivity({ entityType: 'task', entityId: id, action: 'deleted', actor: 'user', summary: `Task archived: "${task.title}"` })
|
|
233
|
+
pushMainLoopEventToMainSessions({
|
|
234
|
+
type: 'task_archived',
|
|
235
|
+
text: `Task archived: "${task.title}" (${id}).`,
|
|
236
|
+
})
|
|
237
|
+
notify('tasks')
|
|
238
|
+
return serviceOk(task)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createTaskFromRoute(body: Record<string, unknown>): ServiceResult<BoardTask> {
|
|
242
|
+
const id = genId()
|
|
243
|
+
const now = Date.now()
|
|
244
|
+
const tasks = loadTasks()
|
|
245
|
+
const settings = loadSettings()
|
|
246
|
+
const maxAttempts = Number.isFinite(Number(body.maxAttempts))
|
|
247
|
+
? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
|
|
248
|
+
: Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
|
|
249
|
+
const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
|
|
250
|
+
? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
|
|
251
|
+
: Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
252
|
+
if (Array.isArray(body.blockedBy) && body.blockedBy.length > 0) {
|
|
253
|
+
const dagResult = validateDag(tasks, id, body.blockedBy)
|
|
254
|
+
if (!dagResult.valid) {
|
|
255
|
+
return serviceFail(400, 'Dependency cycle detected')
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const description = typeof body.description === 'string' ? body.description : ''
|
|
259
|
+
const resolvedAgentId = description
|
|
260
|
+
? resolveTaskAgentFromDescription(description, (body.agentId as string) || '', loadAgents())
|
|
261
|
+
: ((body.agentId as string) || '')
|
|
262
|
+
|
|
263
|
+
const prepared = prepareTaskCreation({
|
|
264
|
+
id,
|
|
265
|
+
input: {
|
|
266
|
+
...body,
|
|
267
|
+
agentId: resolvedAgentId,
|
|
268
|
+
},
|
|
269
|
+
tasks,
|
|
270
|
+
now,
|
|
271
|
+
settings,
|
|
272
|
+
seed: {
|
|
273
|
+
projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
|
|
274
|
+
goalContract: body.goalContract || null,
|
|
275
|
+
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
276
|
+
file: typeof body.file === 'string' ? body.file : null,
|
|
277
|
+
sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
|
|
278
|
+
result: typeof body.result === 'string' ? body.result : null,
|
|
279
|
+
error: typeof body.error === 'string' ? body.error : null,
|
|
280
|
+
outputFiles: Array.isArray(body.outputFiles)
|
|
281
|
+
? body.outputFiles.filter((entry): entry is string => typeof entry === 'string').slice(0, 24)
|
|
282
|
+
: [],
|
|
283
|
+
artifacts: Array.isArray(body.artifacts)
|
|
284
|
+
? body.artifacts
|
|
285
|
+
.filter((artifact) => artifact && typeof artifact === 'object')
|
|
286
|
+
.map((artifact) => {
|
|
287
|
+
const row = artifact as { url?: unknown; type?: unknown; filename?: unknown }
|
|
288
|
+
const normalizedType = String(row.type || '')
|
|
289
|
+
return {
|
|
290
|
+
url: String(row.url || ''),
|
|
291
|
+
type: ['image', 'video', 'pdf', 'file'].includes(normalizedType)
|
|
292
|
+
? (normalizedType as 'image' | 'video' | 'pdf' | 'file')
|
|
293
|
+
: 'file',
|
|
294
|
+
filename: String(row.filename || ''),
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.filter((artifact) => artifact.url && artifact.filename)
|
|
298
|
+
.slice(0, 24)
|
|
299
|
+
: [],
|
|
300
|
+
archivedAt: null,
|
|
301
|
+
attempts: 0,
|
|
302
|
+
maxAttempts,
|
|
303
|
+
retryBackoffSec,
|
|
304
|
+
retryScheduledAt: null,
|
|
305
|
+
deadLetteredAt: null,
|
|
306
|
+
checkpoint: null,
|
|
307
|
+
blockedBy: Array.isArray(body.blockedBy) ? body.blockedBy.filter((s): s is string => typeof s === 'string') : [],
|
|
308
|
+
blocks: Array.isArray(body.blocks) ? body.blocks.filter((s): s is string => typeof s === 'string') : [],
|
|
309
|
+
tags: Array.isArray(body.tags) ? body.tags.filter((s): s is string => typeof s === 'string') : [],
|
|
310
|
+
dueAt: typeof body.dueAt === 'number' ? body.dueAt : null,
|
|
311
|
+
customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
|
|
312
|
+
priority: body.priority && ['low', 'medium', 'high', 'critical'].includes(String(body.priority))
|
|
313
|
+
? body.priority as BoardTask['priority']
|
|
314
|
+
: undefined,
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
if (!prepared.ok) {
|
|
318
|
+
return serviceFail(400, prepared.error)
|
|
319
|
+
}
|
|
320
|
+
if (prepared.duplicate) {
|
|
321
|
+
return serviceOk({ ...prepared.duplicate, deduplicated: true } as BoardTask)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const task = prepared.task
|
|
325
|
+
if (task.status === 'completed') {
|
|
326
|
+
const agentExtensions = resolvedAgentId ? getEnabledCapabilityIds(loadAgents()[resolvedAgentId]) : []
|
|
327
|
+
getExtensionManager().runHook(
|
|
328
|
+
'onTaskComplete',
|
|
329
|
+
{ taskId: id, result: task.result },
|
|
330
|
+
{ enabledIds: agentExtensions },
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const parentTaskId = typeof body.parentTaskId === 'string' && body.parentTaskId.trim() ? body.parentTaskId.trim() : null
|
|
335
|
+
if (parentTaskId) {
|
|
336
|
+
task.parentTaskId = parentTaskId
|
|
337
|
+
const parentTask = tasks[parentTaskId]
|
|
338
|
+
if (parentTask) {
|
|
339
|
+
const subtaskIds = Array.isArray(parentTask.subtaskIds) ? parentTask.subtaskIds : []
|
|
340
|
+
if (!subtaskIds.includes(id)) {
|
|
341
|
+
parentTask.subtaskIds = [...subtaskIds, id]
|
|
342
|
+
parentTask.updatedAt = now
|
|
343
|
+
saveTask(parentTaskId, parentTask)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
saveTask(id, task)
|
|
349
|
+
const mission = ensureMissionForTask(task, { source: 'manual' })
|
|
350
|
+
const finalTask = enrichTaskWithMissionSummary({
|
|
351
|
+
...task,
|
|
352
|
+
missionId: mission?.id || task.missionId || null,
|
|
353
|
+
})
|
|
354
|
+
logActivity({ entityType: 'task', entityId: id, action: 'created', actor: 'user', summary: `Task created: "${task.title}"` })
|
|
355
|
+
pushMainLoopEventToMainSessions({
|
|
356
|
+
type: 'task_created',
|
|
357
|
+
text: `Task created: "${task.title}" (${id}) with status ${task.status}.`,
|
|
358
|
+
})
|
|
359
|
+
if (task.status === 'queued') {
|
|
360
|
+
enqueueTask(id)
|
|
361
|
+
}
|
|
362
|
+
notify('tasks')
|
|
363
|
+
return serviceOk(finalTask)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function bulkUpdateTasksFromRoute(body: Record<string, unknown>): ServiceResult<{ updated: number; ids: string[] }> {
|
|
367
|
+
const ids = body.ids
|
|
368
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
369
|
+
return serviceFail(400, 'ids must be a non-empty array')
|
|
370
|
+
}
|
|
371
|
+
const taskIds = ids.filter((id): id is string => typeof id === 'string')
|
|
372
|
+
if (taskIds.length === 0) {
|
|
373
|
+
return serviceFail(400, 'No valid task IDs provided')
|
|
374
|
+
}
|
|
375
|
+
const tasks = loadTasks()
|
|
376
|
+
let updated = 0
|
|
377
|
+
const results: string[] = []
|
|
378
|
+
|
|
379
|
+
for (const id of taskIds) {
|
|
380
|
+
if (!tasks[id]) continue
|
|
381
|
+
const prevStatus = tasks[id].status
|
|
382
|
+
if (typeof body.status === 'string' && VALID_BULK_STATUSES.includes(body.status as BoardTaskStatus)) {
|
|
383
|
+
tasks[id].status = body.status as BoardTaskStatus
|
|
384
|
+
if (body.status === 'archived' && prevStatus !== 'archived') {
|
|
385
|
+
tasks[id].archivedAt = Date.now()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if ('agentId' in body) {
|
|
389
|
+
tasks[id].agentId = body.agentId === null ? '' : String(body.agentId)
|
|
390
|
+
}
|
|
391
|
+
if ('projectId' in body) {
|
|
392
|
+
if (body.projectId === null) delete tasks[id].projectId
|
|
393
|
+
else tasks[id].projectId = String(body.projectId)
|
|
394
|
+
}
|
|
395
|
+
tasks[id].updatedAt = Date.now()
|
|
396
|
+
updated += 1
|
|
397
|
+
results.push(id)
|
|
398
|
+
if (prevStatus !== tasks[id].status) {
|
|
399
|
+
logActivity({
|
|
400
|
+
entityType: 'task',
|
|
401
|
+
entityId: id,
|
|
402
|
+
action: 'updated',
|
|
403
|
+
actor: 'user',
|
|
404
|
+
summary: `Bulk update: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})`,
|
|
405
|
+
})
|
|
406
|
+
pushMainLoopEventToMainSessions({
|
|
407
|
+
type: 'task_status_changed',
|
|
408
|
+
text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
|
|
409
|
+
})
|
|
410
|
+
if (tasks[id].status === 'completed' || tasks[id].status === 'failed') {
|
|
411
|
+
disableSessionHeartbeat(tasks[id].sessionId)
|
|
412
|
+
}
|
|
413
|
+
if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
|
|
414
|
+
enqueueTask(id)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
saveTaskMany(results.map((id) => [id, tasks[id]] as [string, BoardTask]))
|
|
419
|
+
if (updated > 0) {
|
|
420
|
+
const action = body.status
|
|
421
|
+
? `moved ${updated} task(s) to ${body.status}`
|
|
422
|
+
: `updated ${updated} task(s)`
|
|
423
|
+
createNotification({
|
|
424
|
+
type: 'success',
|
|
425
|
+
title: `Bulk update: ${action}`,
|
|
426
|
+
entityType: 'task',
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
notify('tasks')
|
|
430
|
+
return serviceOk({ updated, ids: results })
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function deleteTasksByFilter(filter: string | null) {
|
|
434
|
+
const tasks = loadTasks()
|
|
435
|
+
let removed = 0
|
|
436
|
+
const shouldRemove = (task: { status: string; sourceType?: string }) =>
|
|
437
|
+
filter === 'all'
|
|
438
|
+
|| (filter === 'schedule' && task.sourceType === 'schedule')
|
|
439
|
+
|| (filter === 'done' && (task.status === 'completed' || task.status === 'failed'))
|
|
440
|
+
|| (!filter && task.status === 'archived')
|
|
441
|
+
|
|
442
|
+
for (const [id, task] of Object.entries(tasks)) {
|
|
443
|
+
if (!shouldRemove(task as { status: string; sourceType?: string })) continue
|
|
444
|
+
deleteTask(id)
|
|
445
|
+
removed += 1
|
|
446
|
+
}
|
|
447
|
+
notify('tasks')
|
|
448
|
+
return { removed, remaining: Object.keys(tasks).length - removed }
|
|
449
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared text-normalization utilities.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the cleanText / cleanMultiline / normalizeList helpers that
|
|
5
|
+
* were previously duplicated across 7+ server modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Collapse whitespace, trim, and cap at `max` characters. Returns `''` for non-string input. */
|
|
9
|
+
export function cleanText(value: unknown, max = 320): string {
|
|
10
|
+
if (typeof value !== 'string') return ''
|
|
11
|
+
return value.replace(/\s+/g, ' ').trim().slice(0, max)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Trim each line, drop blanks, rejoin, and cap at `max` characters. Returns `''` for non-string input. */
|
|
15
|
+
export function cleanMultiline(value: unknown, max = 1_200): string {
|
|
16
|
+
if (typeof value !== 'string') return ''
|
|
17
|
+
return value
|
|
18
|
+
.split('\n')
|
|
19
|
+
.map((line) => line.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('\n')
|
|
22
|
+
.slice(0, max)
|
|
23
|
+
.trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Deduplicated, cleaned list of strings from unknown input. */
|
|
27
|
+
export function normalizeList(input: unknown, maxItems: number, maxChars = 240): string[] {
|
|
28
|
+
const values = Array.isArray(input) ? input : []
|
|
29
|
+
const seen = new Set<string>()
|
|
30
|
+
const out: string[] = []
|
|
31
|
+
for (const value of values) {
|
|
32
|
+
const cleaned = cleanText(value, maxChars)
|
|
33
|
+
if (!cleaned) continue
|
|
34
|
+
const key = cleaned.toLowerCase()
|
|
35
|
+
if (seen.has(key)) continue
|
|
36
|
+
seen.add(key)
|
|
37
|
+
out.push(cleaned)
|
|
38
|
+
if (out.length >= maxItems) break
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|