@swarmclawai/swarmclaw 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -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]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- 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/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- 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 +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- 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/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- 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 +3 -1
- 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 +10 -3
- 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/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 -1926
- 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/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- 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 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- 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/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- 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 -1470
- 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 +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- 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 -2061
- 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 +4 -2
- 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 -1377
- 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/schedules/schedule-repository.ts +42 -0
- 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/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { genId } from '@/lib/id'
|
|
2
|
+
import type { RunEventRecord, SessionRunRecord, SessionRunStatus, SSEEvent } from '@/types'
|
|
3
|
+
import {
|
|
4
|
+
isRuntimeLockActive,
|
|
5
|
+
releaseRuntimeLock,
|
|
6
|
+
tryAcquireRuntimeLock,
|
|
7
|
+
} from '@/lib/server/runtime/runtime-lock-repository'
|
|
8
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
9
|
+
import { log } from '@/lib/server/logger'
|
|
10
|
+
import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
|
|
11
|
+
import { cleanupSessionBrowser } from '@/lib/server/session-tools/web'
|
|
12
|
+
import { cancelDelegationJobsForParentSession } from '@/lib/server/agents/delegation-jobs'
|
|
13
|
+
import { getMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
|
|
14
|
+
import { observeAutonomyRunOutcome } from '@/lib/server/autonomy/supervisor-reflection'
|
|
15
|
+
import { observeLearnedSkillRunOutcome } from '@/lib/server/skills/learned-skills'
|
|
16
|
+
import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
|
|
17
|
+
import {
|
|
18
|
+
appendPersistedRunEvent,
|
|
19
|
+
patchPersistedRun,
|
|
20
|
+
persistRun,
|
|
21
|
+
} from '@/lib/server/runtime/run-ledger'
|
|
22
|
+
import { getActiveSessionProcess, stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
|
|
23
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
24
|
+
import type { SessionRunManagerState, SessionRunQueueEntry, SessionQueueMode } from './types'
|
|
25
|
+
|
|
26
|
+
export const MAX_RECENT_RUNS = 500
|
|
27
|
+
export const COLLECT_COALESCE_WINDOW_MS = 1500
|
|
28
|
+
export const SHARED_ACTIVITY_LEASE_TTL_MS = 15_000
|
|
29
|
+
export const SHARED_ACTIVITY_LEASE_RENEW_MS = 5_000
|
|
30
|
+
export const EXTERNAL_HOLD_TTL_MS = 60_000
|
|
31
|
+
export const MAX_DRAIN_DEPTH = 25
|
|
32
|
+
export const HEARTBEAT_BUSY_RETRY_MS = 1_000
|
|
33
|
+
export const STALE_QUEUED_RUN_MS = 15_000
|
|
34
|
+
export const STUCK_RUN_THRESHOLD_MS = 20 * 60_000
|
|
35
|
+
export const SHARED_ACTIVITY_LEASE_OWNER = `session-run:${process.pid}:${genId(6)}`
|
|
36
|
+
|
|
37
|
+
export const state: SessionRunManagerState = hmrSingleton<SessionRunManagerState>(
|
|
38
|
+
'__swarmclaw_session_run_manager__',
|
|
39
|
+
() => ({
|
|
40
|
+
runningByExecution: new Map<string, SessionRunQueueEntry>(),
|
|
41
|
+
queueByExecution: new Map<string, SessionRunQueueEntry[]>(),
|
|
42
|
+
runs: new Map<string, SessionRunRecord>(),
|
|
43
|
+
recentRunIds: [],
|
|
44
|
+
promises: new Map<string, Promise<import('@/lib/server/chat-execution/chat-execution').ExecuteChatTurnResult>>(),
|
|
45
|
+
deferredDrainTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
|
46
|
+
activityLeaseRenewTimers: new Map<string, ReturnType<typeof setInterval>>(),
|
|
47
|
+
externalSessionHolds: new Map<string, number>(),
|
|
48
|
+
externalHoldTimers: new Map<string, ReturnType<typeof setTimeout>>(),
|
|
49
|
+
drainDepth: new Map<string, number>(),
|
|
50
|
+
lastQueuedAt: 0,
|
|
51
|
+
nonHeartbeatWorkCount: new Map<string, number>(),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export const recoveryState = hmrSingleton('__swarmclaw_session_run_recovery__', () => ({ completed: false }))
|
|
56
|
+
|
|
57
|
+
if (!state.runningByExecution) state.runningByExecution = new Map<string, SessionRunQueueEntry>()
|
|
58
|
+
if (!state.queueByExecution) state.queueByExecution = new Map<string, SessionRunQueueEntry[]>()
|
|
59
|
+
if (!state.runs) state.runs = new Map<string, SessionRunRecord>()
|
|
60
|
+
if (!state.recentRunIds) state.recentRunIds = []
|
|
61
|
+
if (!state.promises) {
|
|
62
|
+
state.promises = new Map<string, Promise<import('@/lib/server/chat-execution/chat-execution').ExecuteChatTurnResult>>()
|
|
63
|
+
}
|
|
64
|
+
if (!state.deferredDrainTimers) state.deferredDrainTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
65
|
+
if (!state.activityLeaseRenewTimers) state.activityLeaseRenewTimers = new Map<string, ReturnType<typeof setInterval>>()
|
|
66
|
+
if (!state.externalSessionHolds) state.externalSessionHolds = new Map<string, number>()
|
|
67
|
+
if (!state.externalHoldTimers) state.externalHoldTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
68
|
+
if (!state.drainDepth) state.drainDepth = new Map<string, number>()
|
|
69
|
+
if (typeof state.lastQueuedAt !== 'number') state.lastQueuedAt = 0
|
|
70
|
+
if (!state.nonHeartbeatWorkCount) state.nonHeartbeatWorkCount = new Map<string, number>()
|
|
71
|
+
|
|
72
|
+
export function now() {
|
|
73
|
+
return Date.now()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function nextQueuedAt() {
|
|
77
|
+
const current = now()
|
|
78
|
+
const next = current <= state.lastQueuedAt ? state.lastQueuedAt + 1 : current
|
|
79
|
+
state.lastQueuedAt = next
|
|
80
|
+
return next
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function messagePreview(text: string): string {
|
|
84
|
+
return (text || '').replace(/\s+/g, ' ').trim().slice(0, 140)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function trimRecentRuns() {
|
|
88
|
+
while (state.recentRunIds.length > MAX_RECENT_RUNS) {
|
|
89
|
+
const id = state.recentRunIds.shift()
|
|
90
|
+
if (!id) continue
|
|
91
|
+
state.runs.delete(id)
|
|
92
|
+
state.promises.delete(id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function syncRunRecord(run: SessionRunRecord): SessionRunRecord {
|
|
97
|
+
state.runs.set(run.id, run)
|
|
98
|
+
persistRun(run)
|
|
99
|
+
return run
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function registerRun(run: SessionRunRecord) {
|
|
103
|
+
syncRunRecord(run)
|
|
104
|
+
state.recentRunIds.push(run.id)
|
|
105
|
+
trimRecentRuns()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function shouldPersistRunEvent(event: SSEEvent): boolean {
|
|
109
|
+
return event.t !== 'd' && event.t !== 'thinking' && event.t !== 'reset'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function persistEventForRun(entry: SessionRunQueueEntry, event: SSEEvent, opts?: {
|
|
113
|
+
phase?: RunEventRecord['phase']
|
|
114
|
+
status?: SessionRunStatus
|
|
115
|
+
summary?: string
|
|
116
|
+
}): void {
|
|
117
|
+
if (!shouldPersistRunEvent(event)) return
|
|
118
|
+
appendPersistedRunEvent({
|
|
119
|
+
runId: entry.run.id,
|
|
120
|
+
sessionId: entry.run.sessionId,
|
|
121
|
+
phase: opts?.phase || 'event',
|
|
122
|
+
status: opts?.status,
|
|
123
|
+
summary: opts?.summary,
|
|
124
|
+
event,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function chainCallerSignal(callerSignal: AbortSignal, controller: AbortController): void {
|
|
129
|
+
if (callerSignal.aborted) {
|
|
130
|
+
controller.abort()
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const onAbort = () => controller.abort()
|
|
134
|
+
callerSignal.addEventListener('abort', onAbort, { once: true })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function emitToSubscribers(entry: SessionRunQueueEntry, event: SSEEvent) {
|
|
138
|
+
persistEventForRun(entry, event)
|
|
139
|
+
for (const send of entry.onEvents) {
|
|
140
|
+
try {
|
|
141
|
+
send(event)
|
|
142
|
+
} catch {
|
|
143
|
+
// Subscriber stream can be closed by the client.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function emitRunMeta(entry: SessionRunQueueEntry, status: SessionRunStatus, extra?: Record<string, unknown>) {
|
|
149
|
+
const event: SSEEvent = {
|
|
150
|
+
t: 'md',
|
|
151
|
+
text: JSON.stringify({
|
|
152
|
+
run: {
|
|
153
|
+
id: entry.run.id,
|
|
154
|
+
sessionId: entry.run.sessionId,
|
|
155
|
+
status,
|
|
156
|
+
source: entry.run.source,
|
|
157
|
+
internal: entry.run.internal,
|
|
158
|
+
...extra,
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
}
|
|
162
|
+
persistEventForRun(entry, event, { phase: 'status', status })
|
|
163
|
+
for (const send of entry.onEvents) {
|
|
164
|
+
try {
|
|
165
|
+
send(event)
|
|
166
|
+
} catch {
|
|
167
|
+
// Subscriber stream can be closed by the client.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
notifySessionRunState(entry.run.sessionId)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function notifySessionRunState(sessionId: string): void {
|
|
174
|
+
notify('runs')
|
|
175
|
+
notify('sessions')
|
|
176
|
+
notify(`session:${sessionId}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function queueAutonomyObservation(input: {
|
|
180
|
+
runId: string
|
|
181
|
+
sessionId: string
|
|
182
|
+
source: string
|
|
183
|
+
status: SessionRunStatus
|
|
184
|
+
resultText?: string | null
|
|
185
|
+
error?: string | null
|
|
186
|
+
toolEvents?: import('@/lib/server/chat-execution/chat-execution').ExecuteChatTurnResult['toolEvents']
|
|
187
|
+
sourceMessage?: string | null
|
|
188
|
+
}) {
|
|
189
|
+
const session = getSession(input.sessionId)
|
|
190
|
+
void observeAutonomyRunOutcome({
|
|
191
|
+
runId: input.runId,
|
|
192
|
+
sessionId: input.sessionId,
|
|
193
|
+
agentId: session?.agentId || null,
|
|
194
|
+
source: input.source,
|
|
195
|
+
status: input.status,
|
|
196
|
+
resultText: input.resultText,
|
|
197
|
+
error: input.error || undefined,
|
|
198
|
+
toolEvents: input.toolEvents,
|
|
199
|
+
mainLoopState: getMainLoopStateForSession(input.sessionId),
|
|
200
|
+
sourceMessage: input.sourceMessage,
|
|
201
|
+
}).then(({ reflection }) => observeLearnedSkillRunOutcome({
|
|
202
|
+
runId: input.runId,
|
|
203
|
+
sessionId: input.sessionId,
|
|
204
|
+
agentId: session?.agentId || null,
|
|
205
|
+
source: input.source,
|
|
206
|
+
status: input.status,
|
|
207
|
+
resultText: input.resultText,
|
|
208
|
+
error: input.error || undefined,
|
|
209
|
+
toolEvents: input.toolEvents,
|
|
210
|
+
reflection,
|
|
211
|
+
})).catch((err: unknown) => {
|
|
212
|
+
log.warn('session-run', `Autonomy observation failed for ${input.runId}`, {
|
|
213
|
+
sessionId: input.sessionId,
|
|
214
|
+
error: errorMessage(err),
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function markRunningEntryCancelled(entry: SessionRunQueueEntry, reason: string) {
|
|
220
|
+
if (entry.run.status === 'cancelled') return
|
|
221
|
+
entry.run.status = 'cancelled'
|
|
222
|
+
entry.run.endedAt = now()
|
|
223
|
+
entry.run.error = reason
|
|
224
|
+
syncRunRecord(entry.run)
|
|
225
|
+
emitRunMeta(entry, 'cancelled', { reason })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function abortSessionRuntime(entry: SessionRunQueueEntry, reason: string) {
|
|
229
|
+
markRunningEntryCancelled(entry, reason)
|
|
230
|
+
entry.signalController.abort()
|
|
231
|
+
try { getActiveSessionProcess(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
|
|
232
|
+
stopActiveSessionProcess(entry.run.sessionId)
|
|
233
|
+
try { cleanupSessionBrowser(entry.run.sessionId) } catch { /* noop */ }
|
|
234
|
+
try { cancelDelegationJobsForParentSession(entry.run.sessionId, reason) } catch { /* noop */ }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function executionKeyForSession(sessionId: string): string {
|
|
238
|
+
return `session:${sessionId}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function nonHeartbeatActivityLeaseName(sessionId: string): string {
|
|
242
|
+
return `session-non-heartbeat:${sessionId}`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function hasActiveNonHeartbeatSessionLease(sessionId: string): boolean {
|
|
246
|
+
return isRuntimeLockActive(nonHeartbeatActivityLeaseName(sessionId))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function hasExternalSessionExecutionHold(sessionId: string): boolean {
|
|
250
|
+
return (state.externalSessionHolds.get(sessionId) || 0) > 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function acquireExternalSessionExecutionHold(
|
|
254
|
+
sessionId: string,
|
|
255
|
+
onRelease: (executionKey: string) => void,
|
|
256
|
+
): () => void {
|
|
257
|
+
const current = state.externalSessionHolds.get(sessionId) || 0
|
|
258
|
+
state.externalSessionHolds.set(sessionId, current + 1)
|
|
259
|
+
let released = false
|
|
260
|
+
const holdKey = `${sessionId}:${current + 1}`
|
|
261
|
+
const ttlTimer = setTimeout(() => {
|
|
262
|
+
if (released) return
|
|
263
|
+
log.warn('session-run', 'External hold auto-released after TTL', { sessionId, holdKey, ttlMs: EXTERNAL_HOLD_TTL_MS })
|
|
264
|
+
release()
|
|
265
|
+
}, EXTERNAL_HOLD_TTL_MS)
|
|
266
|
+
state.externalHoldTimers.set(holdKey, ttlTimer)
|
|
267
|
+
const release = () => {
|
|
268
|
+
if (released) return
|
|
269
|
+
released = true
|
|
270
|
+
const timer = state.externalHoldTimers.get(holdKey)
|
|
271
|
+
if (timer) {
|
|
272
|
+
clearTimeout(timer)
|
|
273
|
+
state.externalHoldTimers.delete(holdKey)
|
|
274
|
+
}
|
|
275
|
+
const next = (state.externalSessionHolds.get(sessionId) || 1) - 1
|
|
276
|
+
if (next > 0) state.externalSessionHolds.set(sessionId, next)
|
|
277
|
+
else state.externalSessionHolds.delete(sessionId)
|
|
278
|
+
onRelease(executionKeyForSession(sessionId))
|
|
279
|
+
}
|
|
280
|
+
return release
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function queueForExecution(executionKey: string): SessionRunQueueEntry[] {
|
|
284
|
+
const existing = state.queueByExecution.get(executionKey)
|
|
285
|
+
if (existing) return existing
|
|
286
|
+
const created: SessionRunQueueEntry[] = []
|
|
287
|
+
state.queueByExecution.set(executionKey, created)
|
|
288
|
+
return created
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function normalizeMode(mode: string | undefined, internal: boolean): SessionQueueMode {
|
|
292
|
+
if (mode === 'steer' || mode === 'collect' || mode === 'followup') return mode
|
|
293
|
+
return internal ? 'collect' : 'followup'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function markPersistedRunInterrupted(run: SessionRunRecord, reason: string): SessionRunRecord {
|
|
297
|
+
const interruptedAt = now()
|
|
298
|
+
const next = patchPersistedRun(run.id, (current) => {
|
|
299
|
+
const target = current || run
|
|
300
|
+
return {
|
|
301
|
+
...target,
|
|
302
|
+
status: 'cancelled',
|
|
303
|
+
endedAt: target.endedAt || interruptedAt,
|
|
304
|
+
interruptedAt,
|
|
305
|
+
interruptedReason: reason,
|
|
306
|
+
error: target.error || reason,
|
|
307
|
+
}
|
|
308
|
+
}) || {
|
|
309
|
+
...run,
|
|
310
|
+
status: 'cancelled',
|
|
311
|
+
endedAt: run.endedAt || interruptedAt,
|
|
312
|
+
interruptedAt,
|
|
313
|
+
interruptedReason: reason,
|
|
314
|
+
error: run.error || reason,
|
|
315
|
+
}
|
|
316
|
+
state.runs.set(next.id, next)
|
|
317
|
+
if (!state.recentRunIds.includes(next.id)) {
|
|
318
|
+
state.recentRunIds.push(next.id)
|
|
319
|
+
trimRecentRuns()
|
|
320
|
+
}
|
|
321
|
+
appendPersistedRunEvent({
|
|
322
|
+
runId: next.id,
|
|
323
|
+
sessionId: next.sessionId,
|
|
324
|
+
phase: 'status',
|
|
325
|
+
status: 'cancelled',
|
|
326
|
+
summary: reason,
|
|
327
|
+
event: {
|
|
328
|
+
t: 'md',
|
|
329
|
+
text: JSON.stringify({
|
|
330
|
+
run: {
|
|
331
|
+
id: next.id,
|
|
332
|
+
sessionId: next.sessionId,
|
|
333
|
+
status: 'cancelled',
|
|
334
|
+
interrupted: true,
|
|
335
|
+
reason,
|
|
336
|
+
},
|
|
337
|
+
}),
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
return next
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isNonHeartbeatEntry(entry: SessionRunQueueEntry): boolean {
|
|
344
|
+
return !isInternalHeartbeatRun(entry.run.internal, entry.run.source)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function incrementNonHeartbeatWork(entry: SessionRunQueueEntry): void {
|
|
348
|
+
if (!isNonHeartbeatEntry(entry)) return
|
|
349
|
+
entry.nonHeartbeatCounted = true
|
|
350
|
+
state.nonHeartbeatWorkCount.set(entry.run.sessionId, (state.nonHeartbeatWorkCount.get(entry.run.sessionId) || 0) + 1)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function decrementNonHeartbeatWork(entry: SessionRunQueueEntry): void {
|
|
354
|
+
if (!entry.nonHeartbeatCounted) return
|
|
355
|
+
entry.nonHeartbeatCounted = false
|
|
356
|
+
const sessionId = entry.run.sessionId
|
|
357
|
+
const count = (state.nonHeartbeatWorkCount.get(sessionId) || 0) - 1
|
|
358
|
+
if (count <= 0) state.nonHeartbeatWorkCount.delete(sessionId)
|
|
359
|
+
else state.nonHeartbeatWorkCount.set(sessionId, count)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function hasLocalNonHeartbeatWork(sessionId: string): boolean {
|
|
363
|
+
return (state.nonHeartbeatWorkCount.get(sessionId) || 0) > 0
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function clearDeferredDrain(executionKey: string): void {
|
|
367
|
+
const timer = state.deferredDrainTimers.get(executionKey)
|
|
368
|
+
if (!timer) return
|
|
369
|
+
clearTimeout(timer)
|
|
370
|
+
state.deferredDrainTimers.delete(executionKey)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function deleteQueueEntry(queue: SessionRunQueueEntry[], target: SessionRunQueueEntry): boolean {
|
|
374
|
+
const idx = queue.indexOf(target)
|
|
375
|
+
if (idx === -1) return false
|
|
376
|
+
queue.splice(idx, 1)
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function scheduleDeferredDrain(
|
|
381
|
+
executionKey: string,
|
|
382
|
+
onDrain: (executionKey: string) => void,
|
|
383
|
+
delayMs = HEARTBEAT_BUSY_RETRY_MS,
|
|
384
|
+
): void {
|
|
385
|
+
if (state.deferredDrainTimers.has(executionKey)) return
|
|
386
|
+
const timer = setTimeout(() => {
|
|
387
|
+
state.deferredDrainTimers.delete(executionKey)
|
|
388
|
+
onDrain(executionKey)
|
|
389
|
+
}, delayMs)
|
|
390
|
+
state.deferredDrainTimers.set(executionKey, timer)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function stopSessionActivityLease(sessionId: string): void {
|
|
394
|
+
const timer = state.activityLeaseRenewTimers.get(sessionId)
|
|
395
|
+
if (timer) {
|
|
396
|
+
clearInterval(timer)
|
|
397
|
+
state.activityLeaseRenewTimers.delete(sessionId)
|
|
398
|
+
}
|
|
399
|
+
releaseRuntimeLock(nonHeartbeatActivityLeaseName(sessionId), SHARED_ACTIVITY_LEASE_OWNER)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function startSessionActivityLease(sessionId: string): void {
|
|
403
|
+
if (state.activityLeaseRenewTimers.has(sessionId)) return
|
|
404
|
+
const leaseName = nonHeartbeatActivityLeaseName(sessionId)
|
|
405
|
+
tryAcquireRuntimeLock(leaseName, SHARED_ACTIVITY_LEASE_OWNER, SHARED_ACTIVITY_LEASE_TTL_MS)
|
|
406
|
+
const timer = setInterval(() => {
|
|
407
|
+
if (!hasLocalNonHeartbeatWork(sessionId)) {
|
|
408
|
+
stopSessionActivityLease(sessionId)
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
tryAcquireRuntimeLock(leaseName, SHARED_ACTIVITY_LEASE_OWNER, SHARED_ACTIVITY_LEASE_TTL_MS)
|
|
412
|
+
}, SHARED_ACTIVITY_LEASE_RENEW_MS)
|
|
413
|
+
state.activityLeaseRenewTimers.set(sessionId, timer)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function reconcileSessionActivityLease(sessionId: string): void {
|
|
417
|
+
if (hasLocalNonHeartbeatWork(sessionId)) startSessionActivityLease(sessionId)
|
|
418
|
+
else stopSessionActivityLease(sessionId)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function resetSessionRunManagerStateForTests(): void {
|
|
422
|
+
recoveryState.completed = false
|
|
423
|
+
for (const timer of state.deferredDrainTimers.values()) clearTimeout(timer)
|
|
424
|
+
state.deferredDrainTimers.clear()
|
|
425
|
+
for (const [sessionId, timer] of state.activityLeaseRenewTimers.entries()) {
|
|
426
|
+
clearInterval(timer)
|
|
427
|
+
releaseRuntimeLock(nonHeartbeatActivityLeaseName(sessionId), SHARED_ACTIVITY_LEASE_OWNER)
|
|
428
|
+
}
|
|
429
|
+
state.activityLeaseRenewTimers.clear()
|
|
430
|
+
state.runningByExecution.clear()
|
|
431
|
+
state.queueByExecution.clear()
|
|
432
|
+
state.runs.clear()
|
|
433
|
+
state.recentRunIds.length = 0
|
|
434
|
+
state.promises.clear()
|
|
435
|
+
state.externalSessionHolds.clear()
|
|
436
|
+
for (const timer of state.externalHoldTimers.values()) clearTimeout(timer)
|
|
437
|
+
state.externalHoldTimers.clear()
|
|
438
|
+
state.nonHeartbeatWorkCount.clear()
|
|
439
|
+
state.drainDepth.clear()
|
|
440
|
+
state.lastQueuedAt = 0
|
|
441
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution'
|
|
2
|
+
import type {
|
|
3
|
+
SessionRunHeartbeatConfig,
|
|
4
|
+
SessionRunRecord,
|
|
5
|
+
SSEEvent,
|
|
6
|
+
} from '@/types'
|
|
7
|
+
|
|
8
|
+
export type SessionQueueMode = 'followup' | 'steer' | 'collect'
|
|
9
|
+
|
|
10
|
+
export interface SessionRunQueueEntry {
|
|
11
|
+
executionKey: string
|
|
12
|
+
run: SessionRunRecord
|
|
13
|
+
message: string
|
|
14
|
+
imagePath?: string
|
|
15
|
+
imageUrl?: string
|
|
16
|
+
attachedFiles?: string[]
|
|
17
|
+
onEvents: Array<(event: SSEEvent) => void>
|
|
18
|
+
signalController: AbortController
|
|
19
|
+
maxRuntimeMs?: number
|
|
20
|
+
modelOverride?: string
|
|
21
|
+
heartbeatConfig?: SessionRunHeartbeatConfig
|
|
22
|
+
replyToId?: string
|
|
23
|
+
resolve: (value: ExecuteChatTurnResult) => void
|
|
24
|
+
reject: (error: Error) => void
|
|
25
|
+
promise: Promise<ExecuteChatTurnResult>
|
|
26
|
+
nonHeartbeatCounted?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionRunManagerState {
|
|
30
|
+
runningByExecution: Map<string, SessionRunQueueEntry>
|
|
31
|
+
queueByExecution: Map<string, SessionRunQueueEntry[]>
|
|
32
|
+
runs: Map<string, SessionRunRecord>
|
|
33
|
+
recentRunIds: string[]
|
|
34
|
+
promises: Map<string, Promise<ExecuteChatTurnResult>>
|
|
35
|
+
deferredDrainTimers: Map<string, ReturnType<typeof setTimeout>>
|
|
36
|
+
activityLeaseRenewTimers: Map<string, ReturnType<typeof setInterval>>
|
|
37
|
+
externalSessionHolds: Map<string, number>
|
|
38
|
+
externalHoldTimers: Map<string, ReturnType<typeof setTimeout>>
|
|
39
|
+
drainDepth: Map<string, number>
|
|
40
|
+
lastQueuedAt: number
|
|
41
|
+
nonHeartbeatWorkCount: Map<string, number>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EnqueueSessionRunInput {
|
|
45
|
+
sessionId: string
|
|
46
|
+
message: string
|
|
47
|
+
missionId?: string | null
|
|
48
|
+
imagePath?: string
|
|
49
|
+
imageUrl?: string
|
|
50
|
+
attachedFiles?: string[]
|
|
51
|
+
internal?: boolean
|
|
52
|
+
source?: string
|
|
53
|
+
mode?: SessionQueueMode
|
|
54
|
+
onEvent?: (event: SSEEvent) => void
|
|
55
|
+
dedupeKey?: string
|
|
56
|
+
maxRuntimeMs?: number
|
|
57
|
+
modelOverride?: string
|
|
58
|
+
heartbeatConfig?: SessionRunHeartbeatConfig
|
|
59
|
+
replyToId?: string
|
|
60
|
+
executionGroupKey?: string
|
|
61
|
+
callerSignal?: AbortSignal
|
|
62
|
+
recoveredFromRestart?: boolean
|
|
63
|
+
recoveredFromRunId?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EnqueueSessionRunResult {
|
|
67
|
+
runId: string
|
|
68
|
+
position: number
|
|
69
|
+
deduped?: boolean
|
|
70
|
+
coalesced?: boolean
|
|
71
|
+
promise: Promise<ExecuteChatTurnResult>
|
|
72
|
+
abort: () => void
|
|
73
|
+
unsubscribe: () => void
|
|
74
|
+
}
|