@swarmclawai/swarmclaw 1.2.0 → 1.2.2
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 +19 -0
- package/package.json +5 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +17 -20
- package/src/app/api/chats/[id]/messages/route.ts +15 -11
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +15 -12
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +20 -9
- package/src/components/chat/message-list.tsx +62 -42
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +23 -4
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +14 -6
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
- package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2263
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +8 -7
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1331
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +78 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +6 -3
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2058
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +13 -8
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1374
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +32 -32
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +78 -37
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +26 -6
- package/src/types/index.ts +6 -0
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { executeSessionChatTurn } from '@/lib/server/chat-execution/chat-execution'
|
|
2
|
+
import { log } from '@/lib/server/logger'
|
|
3
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
6
|
+
import { handleMainLoopRunResult } from '@/lib/server/agents/main-agent-loop'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
clearDeferredDrain,
|
|
10
|
+
decrementNonHeartbeatWork,
|
|
11
|
+
emitRunMeta,
|
|
12
|
+
emitToSubscribers,
|
|
13
|
+
hasActiveNonHeartbeatSessionLease,
|
|
14
|
+
hasExternalSessionExecutionHold,
|
|
15
|
+
HEARTBEAT_BUSY_RETRY_MS,
|
|
16
|
+
MAX_DRAIN_DEPTH,
|
|
17
|
+
now,
|
|
18
|
+
queueAutonomyObservation,
|
|
19
|
+
queueForExecution,
|
|
20
|
+
reconcileSessionActivityLease,
|
|
21
|
+
scheduleDeferredDrain,
|
|
22
|
+
state,
|
|
23
|
+
syncRunRecord,
|
|
24
|
+
} from './state'
|
|
25
|
+
import type { EnqueueSessionRunInput } from './types'
|
|
26
|
+
|
|
27
|
+
type EnqueueSessionRunFn = (input: EnqueueSessionRunInput) => unknown
|
|
28
|
+
|
|
29
|
+
export async function drainExecution(
|
|
30
|
+
executionKey: string,
|
|
31
|
+
deps: { enqueueSessionRun: EnqueueSessionRunFn },
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const depth = (state.drainDepth.get(executionKey) || 0) + 1
|
|
34
|
+
state.drainDepth.set(executionKey, depth)
|
|
35
|
+
if (depth > MAX_DRAIN_DEPTH) {
|
|
36
|
+
log.error('session-run', 'Drain recursion depth exceeded, deferring', { executionKey, depth, max: MAX_DRAIN_DEPTH })
|
|
37
|
+
state.drainDepth.delete(executionKey)
|
|
38
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, 500)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (state.runningByExecution.has(executionKey)) return
|
|
43
|
+
const queue = queueForExecution(executionKey)
|
|
44
|
+
const userIdx = queue.findIndex((entry) => !entry.run.internal)
|
|
45
|
+
let next
|
|
46
|
+
if (userIdx >= 0) {
|
|
47
|
+
next = queue.splice(userIdx, 1)[0]
|
|
48
|
+
} else {
|
|
49
|
+
const internalIdx = queue.findIndex((entry) => !isInternalHeartbeatRun(entry.run.internal, entry.run.source))
|
|
50
|
+
next = internalIdx >= 0 ? queue.splice(internalIdx, 1)[0] : queue.shift()
|
|
51
|
+
}
|
|
52
|
+
if (!next) {
|
|
53
|
+
clearDeferredDrain(executionKey)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isInternalHeartbeatRun(next.run.internal, next.run.source) && hasActiveNonHeartbeatSessionLease(next.run.sessionId)) {
|
|
58
|
+
queue.unshift(next)
|
|
59
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, HEARTBEAT_BUSY_RETRY_MS)
|
|
60
|
+
log.info('session-run', `Deferred heartbeat run ${next.run.id} for shared busy session`, {
|
|
61
|
+
sessionId: next.run.sessionId,
|
|
62
|
+
source: next.run.source,
|
|
63
|
+
})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasExternalSessionExecutionHold(next.run.sessionId)) {
|
|
68
|
+
queue.unshift(next)
|
|
69
|
+
scheduleDeferredDrain(executionKey, (nextExecutionKey) => { void drainExecution(nextExecutionKey, deps) }, HEARTBEAT_BUSY_RETRY_MS)
|
|
70
|
+
log.info('session-run', `Deferred run ${next.run.id} for external session hold`, {
|
|
71
|
+
sessionId: next.run.sessionId,
|
|
72
|
+
source: next.run.source,
|
|
73
|
+
mode: next.run.mode,
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clearDeferredDrain(executionKey)
|
|
79
|
+
state.runningByExecution.set(executionKey, next)
|
|
80
|
+
next.run.status = 'running'
|
|
81
|
+
next.run.startedAt = now()
|
|
82
|
+
syncRunRecord(next.run)
|
|
83
|
+
emitRunMeta(next, 'running')
|
|
84
|
+
log.info('session-run', `Run started ${next.run.id}`, {
|
|
85
|
+
sessionId: next.run.sessionId,
|
|
86
|
+
source: next.run.source,
|
|
87
|
+
internal: next.run.internal,
|
|
88
|
+
mode: next.run.mode,
|
|
89
|
+
timeoutMs: next.maxRuntimeMs || null,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
let runtimeTimer: ReturnType<typeof setTimeout> | null = null
|
|
93
|
+
let finishedMissionId: string | null = null
|
|
94
|
+
if (next.maxRuntimeMs && next.maxRuntimeMs > 0) {
|
|
95
|
+
runtimeTimer = setTimeout(() => {
|
|
96
|
+
next.signalController.abort()
|
|
97
|
+
}, next.maxRuntimeMs)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await executeSessionChatTurn({
|
|
102
|
+
sessionId: next.run.sessionId,
|
|
103
|
+
message: next.message,
|
|
104
|
+
imagePath: next.imagePath,
|
|
105
|
+
imageUrl: next.imageUrl,
|
|
106
|
+
attachedFiles: next.attachedFiles,
|
|
107
|
+
internal: next.run.internal,
|
|
108
|
+
source: next.run.source,
|
|
109
|
+
runId: next.run.id,
|
|
110
|
+
signal: next.signalController.signal,
|
|
111
|
+
onEvent: (event) => emitToSubscribers(next, event),
|
|
112
|
+
modelOverride: next.modelOverride,
|
|
113
|
+
heartbeatConfig: next.heartbeatConfig,
|
|
114
|
+
replyToId: next.replyToId,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const failed = !!result.error
|
|
118
|
+
const aborted = next.signalController.signal.aborted
|
|
119
|
+
next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
|
|
120
|
+
next.run.endedAt = next.run.endedAt || now()
|
|
121
|
+
next.run.error = aborted ? (next.run.error || 'Cancelled') : result.error
|
|
122
|
+
next.run.missionId = result.missionId || next.run.missionId || null
|
|
123
|
+
finishedMissionId = next.run.missionId || null
|
|
124
|
+
next.run.resultPreview = result.text?.slice(0, 280)
|
|
125
|
+
if (typeof result.inputTokens === 'number') next.run.totalInputTokens = result.inputTokens
|
|
126
|
+
if (typeof result.outputTokens === 'number') next.run.totalOutputTokens = result.outputTokens
|
|
127
|
+
if (typeof result.estimatedCost === 'number') next.run.estimatedCost = result.estimatedCost
|
|
128
|
+
syncRunRecord(next.run)
|
|
129
|
+
emitRunMeta(next, next.run.status, {
|
|
130
|
+
persisted: result.persisted,
|
|
131
|
+
hasText: !!result.text,
|
|
132
|
+
error: next.run.error || null,
|
|
133
|
+
})
|
|
134
|
+
log.info('session-run', `Run finished ${next.run.id}`, {
|
|
135
|
+
sessionId: next.run.sessionId,
|
|
136
|
+
status: next.run.status,
|
|
137
|
+
persisted: result.persisted,
|
|
138
|
+
hasText: !!result.text,
|
|
139
|
+
error: next.run.error || null,
|
|
140
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
141
|
+
})
|
|
142
|
+
const followup = handleMainLoopRunResult({
|
|
143
|
+
runId: next.run.id,
|
|
144
|
+
sessionId: next.run.sessionId,
|
|
145
|
+
message: next.message,
|
|
146
|
+
internal: next.run.internal,
|
|
147
|
+
source: next.run.source,
|
|
148
|
+
resultText: result.text,
|
|
149
|
+
error: next.run.error,
|
|
150
|
+
toolEvents: result.toolEvents,
|
|
151
|
+
inputTokens: result.inputTokens,
|
|
152
|
+
outputTokens: result.outputTokens,
|
|
153
|
+
estimatedCost: result.estimatedCost,
|
|
154
|
+
})
|
|
155
|
+
queueAutonomyObservation({
|
|
156
|
+
runId: next.run.id,
|
|
157
|
+
sessionId: next.run.sessionId,
|
|
158
|
+
source: next.run.source,
|
|
159
|
+
status: next.run.status,
|
|
160
|
+
resultText: result.text,
|
|
161
|
+
error: next.run.error || null,
|
|
162
|
+
toolEvents: result.toolEvents,
|
|
163
|
+
sourceMessage: next.message,
|
|
164
|
+
})
|
|
165
|
+
if (followup) {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
try {
|
|
168
|
+
deps.enqueueSessionRun({
|
|
169
|
+
sessionId: next.run.sessionId,
|
|
170
|
+
message: followup.message,
|
|
171
|
+
internal: true,
|
|
172
|
+
source: 'main-loop-followup',
|
|
173
|
+
mode: 'followup',
|
|
174
|
+
dedupeKey: followup.dedupeKey,
|
|
175
|
+
})
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
log.warn('session-run', `Main loop follow-up enqueue failed for ${next.run.sessionId}`, {
|
|
178
|
+
error: errorMessage(err),
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}, Math.max(0, followup.delayMs || 0))
|
|
182
|
+
}
|
|
183
|
+
next.resolve(result)
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
const aborted = next.signalController.signal.aborted
|
|
186
|
+
next.run.status = aborted ? 'cancelled' : 'failed'
|
|
187
|
+
next.run.endedAt = now()
|
|
188
|
+
next.run.error = errorMessage(err)
|
|
189
|
+
finishedMissionId = next.run.missionId || null
|
|
190
|
+
syncRunRecord(next.run)
|
|
191
|
+
emitRunMeta(next, next.run.status, { error: next.run.error })
|
|
192
|
+
log.error('session-run', `Run failed ${next.run.id}`, {
|
|
193
|
+
sessionId: next.run.sessionId,
|
|
194
|
+
status: next.run.status,
|
|
195
|
+
error: next.run.error,
|
|
196
|
+
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
197
|
+
})
|
|
198
|
+
if (err instanceof Error && err.stack) {
|
|
199
|
+
log.error('session-run', `Run failed stack trace ${next.run.id}`, {
|
|
200
|
+
sessionId: next.run.sessionId,
|
|
201
|
+
stack: err.stack,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
queueAutonomyObservation({
|
|
205
|
+
runId: next.run.id,
|
|
206
|
+
sessionId: next.run.sessionId,
|
|
207
|
+
source: next.run.source,
|
|
208
|
+
status: next.run.status,
|
|
209
|
+
error: next.run.error || null,
|
|
210
|
+
sourceMessage: next.message,
|
|
211
|
+
})
|
|
212
|
+
next.reject(err instanceof Error ? err : new Error(next.run.error))
|
|
213
|
+
} finally {
|
|
214
|
+
if (runtimeTimer) clearTimeout(runtimeTimer)
|
|
215
|
+
state.runningByExecution.delete(executionKey)
|
|
216
|
+
decrementNonHeartbeatWork(next)
|
|
217
|
+
reconcileSessionActivityLease(next.run.sessionId)
|
|
218
|
+
notify(`stream-end:${next.run.sessionId}`)
|
|
219
|
+
if (finishedMissionId && next.run.source !== 'chat') {
|
|
220
|
+
const missionId = finishedMissionId
|
|
221
|
+
queueMicrotask(() => {
|
|
222
|
+
import('@/lib/server/missions/mission-service')
|
|
223
|
+
.then(({ loadMissionById, requestMissionTick }) => {
|
|
224
|
+
const mission = loadMissionById(missionId)
|
|
225
|
+
if (!mission) return
|
|
226
|
+
if (mission.status !== 'active') return
|
|
227
|
+
if (mission.phase === 'dispatching' || mission.phase === 'executing') return
|
|
228
|
+
requestMissionTick(missionId, 'run_drained', {
|
|
229
|
+
runId: next.run.id,
|
|
230
|
+
source: next.run.source,
|
|
231
|
+
status: next.run.status,
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
.catch((err: unknown) => {
|
|
235
|
+
log.warn('session-run', 'Mission tick failed', { missionId, runId: next.run.id, error: errorMessage(err) })
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
void drainExecution(executionKey, deps)
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
const currentDepth = state.drainDepth.get(executionKey)
|
|
243
|
+
if (currentDepth && currentDepth > 1) state.drainDepth.set(executionKey, currentDepth - 1)
|
|
244
|
+
else state.drainDepth.delete(executionKey)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import type { SessionRunRecord } from '@/types'
|
|
3
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
4
|
+
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
6
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
7
|
+
import { getEnabledToolIds } from '@/lib/capability-selection'
|
|
8
|
+
import { isAllEstopEngaged, isAutonomyEstopEngaged } from '@/lib/server/runtime/estop'
|
|
9
|
+
import { getActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
10
|
+
|
|
11
|
+
import { cancelPendingForSession } from './cancellation'
|
|
12
|
+
import {
|
|
13
|
+
abortSessionRuntime,
|
|
14
|
+
chainCallerSignal,
|
|
15
|
+
COLLECT_COALESCE_WINDOW_MS,
|
|
16
|
+
emitRunMeta,
|
|
17
|
+
executionKeyForSession,
|
|
18
|
+
incrementNonHeartbeatWork,
|
|
19
|
+
messagePreview,
|
|
20
|
+
nextQueuedAt,
|
|
21
|
+
normalizeMode,
|
|
22
|
+
queueForExecution,
|
|
23
|
+
reconcileSessionActivityLease,
|
|
24
|
+
registerRun,
|
|
25
|
+
state,
|
|
26
|
+
syncRunRecord,
|
|
27
|
+
} from './state'
|
|
28
|
+
import type {
|
|
29
|
+
EnqueueSessionRunInput,
|
|
30
|
+
EnqueueSessionRunResult,
|
|
31
|
+
SessionQueueMode,
|
|
32
|
+
SessionRunQueueEntry,
|
|
33
|
+
} from './types'
|
|
34
|
+
|
|
35
|
+
type RepairSessionRunQueueFn = (
|
|
36
|
+
sessionId: string,
|
|
37
|
+
opts?: {
|
|
38
|
+
executionKey?: string
|
|
39
|
+
maxQueuedAgeMs?: number
|
|
40
|
+
reason?: string
|
|
41
|
+
},
|
|
42
|
+
) => { kickedExecutionKeys: number; recoveredQueuedRuns: number }
|
|
43
|
+
|
|
44
|
+
type DrainExecutionFn = (executionKey: string) => Promise<void>
|
|
45
|
+
|
|
46
|
+
const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli'])
|
|
47
|
+
|
|
48
|
+
type SessionToolConfig = {
|
|
49
|
+
tools?: string[] | null
|
|
50
|
+
extensions?: string[] | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function computeEffectiveRunTimeoutMs(
|
|
54
|
+
baseTimeoutMs: number,
|
|
55
|
+
sessionTools: string[],
|
|
56
|
+
runtime: { claudeCodeTimeoutMs: number },
|
|
57
|
+
): number {
|
|
58
|
+
const hasLongTool = sessionTools.some((tool) => LONG_TOOL_NAMES.has(tool))
|
|
59
|
+
if (!hasLongTool) return baseTimeoutMs
|
|
60
|
+
const toolTimeout = runtime.claudeCodeTimeoutMs + 120_000
|
|
61
|
+
return Math.max(baseTimeoutMs, toolTimeout)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isAutonomyManagedEnqueue(source: string, internal: boolean): boolean {
|
|
65
|
+
return !(source === 'chat' && !internal)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildRecoveryPayload(
|
|
69
|
+
input: EnqueueSessionRunInput,
|
|
70
|
+
source: string,
|
|
71
|
+
mode: SessionQueueMode,
|
|
72
|
+
maxRuntimeMs: number | undefined,
|
|
73
|
+
executionKey: string,
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
message: input.message,
|
|
77
|
+
imagePath: input.imagePath,
|
|
78
|
+
imageUrl: input.imageUrl,
|
|
79
|
+
attachedFiles: input.attachedFiles,
|
|
80
|
+
internal: input.internal === true,
|
|
81
|
+
source,
|
|
82
|
+
mode,
|
|
83
|
+
maxRuntimeMs,
|
|
84
|
+
modelOverride: input.modelOverride,
|
|
85
|
+
heartbeatConfig: input.heartbeatConfig,
|
|
86
|
+
replyToId: input.replyToId,
|
|
87
|
+
executionGroupKey: executionKey.startsWith('session:') ? undefined : executionKey,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findDedupeMatch(sessionId: string, dedupeKey?: string) {
|
|
92
|
+
if (!dedupeKey) return null
|
|
93
|
+
const executionKey = executionKeyForSession(sessionId)
|
|
94
|
+
const running = state.runningByExecution.get(executionKey)
|
|
95
|
+
if (running?.run.sessionId === sessionId && running.run.dedupeKey === dedupeKey) return running
|
|
96
|
+
const queue = queueForExecution(executionKey)
|
|
97
|
+
return queue.find((entry) => entry.run.sessionId === sessionId && entry.run.dedupeKey === dedupeKey) || null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function enqueueSessionRun(
|
|
101
|
+
input: EnqueueSessionRunInput,
|
|
102
|
+
deps: {
|
|
103
|
+
repairSessionRunQueue: RepairSessionRunQueueFn
|
|
104
|
+
drainExecution: DrainExecutionFn
|
|
105
|
+
},
|
|
106
|
+
): EnqueueSessionRunResult {
|
|
107
|
+
const internal = input.internal === true
|
|
108
|
+
const mode = normalizeMode(input.mode, internal)
|
|
109
|
+
const source = input.source || 'chat'
|
|
110
|
+
if (isAllEstopEngaged()) {
|
|
111
|
+
throw new Error('Execution is blocked because all estop is engaged.')
|
|
112
|
+
}
|
|
113
|
+
if (isAutonomyEstopEngaged() && isAutonomyManagedEnqueue(source, internal)) {
|
|
114
|
+
throw new Error(`Autonomy estop is engaged. New ${source} runs are paused.`)
|
|
115
|
+
}
|
|
116
|
+
const executionKey = typeof input.executionGroupKey === 'string' && input.executionGroupKey.trim()
|
|
117
|
+
? input.executionGroupKey.trim()
|
|
118
|
+
: executionKeyForSession(input.sessionId)
|
|
119
|
+
deps.repairSessionRunQueue(input.sessionId, {
|
|
120
|
+
executionKey,
|
|
121
|
+
reason: 'Recovered stale queued run before enqueue',
|
|
122
|
+
})
|
|
123
|
+
const runtime = loadRuntimeSettings()
|
|
124
|
+
const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
|
|
125
|
+
const sessionData = getSession(input.sessionId) as SessionToolConfig | null
|
|
126
|
+
const sessionTools = getEnabledToolIds(sessionData)
|
|
127
|
+
const adjustedDefaultMs = computeEffectiveRunTimeoutMs(defaultMaxRuntimeMs, sessionTools, runtime)
|
|
128
|
+
const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
|
|
129
|
+
? input.maxRuntimeMs
|
|
130
|
+
: adjustedDefaultMs
|
|
131
|
+
|
|
132
|
+
const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
|
|
133
|
+
if (dedupe) {
|
|
134
|
+
const cb = input.onEvent
|
|
135
|
+
if (cb) dedupe.onEvents.push(cb)
|
|
136
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, dedupe.signalController)
|
|
137
|
+
return {
|
|
138
|
+
runId: dedupe.run.id,
|
|
139
|
+
position: 0,
|
|
140
|
+
deduped: true,
|
|
141
|
+
promise: dedupe.promise,
|
|
142
|
+
abort: () => dedupe.signalController.abort(),
|
|
143
|
+
unsubscribe: () => {
|
|
144
|
+
if (!cb) return
|
|
145
|
+
const idx = dedupe.onEvents.indexOf(cb)
|
|
146
|
+
if (idx >= 0) dedupe.onEvents.splice(idx, 1)
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (mode === 'steer') {
|
|
152
|
+
const running = state.runningByExecution.get(executionKey)
|
|
153
|
+
if (running && running.run.sessionId === input.sessionId) {
|
|
154
|
+
running.signalController.abort()
|
|
155
|
+
try { getActiveSessionProcess(input.sessionId)?.kill?.() } catch { /* noop */ }
|
|
156
|
+
}
|
|
157
|
+
cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!internal && source === 'chat') {
|
|
161
|
+
const running = state.runningByExecution.get(executionKey)
|
|
162
|
+
if (running && isInternalHeartbeatRun(running.run.internal, running.run.source)) {
|
|
163
|
+
log.info('session-run', `Preempting heartbeat ${running.run.id} for user chat on ${input.sessionId}`)
|
|
164
|
+
abortSessionRuntime(running, 'Preempted by user chat')
|
|
165
|
+
state.runningByExecution.delete(executionKey)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const running = state.runningByExecution.get(executionKey)
|
|
170
|
+
const queue = queueForExecution(executionKey)
|
|
171
|
+
if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
|
|
172
|
+
const nowMs = nextQueuedAt()
|
|
173
|
+
const candidate = queue.at(-1)
|
|
174
|
+
const canCoalesce = !!candidate
|
|
175
|
+
&& candidate.run.mode === 'collect'
|
|
176
|
+
&& candidate.run.internal === internal
|
|
177
|
+
&& candidate.run.source === source
|
|
178
|
+
&& !candidate.imagePath
|
|
179
|
+
&& !candidate.imageUrl
|
|
180
|
+
&& !candidate.attachedFiles?.length
|
|
181
|
+
&& (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
|
|
182
|
+
|
|
183
|
+
if (candidate && canCoalesce) {
|
|
184
|
+
const nextChunk = input.message.trim()
|
|
185
|
+
if (nextChunk) {
|
|
186
|
+
const current = candidate.message.trim()
|
|
187
|
+
candidate.message = current
|
|
188
|
+
? `${current}\n\n[Collected follow-up]\n${nextChunk}`
|
|
189
|
+
: nextChunk
|
|
190
|
+
candidate.run.messagePreview = messagePreview(candidate.message)
|
|
191
|
+
candidate.run.queuedAt = nowMs
|
|
192
|
+
syncRunRecord(candidate.run)
|
|
193
|
+
}
|
|
194
|
+
const coalesceCb = input.onEvent
|
|
195
|
+
if (coalesceCb) candidate.onEvents.push(coalesceCb)
|
|
196
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, candidate.signalController)
|
|
197
|
+
emitRunMeta(candidate, 'queued', { position: 0, coalesced: true, mergedIntoRunId: candidate.run.id })
|
|
198
|
+
return {
|
|
199
|
+
runId: candidate.run.id,
|
|
200
|
+
position: 0,
|
|
201
|
+
coalesced: true,
|
|
202
|
+
promise: candidate.promise,
|
|
203
|
+
abort: () => candidate.signalController.abort(),
|
|
204
|
+
unsubscribe: () => {
|
|
205
|
+
if (!coalesceCb) return
|
|
206
|
+
const idx = candidate.onEvents.indexOf(coalesceCb)
|
|
207
|
+
if (idx >= 0) candidate.onEvents.splice(idx, 1)
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const runId = genId(8)
|
|
214
|
+
const run: SessionRunRecord = {
|
|
215
|
+
id: runId,
|
|
216
|
+
sessionId: input.sessionId,
|
|
217
|
+
missionId: input.missionId ?? getSession(input.sessionId)?.missionId ?? null,
|
|
218
|
+
source,
|
|
219
|
+
internal,
|
|
220
|
+
mode,
|
|
221
|
+
status: 'queued',
|
|
222
|
+
messagePreview: messagePreview(input.message),
|
|
223
|
+
dedupeKey: input.dedupeKey,
|
|
224
|
+
queuedAt: nextQueuedAt(),
|
|
225
|
+
recoveredFromRestart: input.recoveredFromRestart === true,
|
|
226
|
+
recoveredFromRunId: input.recoveredFromRunId,
|
|
227
|
+
recoveryPayload: buildRecoveryPayload(
|
|
228
|
+
input,
|
|
229
|
+
source,
|
|
230
|
+
mode,
|
|
231
|
+
effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
232
|
+
executionKey,
|
|
233
|
+
),
|
|
234
|
+
}
|
|
235
|
+
registerRun(run)
|
|
236
|
+
|
|
237
|
+
let resolve!: EnqueueSessionRunResult['promise'] extends Promise<infer T> ? (value: T) => void : never
|
|
238
|
+
let reject!: (error: Error) => void
|
|
239
|
+
const promise = new Promise<import('@/lib/server/chat-execution/chat-execution-types').ExecuteChatTurnResult>((res, rej) => {
|
|
240
|
+
resolve = res
|
|
241
|
+
reject = rej
|
|
242
|
+
})
|
|
243
|
+
promise.catch(() => {})
|
|
244
|
+
state.promises.set(runId, promise)
|
|
245
|
+
|
|
246
|
+
const entry: SessionRunQueueEntry = {
|
|
247
|
+
executionKey,
|
|
248
|
+
run,
|
|
249
|
+
message: input.message,
|
|
250
|
+
imagePath: input.imagePath,
|
|
251
|
+
imageUrl: input.imageUrl,
|
|
252
|
+
attachedFiles: input.attachedFiles,
|
|
253
|
+
onEvents: input.onEvent ? [input.onEvent] : [],
|
|
254
|
+
signalController: new AbortController(),
|
|
255
|
+
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
256
|
+
modelOverride: input.modelOverride,
|
|
257
|
+
heartbeatConfig: input.heartbeatConfig,
|
|
258
|
+
replyToId: input.replyToId,
|
|
259
|
+
resolve,
|
|
260
|
+
reject,
|
|
261
|
+
promise,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, entry.signalController)
|
|
265
|
+
|
|
266
|
+
queue.push(entry)
|
|
267
|
+
incrementNonHeartbeatWork(entry)
|
|
268
|
+
if (entry.nonHeartbeatCounted) {
|
|
269
|
+
reconcileSessionActivityLease(input.sessionId)
|
|
270
|
+
}
|
|
271
|
+
const position = (running ? 1 : 0) + queue.length - 1
|
|
272
|
+
emitRunMeta(entry, 'queued', { position })
|
|
273
|
+
void deps.drainExecution(executionKey)
|
|
274
|
+
|
|
275
|
+
const entryCb = input.onEvent
|
|
276
|
+
return {
|
|
277
|
+
runId,
|
|
278
|
+
position,
|
|
279
|
+
promise,
|
|
280
|
+
abort: () => entry.signalController.abort(),
|
|
281
|
+
unsubscribe: () => {
|
|
282
|
+
if (!entryCb) return
|
|
283
|
+
const idx = entry.onEvents.indexOf(entryCb)
|
|
284
|
+
if (idx >= 0) entry.onEvents.splice(idx, 1)
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RunEventRecord,
|
|
3
|
+
SessionQueueSnapshot,
|
|
4
|
+
SessionQueuedTurn,
|
|
5
|
+
SessionRunRecord,
|
|
6
|
+
SessionRunStatus,
|
|
7
|
+
} from '@/types'
|
|
8
|
+
import {
|
|
9
|
+
listPersistedRunEvents,
|
|
10
|
+
listPersistedRuns,
|
|
11
|
+
loadPersistedRun,
|
|
12
|
+
} from '@/lib/server/runtime/run-ledger'
|
|
13
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
14
|
+
|
|
15
|
+
import { state } from './state'
|
|
16
|
+
import type { SessionRunQueueEntry } from './types'
|
|
17
|
+
|
|
18
|
+
export function getSessionRunState(sessionId: string): {
|
|
19
|
+
runningRunId?: string
|
|
20
|
+
queueLength: number
|
|
21
|
+
} {
|
|
22
|
+
const summary = getSessionExecutionState(sessionId)
|
|
23
|
+
return {
|
|
24
|
+
runningRunId: summary.runningRunId,
|
|
25
|
+
queueLength: summary.queueLength,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function visibleQueuedEntriesForSession(sessionId: string): SessionRunQueueEntry[] {
|
|
30
|
+
return Array.from(state.queueByExecution.values())
|
|
31
|
+
.flatMap((queue) => queue)
|
|
32
|
+
.filter((entry) => entry.run.sessionId === sessionId && entry.run.internal !== true)
|
|
33
|
+
.sort((left, right) => left.run.queuedAt - right.run.queuedAt)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toQueuedTurn(entry: SessionRunQueueEntry, index: number): SessionQueuedTurn {
|
|
37
|
+
return {
|
|
38
|
+
runId: entry.run.id,
|
|
39
|
+
sessionId: entry.run.sessionId,
|
|
40
|
+
missionId: entry.run.missionId || null,
|
|
41
|
+
text: entry.message,
|
|
42
|
+
queuedAt: entry.run.queuedAt,
|
|
43
|
+
position: index + 1,
|
|
44
|
+
imagePath: entry.imagePath,
|
|
45
|
+
imageUrl: entry.imageUrl,
|
|
46
|
+
attachedFiles: entry.attachedFiles,
|
|
47
|
+
replyToId: entry.replyToId,
|
|
48
|
+
source: entry.run.source,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSessionQueueSnapshot(sessionId: string): SessionQueueSnapshot {
|
|
53
|
+
const execution = getSessionExecutionState(sessionId)
|
|
54
|
+
const visibleQueued = visibleQueuedEntriesForSession(sessionId)
|
|
55
|
+
return {
|
|
56
|
+
sessionId,
|
|
57
|
+
activeRunId: execution.runningRunId || null,
|
|
58
|
+
queueLength: visibleQueued.length,
|
|
59
|
+
items: visibleQueued.map((entry, index) => toQueuedTurn(entry, index)),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getSessionExecutionState(sessionId: string): {
|
|
64
|
+
runningRunId?: string
|
|
65
|
+
queueLength: number
|
|
66
|
+
hasRunning: boolean
|
|
67
|
+
hasQueued: boolean
|
|
68
|
+
hasRunningHeartbeat: boolean
|
|
69
|
+
hasQueuedHeartbeat: boolean
|
|
70
|
+
hasRunningNonHeartbeat: boolean
|
|
71
|
+
hasQueuedNonHeartbeat: boolean
|
|
72
|
+
} {
|
|
73
|
+
const running = Array.from(state.runningByExecution.values())
|
|
74
|
+
.find((entry) => entry.run.sessionId === sessionId)
|
|
75
|
+
const runningMatchesSession = Boolean(running)
|
|
76
|
+
const runningHeartbeat = Boolean(
|
|
77
|
+
runningMatchesSession
|
|
78
|
+
&& running
|
|
79
|
+
&& isInternalHeartbeatRun(running.run.internal, running.run.source),
|
|
80
|
+
)
|
|
81
|
+
const runningNonHeartbeat = Boolean(runningMatchesSession && !runningHeartbeat)
|
|
82
|
+
const queuedEntries = Array.from(state.queueByExecution.values())
|
|
83
|
+
.flatMap((queue) => queue)
|
|
84
|
+
.filter((entry) => entry.run.sessionId === sessionId)
|
|
85
|
+
const queuedHeartbeat = queuedEntries.filter((entry) =>
|
|
86
|
+
isInternalHeartbeatRun(entry.run.internal, entry.run.source),
|
|
87
|
+
).length
|
|
88
|
+
const queuedNonHeartbeat = queuedEntries.length - queuedHeartbeat
|
|
89
|
+
return {
|
|
90
|
+
runningRunId: (runningMatchesSession && running?.run.status === 'running')
|
|
91
|
+
? running.run.id
|
|
92
|
+
: undefined,
|
|
93
|
+
queueLength: queuedEntries.length,
|
|
94
|
+
hasRunning: Boolean(runningMatchesSession),
|
|
95
|
+
hasQueued: queuedEntries.length > 0,
|
|
96
|
+
hasRunningHeartbeat: runningHeartbeat,
|
|
97
|
+
hasQueuedHeartbeat: queuedHeartbeat > 0,
|
|
98
|
+
hasRunningNonHeartbeat: runningNonHeartbeat,
|
|
99
|
+
hasQueuedNonHeartbeat: queuedNonHeartbeat > 0,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getRunById(runId: string): SessionRunRecord | null {
|
|
104
|
+
return state.runs.get(runId) || loadPersistedRun(runId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function listRuns(params?: {
|
|
108
|
+
sessionId?: string
|
|
109
|
+
status?: SessionRunStatus
|
|
110
|
+
limit?: number
|
|
111
|
+
}): SessionRunRecord[] {
|
|
112
|
+
return listPersistedRuns(params)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function listRunEvents(runId: string, limit?: number): RunEventRecord[] {
|
|
116
|
+
return listPersistedRunEvents(runId, limit)
|
|
117
|
+
}
|