@swarmclawai/swarmclaw 0.6.7 → 0.7.0
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 +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -5,6 +5,7 @@ const MAX_LOG_CHARS = 200_000
|
|
|
5
5
|
const DEFAULT_BACKGROUND_YIELD_MS = 10_000
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 30 * 60_000
|
|
7
7
|
const DEFAULT_TTL_MS = 30 * 60_000
|
|
8
|
+
const BACKGROUND_STARTUP_GRACE_MS = 500
|
|
8
9
|
|
|
9
10
|
export type ProcessStatus = 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
|
|
10
11
|
|
|
@@ -170,6 +171,23 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
|
|
|
170
171
|
state.exitWaiters.set(id, exitPromise)
|
|
171
172
|
|
|
172
173
|
if (opts.background) {
|
|
174
|
+
// Give background processes a brief grace window so immediate crashes
|
|
175
|
+
// (e.g., bind/permission errors) are surfaced instead of misreported as running.
|
|
176
|
+
const startupWaitMs = Math.min(
|
|
177
|
+
Math.max(100, BACKGROUND_STARTUP_GRACE_MS),
|
|
178
|
+
Math.max(200, timeoutMs),
|
|
179
|
+
)
|
|
180
|
+
await wait(startupWaitMs)
|
|
181
|
+
const rec = state.records.get(id)
|
|
182
|
+
if (rec && rec.status !== 'running') {
|
|
183
|
+
return {
|
|
184
|
+
status: 'completed',
|
|
185
|
+
processId: id,
|
|
186
|
+
output: rec.log,
|
|
187
|
+
exitCode: rec.exitCode,
|
|
188
|
+
signal: rec.signal,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
173
191
|
return {
|
|
174
192
|
status: 'running',
|
|
175
193
|
processId: id,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { loadAgents, loadSettings, loadCredentials, decryptKey } from './storage'
|
|
2
|
+
import { getProvider } from '../providers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expands a single user query into multiple semantic search variants
|
|
6
|
+
* to improve vector database recall (OpenClaw-style).
|
|
7
|
+
*/
|
|
8
|
+
export async function expandQuery(query: string): Promise<string[]> {
|
|
9
|
+
const agents = loadAgents()
|
|
10
|
+
const settings = loadSettings()
|
|
11
|
+
const defaultAgent = agents[settings.defaultAgentId]
|
|
12
|
+
if (!defaultAgent) return [query]
|
|
13
|
+
|
|
14
|
+
const providerEntry = getProvider(defaultAgent.provider)
|
|
15
|
+
if (!providerEntry?.handler?.streamChat) return [query]
|
|
16
|
+
|
|
17
|
+
const creds = loadCredentials()
|
|
18
|
+
const cred = creds[defaultAgent.credentialId || '']
|
|
19
|
+
const apiKey = cred ? decryptKey(cred.encryptedKey) : undefined
|
|
20
|
+
|
|
21
|
+
const systemPrompt = `You are a search query expansion assistant.
|
|
22
|
+
Given a user's question, generate 3 different semantic search queries that would help find the answer in a vector database.
|
|
23
|
+
Use different vocabulary and focus on different aspects of the intent.
|
|
24
|
+
Format your response as a simple newline-separated list. No numbering, no bullets, no introduction.`
|
|
25
|
+
|
|
26
|
+
let expanded = ''
|
|
27
|
+
try {
|
|
28
|
+
await providerEntry.handler.streamChat({
|
|
29
|
+
session: { id: 'expansion', messages: [], model: defaultAgent.model, provider: defaultAgent.provider },
|
|
30
|
+
message: query,
|
|
31
|
+
apiKey,
|
|
32
|
+
systemPrompt,
|
|
33
|
+
write: (raw: string) => {
|
|
34
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (!line.startsWith('data: ')) continue
|
|
37
|
+
try {
|
|
38
|
+
const ev = JSON.parse(line.slice(6))
|
|
39
|
+
if (ev.t === 'd' && ev.text) expanded += ev.text
|
|
40
|
+
} catch { /* skip */ }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
active: new Map(),
|
|
44
|
+
loadHistory: () => [],
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const variants = expanded.split('\n').map(l => l.trim()).filter(Boolean)
|
|
48
|
+
if (variants.length > 0) {
|
|
49
|
+
// Return original query + variants
|
|
50
|
+
return [query, ...variants.slice(0, 3)]
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('[query-expansion] Failed to expand query:', err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [query]
|
|
57
|
+
}
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { extractTaskResult, formatResultBody } from './task-result'
|
|
|
13
13
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
14
14
|
import { isProtectedMainSession } from './main-session'
|
|
15
15
|
import { cascadeUnblock } from './dag-validation'
|
|
16
|
+
import { performGuardianRollback } from './guardian'
|
|
16
17
|
import type { Agent, BoardTask, Connector, Message } from '@/types'
|
|
17
18
|
|
|
18
19
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
@@ -22,12 +23,14 @@ interface SessionMessageLike {
|
|
|
22
23
|
role?: string
|
|
23
24
|
text?: string
|
|
24
25
|
time?: number
|
|
25
|
-
kind?:
|
|
26
|
+
kind?: string
|
|
26
27
|
source?: {
|
|
27
28
|
connectorId?: string
|
|
28
29
|
channelId?: string
|
|
29
30
|
}
|
|
30
31
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
32
|
+
streaming?: boolean
|
|
33
|
+
imageUrl?: string
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
interface SessionLike {
|
|
@@ -96,6 +99,181 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
|
|
|
96
99
|
if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
const DEV_TASK_HINT = /\b(dev(?:\s+server)?|start(?:ing)?\s+(?:the\s+)?server|run(?:ning)?\s+(?:the\s+)?(?:app|project|site)|serve|localhost|http\s+server|web\s+server|npm\b|pnpm\b|yarn\b|bun\b|vite|next(?:\.js)?|react|build|compile)\b/i
|
|
103
|
+
const TASK_CWD_NOISE_DIRS = new Set([
|
|
104
|
+
'uploads',
|
|
105
|
+
'data',
|
|
106
|
+
'projects',
|
|
107
|
+
'tasks',
|
|
108
|
+
'.swarm-data-test',
|
|
109
|
+
'.git',
|
|
110
|
+
'.next',
|
|
111
|
+
'node_modules',
|
|
112
|
+
])
|
|
113
|
+
const PROJECT_MARKER_FILES = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', '.git']
|
|
114
|
+
const SOURCE_MARKER_DIRS = ['src', 'app', 'public', 'pages']
|
|
115
|
+
const WORKSPACE_PROJECTS_DIR = path.join(WORKSPACE_DIR, 'projects')
|
|
116
|
+
|
|
117
|
+
interface WorkspaceDirCandidate {
|
|
118
|
+
dir: string
|
|
119
|
+
name: string
|
|
120
|
+
hasProjectMarker: boolean
|
|
121
|
+
hasSourceMarker: boolean
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let workspaceDirCache: { expiresAt: number; candidates: WorkspaceDirCandidate[] } | null = null
|
|
125
|
+
|
|
126
|
+
function isExistingDirectory(dirPath: string): boolean {
|
|
127
|
+
try {
|
|
128
|
+
return fs.statSync(dirPath).isDirectory()
|
|
129
|
+
} catch {
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isWithinDirectory(parent: string, child: string): boolean {
|
|
135
|
+
const parentResolved = path.resolve(parent)
|
|
136
|
+
const childResolved = path.resolve(child)
|
|
137
|
+
const rel = path.relative(parentResolved, childResolved)
|
|
138
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeForMatch(value: string): string {
|
|
142
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasAnyMarker(dirPath: string, markers: string[]): boolean {
|
|
146
|
+
return markers.some((marker) => fs.existsSync(path.join(dirPath, marker)))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeDirCandidate(raw: unknown, baseDir: string): string | null {
|
|
150
|
+
if (typeof raw !== 'string') return null
|
|
151
|
+
const trimmed = raw.trim()
|
|
152
|
+
if (!trimmed) return null
|
|
153
|
+
const homeDir = process.env.HOME || ''
|
|
154
|
+
const expanded = trimmed === '~'
|
|
155
|
+
? homeDir
|
|
156
|
+
: trimmed.startsWith('~/')
|
|
157
|
+
? path.join(homeDir, trimmed.slice(2))
|
|
158
|
+
: trimmed
|
|
159
|
+
const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(baseDir, expanded)
|
|
160
|
+
return isExistingDirectory(resolved) ? resolved : null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function looksLikeDevTask(task: Pick<BoardTask, 'title' | 'description'>): boolean {
|
|
164
|
+
const text = `${task.title || ''} ${task.description || ''}`.trim()
|
|
165
|
+
return DEV_TASK_HINT.test(text)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function listWorkspaceDirCandidates(): WorkspaceDirCandidate[] {
|
|
169
|
+
const now = Date.now()
|
|
170
|
+
if (workspaceDirCache && workspaceDirCache.expiresAt > now) return workspaceDirCache.candidates
|
|
171
|
+
|
|
172
|
+
const candidates: WorkspaceDirCandidate[] = []
|
|
173
|
+
const seen = new Set<string>()
|
|
174
|
+
const roots = [WORKSPACE_DIR, WORKSPACE_PROJECTS_DIR]
|
|
175
|
+
|
|
176
|
+
for (const root of roots) {
|
|
177
|
+
if (!isExistingDirectory(root)) continue
|
|
178
|
+
let entries: fs.Dirent[] = []
|
|
179
|
+
try {
|
|
180
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
181
|
+
} catch {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isDirectory()) continue
|
|
186
|
+
const name = entry.name
|
|
187
|
+
if (!name || name.startsWith('.')) continue
|
|
188
|
+
if (TASK_CWD_NOISE_DIRS.has(name)) continue
|
|
189
|
+
const dir = path.join(root, name)
|
|
190
|
+
const key = path.resolve(dir)
|
|
191
|
+
if (seen.has(key)) continue
|
|
192
|
+
seen.add(key)
|
|
193
|
+
candidates.push({
|
|
194
|
+
dir: key,
|
|
195
|
+
name,
|
|
196
|
+
hasProjectMarker: hasAnyMarker(key, PROJECT_MARKER_FILES),
|
|
197
|
+
hasSourceMarker: hasAnyMarker(key, SOURCE_MARKER_DIRS),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
candidates.sort((a, b) => a.name.localeCompare(b.name))
|
|
203
|
+
workspaceDirCache = {
|
|
204
|
+
expiresAt: now + 15_000,
|
|
205
|
+
candidates,
|
|
206
|
+
}
|
|
207
|
+
return candidates
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function inferWorkspaceProjectCwd(task: Pick<BoardTask, 'title' | 'description' | 'file'>): string | null {
|
|
211
|
+
const candidates = listWorkspaceDirCandidates()
|
|
212
|
+
if (!candidates.length) return null
|
|
213
|
+
|
|
214
|
+
const taskText = normalizeForMatch(`${task.title || ''} ${task.description || ''} ${task.file || ''}`)
|
|
215
|
+
const devTask = looksLikeDevTask(task)
|
|
216
|
+
const markerCandidates = candidates.filter((candidate) => candidate.hasProjectMarker)
|
|
217
|
+
|
|
218
|
+
let best: { dir: string; score: number } | null = null
|
|
219
|
+
for (const candidate of candidates) {
|
|
220
|
+
const nameNorm = normalizeForMatch(candidate.name)
|
|
221
|
+
if (!nameNorm) continue
|
|
222
|
+
let score = 0
|
|
223
|
+
if (taskText.includes(nameNorm)) score += 8
|
|
224
|
+
for (const token of nameNorm.split(' ')) {
|
|
225
|
+
if (token.length < 3) continue
|
|
226
|
+
if (taskText.includes(token)) score += 1
|
|
227
|
+
}
|
|
228
|
+
if (candidate.hasProjectMarker) score += devTask ? 3 : 1
|
|
229
|
+
if (candidate.hasSourceMarker) score += 1
|
|
230
|
+
if (!best || score > best.score) best = { dir: candidate.dir, score }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (best && best.score >= 4) return best.dir
|
|
234
|
+
if (devTask && markerCandidates.length === 1) return markerCandidates[0].dir
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveTaskExecutionCwd(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string {
|
|
239
|
+
const workspaceRoot = path.resolve(WORKSPACE_DIR)
|
|
240
|
+
|
|
241
|
+
const explicitCwd = normalizeDirCandidate(task.cwd, workspaceRoot)
|
|
242
|
+
if (explicitCwd) return explicitCwd
|
|
243
|
+
|
|
244
|
+
const projectId = typeof task.projectId === 'string' ? task.projectId.trim() : ''
|
|
245
|
+
if (projectId) {
|
|
246
|
+
const projectDir = path.join(WORKSPACE_PROJECTS_DIR, projectId)
|
|
247
|
+
if (isExistingDirectory(projectDir)) return projectDir
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const fileRef = typeof task.file === 'string' ? task.file.trim() : ''
|
|
251
|
+
if (fileRef) {
|
|
252
|
+
const filePath = path.isAbsolute(fileRef) ? fileRef : path.resolve(workspaceRoot, fileRef)
|
|
253
|
+
const fileDir = isExistingDirectory(filePath) ? filePath : path.dirname(filePath)
|
|
254
|
+
if (isExistingDirectory(fileDir) && isWithinDirectory(workspaceRoot, fileDir)) return fileDir
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const inferredCwd = inferWorkspaceProjectCwd(task)
|
|
258
|
+
if (inferredCwd) return inferredCwd
|
|
259
|
+
|
|
260
|
+
const sourceSessionId = typeof task.createdInSessionId === 'string' ? task.createdInSessionId.trim() : ''
|
|
261
|
+
const sourceSessionCwd = sourceSessionId
|
|
262
|
+
? normalizeDirCandidate(sessions[sourceSessionId]?.cwd, workspaceRoot)
|
|
263
|
+
: null
|
|
264
|
+
if (sourceSessionCwd && path.resolve(sourceSessionCwd) !== workspaceRoot) return sourceSessionCwd
|
|
265
|
+
|
|
266
|
+
const runSessionId = typeof task.sessionId === 'string' ? task.sessionId.trim() : ''
|
|
267
|
+
const runSessionCwd = runSessionId
|
|
268
|
+
? normalizeDirCandidate(sessions[runSessionId]?.cwd, workspaceRoot)
|
|
269
|
+
: null
|
|
270
|
+
if (runSessionCwd && path.resolve(runSessionCwd) !== workspaceRoot) return runSessionCwd
|
|
271
|
+
|
|
272
|
+
const sandboxDir = path.join(workspaceRoot, 'tasks', task.id)
|
|
273
|
+
fs.mkdirSync(sandboxDir, { recursive: true })
|
|
274
|
+
return sandboxDir
|
|
275
|
+
}
|
|
276
|
+
|
|
99
277
|
function queueContains(queue: string[], id: string): boolean {
|
|
100
278
|
return queue.includes(id)
|
|
101
279
|
}
|
|
@@ -315,12 +493,34 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
315
493
|
// Task result extraction now uses Zod-validated structured data
|
|
316
494
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
317
495
|
|
|
496
|
+
/** Check if a task result looks incomplete (agent stopped mid-objective). */
|
|
497
|
+
function looksIncomplete(text: string): boolean {
|
|
498
|
+
if (!text) return false
|
|
499
|
+
const trimmed = text.trim()
|
|
500
|
+
// Ends with ellipsis or continuation signal
|
|
501
|
+
if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
|
|
502
|
+
// Ends with a step/phase header (agent was listing next steps)
|
|
503
|
+
if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
|
|
504
|
+
// Contains forward-looking language at the end
|
|
505
|
+
const lastChunk = trimmed.slice(-300).toLowerCase()
|
|
506
|
+
if (/\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)) return true
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
|
|
318
510
|
async function executeTaskRun(
|
|
319
511
|
task: BoardTask,
|
|
320
512
|
agent: Agent,
|
|
321
513
|
sessionId: string,
|
|
322
514
|
): Promise<string> {
|
|
323
|
-
const
|
|
515
|
+
const basePrompt = task.description || task.title
|
|
516
|
+
const prompt = [
|
|
517
|
+
basePrompt,
|
|
518
|
+
'',
|
|
519
|
+
'Completion requirements:',
|
|
520
|
+
'- Execute the task before replying; do not reply with only a plan.',
|
|
521
|
+
'- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
|
|
522
|
+
'- If blocked, state the blocker explicitly and what input or permission is missing.',
|
|
523
|
+
].join('\n')
|
|
324
524
|
if (agent?.isOrchestrator) {
|
|
325
525
|
return executeOrchestrator(agent, prompt, sessionId, task.id)
|
|
326
526
|
}
|
|
@@ -330,11 +530,24 @@ async function executeTaskRun(
|
|
|
330
530
|
message: prompt,
|
|
331
531
|
internal: false,
|
|
332
532
|
source: 'task',
|
|
533
|
+
runId: task.id,
|
|
333
534
|
})
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
535
|
+
let text = typeof run.text === 'string' ? run.text.trim() : ''
|
|
536
|
+
if (run.error) return text ? text : `Error: ${run.error}`
|
|
537
|
+
|
|
538
|
+
// Auto-continue if the result looks incomplete
|
|
539
|
+
if (text && looksIncomplete(text)) {
|
|
540
|
+
const followUp = await executeSessionChatTurn({
|
|
541
|
+
sessionId,
|
|
542
|
+
message: 'Continue and complete the remaining steps. Provide a final summary when done.',
|
|
543
|
+
internal: false,
|
|
544
|
+
source: 'task',
|
|
545
|
+
})
|
|
546
|
+
const contText = typeof followUp.text === 'string' ? followUp.text.trim() : ''
|
|
547
|
+
if (contText) text = contText
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return text
|
|
338
551
|
}
|
|
339
552
|
|
|
340
553
|
function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
@@ -377,8 +590,8 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
377
590
|
const now = Date.now()
|
|
378
591
|
let changed = false
|
|
379
592
|
|
|
380
|
-
const buildMsg = ():
|
|
381
|
-
const msg:
|
|
593
|
+
const buildMsg = (): SessionMessageLike => {
|
|
594
|
+
const msg: SessionMessageLike = { role: 'assistant', text: body, time: now, kind: 'system' }
|
|
382
595
|
if (firstImage) msg.imageUrl = firstImage.url
|
|
383
596
|
return msg
|
|
384
597
|
}
|
|
@@ -701,6 +914,7 @@ export function enqueueTask(taskId: string) {
|
|
|
701
914
|
export function validateCompletedTasksQueue() {
|
|
702
915
|
const tasks = loadTasks()
|
|
703
916
|
const sessions = loadSessions()
|
|
917
|
+
const settings = loadSettings()
|
|
704
918
|
const now = Date.now()
|
|
705
919
|
let checked = 0
|
|
706
920
|
let demoted = 0
|
|
@@ -717,7 +931,7 @@ export function validateCompletedTasksQueue() {
|
|
|
717
931
|
tasksDirty = true
|
|
718
932
|
}
|
|
719
933
|
|
|
720
|
-
const validation = validateTaskCompletion(task, { report })
|
|
934
|
+
const validation = validateTaskCompletion(task, { report, settings })
|
|
721
935
|
const prevValidation = task.validation || null
|
|
722
936
|
const validationChanged = !prevValidation
|
|
723
937
|
|| prevValidation.ok !== validation.ok
|
|
@@ -802,6 +1016,32 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
802
1016
|
text: `Task moved to dead-letter after ${task.attempts}/${task.maxAttempts} attempts.\n\nReason: ${reason}`,
|
|
803
1017
|
createdAt: now,
|
|
804
1018
|
})
|
|
1019
|
+
|
|
1020
|
+
// Guardian Auto-Rollback
|
|
1021
|
+
const agents = loadAgents()
|
|
1022
|
+
const agent = task.agentId ? agents[task.agentId] : null
|
|
1023
|
+
if (agent?.autoRecovery) {
|
|
1024
|
+
const cwd = task.projectId
|
|
1025
|
+
? path.join(WORKSPACE_DIR, 'projects', task.projectId)
|
|
1026
|
+
: WORKSPACE_DIR
|
|
1027
|
+
const rollback = performGuardianRollback(cwd)
|
|
1028
|
+
if (rollback.ok) {
|
|
1029
|
+
task.comments.push({
|
|
1030
|
+
id: genId(),
|
|
1031
|
+
author: 'Guardian',
|
|
1032
|
+
text: `Auto-recovery triggered: Workspace successfully rolled back to last clean state.`,
|
|
1033
|
+
createdAt: now + 1,
|
|
1034
|
+
})
|
|
1035
|
+
} else {
|
|
1036
|
+
task.comments.push({
|
|
1037
|
+
id: genId(),
|
|
1038
|
+
author: 'Guardian',
|
|
1039
|
+
text: `Auto-recovery failed: ${rollback.reason}`,
|
|
1040
|
+
createdAt: now + 1,
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
805
1045
|
return 'dead_lettered'
|
|
806
1046
|
}
|
|
807
1047
|
|
|
@@ -903,7 +1143,9 @@ export async function processNext() {
|
|
|
903
1143
|
task.validation = null
|
|
904
1144
|
task.updatedAt = Date.now()
|
|
905
1145
|
|
|
906
|
-
const
|
|
1146
|
+
const sessionsForCwd = loadSessions() as Record<string, SessionLike>
|
|
1147
|
+
const taskCwd = resolveTaskExecutionCwd(task as ScheduleTaskMeta, sessionsForCwd)
|
|
1148
|
+
task.cwd = taskCwd
|
|
907
1149
|
let sessionId = ''
|
|
908
1150
|
const scheduleTask = task as ScheduleTaskMeta
|
|
909
1151
|
const isScheduleTask = scheduleTask.sourceType === 'schedule'
|
|
@@ -1002,6 +1244,7 @@ export async function processNext() {
|
|
|
1002
1244
|
try {
|
|
1003
1245
|
const result = await executeTaskRun(task, agent, sessionId)
|
|
1004
1246
|
const t2 = loadTasks()
|
|
1247
|
+
const settings = loadSettings()
|
|
1005
1248
|
if (t2[taskId]) {
|
|
1006
1249
|
applyTaskPolicyDefaults(t2[taskId])
|
|
1007
1250
|
// Structured extraction: Zod-validated result with typed artifacts
|
|
@@ -1018,7 +1261,7 @@ export async function processNext() {
|
|
|
1018
1261
|
t2[taskId].updatedAt = Date.now()
|
|
1019
1262
|
const report = ensureTaskCompletionReport(t2[taskId])
|
|
1020
1263
|
if (report?.relativePath) t2[taskId].completionReportPath = report.relativePath
|
|
1021
|
-
const validation = validateTaskCompletion(t2[taskId], { report })
|
|
1264
|
+
const validation = validateTaskCompletion(t2[taskId], { report, settings })
|
|
1022
1265
|
t2[taskId].validation = validation
|
|
1023
1266
|
|
|
1024
1267
|
const now = Date.now()
|
|
@@ -1233,6 +1476,29 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
|
|
|
1233
1476
|
|
|
1234
1477
|
for (const task of Object.values(tasks) as BoardTask[]) {
|
|
1235
1478
|
if (task.status !== 'running') continue
|
|
1479
|
+
if (!task.startedAt) {
|
|
1480
|
+
const recoveredAt = Date.now()
|
|
1481
|
+
task.status = 'queued'
|
|
1482
|
+
task.queuedAt = task.queuedAt || recoveredAt
|
|
1483
|
+
task.retryScheduledAt = null
|
|
1484
|
+
task.updatedAt = recoveredAt
|
|
1485
|
+
task.error = 'Recovered inconsistent running state (missing startedAt); requeued.'
|
|
1486
|
+
if (!task.comments) task.comments = []
|
|
1487
|
+
task.comments.push({
|
|
1488
|
+
id: genId(),
|
|
1489
|
+
author: 'System',
|
|
1490
|
+
text: 'Recovered inconsistent running state (missing startedAt). Task requeued.',
|
|
1491
|
+
createdAt: recoveredAt,
|
|
1492
|
+
})
|
|
1493
|
+
pushQueueUnique(queue, task.id)
|
|
1494
|
+
recovered++
|
|
1495
|
+
changed = true
|
|
1496
|
+
pushMainLoopEventToMainSessions({
|
|
1497
|
+
type: 'task_stall_recovered',
|
|
1498
|
+
text: `Recovered inconsistent running task "${task.title}" (${task.id}) and requeued it.`,
|
|
1499
|
+
})
|
|
1500
|
+
continue
|
|
1501
|
+
}
|
|
1236
1502
|
const since = Math.max(task.updatedAt || 0, task.startedAt || 0)
|
|
1237
1503
|
if (!since || (now - since) < staleMs) continue
|
|
1238
1504
|
|
|
@@ -1259,6 +1525,9 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
|
|
|
1259
1525
|
if (changed) {
|
|
1260
1526
|
saveTasks(tasks)
|
|
1261
1527
|
saveQueue(queue)
|
|
1528
|
+
if (recovered > 0) {
|
|
1529
|
+
setTimeout(() => processNext(), 250)
|
|
1530
|
+
}
|
|
1262
1531
|
}
|
|
1263
1532
|
|
|
1264
1533
|
return { recovered, deadLettered }
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
|
|
4
4
|
DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
|
|
5
5
|
DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
|
|
6
|
+
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
6
7
|
DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
|
|
7
8
|
DEFAULT_LOOP_MODE,
|
|
8
9
|
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
@@ -17,6 +18,7 @@ export interface RuntimeSettings {
|
|
|
17
18
|
agentLoopRecursionLimit: number
|
|
18
19
|
orchestratorLoopRecursionLimit: number
|
|
19
20
|
legacyOrchestratorMaxTurns: number
|
|
21
|
+
delegationMaxDepth: number
|
|
20
22
|
ongoingLoopMaxIterations: number
|
|
21
23
|
ongoingLoopMaxRuntimeMs: number | null
|
|
22
24
|
shellCommandTimeoutMs: number
|
|
@@ -61,6 +63,12 @@ export function loadRuntimeSettings(): RuntimeSettings {
|
|
|
61
63
|
1,
|
|
62
64
|
300,
|
|
63
65
|
)
|
|
66
|
+
const delegationMaxDepth = parseIntSetting(
|
|
67
|
+
settings.delegationMaxDepth,
|
|
68
|
+
DEFAULT_DELEGATION_MAX_DEPTH,
|
|
69
|
+
1,
|
|
70
|
+
12,
|
|
71
|
+
)
|
|
64
72
|
const ongoingLoopMaxIterations = parseIntSetting(
|
|
65
73
|
settings.ongoingLoopMaxIterations,
|
|
66
74
|
DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
|
|
@@ -98,6 +106,7 @@ export function loadRuntimeSettings(): RuntimeSettings {
|
|
|
98
106
|
agentLoopRecursionLimit,
|
|
99
107
|
orchestratorLoopRecursionLimit,
|
|
100
108
|
legacyOrchestratorMaxTurns,
|
|
109
|
+
delegationMaxDepth,
|
|
101
110
|
ongoingLoopMaxIterations,
|
|
102
111
|
ongoingLoopMaxRuntimeMs: ongoingLoopMaxRuntimeMinutes > 0 ? ongoingLoopMaxRuntimeMinutes * 60_000 : null,
|
|
103
112
|
shellCommandTimeoutMs: shellCommandTimeoutSec * 1000,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isMainMissionSession } from './session-run-manager'
|
|
4
|
+
|
|
5
|
+
describe('isMainMissionSession', () => {
|
|
6
|
+
it('accepts explicit main sessions', () => {
|
|
7
|
+
assert.equal(isMainMissionSession({ id: 'main-user', name: '__main__' }), true)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('rejects human agent-thread sessions', () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
isMainMissionSession({ id: 'agent-thread-agent_coder-123', name: 'agent-thread:agent_coder', sessionType: 'human' }),
|
|
13
|
+
false,
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts orchestrated sessions', () => {
|
|
18
|
+
assert.equal(
|
|
19
|
+
isMainMissionSession({ id: 'agent-thread-worker-1', name: 'agent-thread:worker', sessionType: 'orchestrated' }),
|
|
20
|
+
true,
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -203,7 +203,7 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
|
|
|
203
203
|
try {
|
|
204
204
|
const sessions = loadSessions()
|
|
205
205
|
const session = sessions[sessionId]
|
|
206
|
-
if (!session || session
|
|
206
|
+
if (!session || !isMainMissionSession(session)) return
|
|
207
207
|
enqueueSessionRun({
|
|
208
208
|
sessionId,
|
|
209
209
|
message: followup.message,
|
|
@@ -218,6 +218,16 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
|
|
|
218
218
|
}, delayMs)
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export function isMainMissionSession(session: Record<string, unknown>): boolean {
|
|
222
|
+
const id = typeof session.id === 'string' ? session.id.trim() : ''
|
|
223
|
+
const name = typeof session.name === 'string' ? session.name.trim() : ''
|
|
224
|
+
const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
|
|
225
|
+
if (id.startsWith('main-') || name === '__main__') return true
|
|
226
|
+
// Only orchestrated thread sessions should receive autonomous main-loop followups.
|
|
227
|
+
if (sessionType === 'orchestrated') return true
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
async function drainExecution(executionKey: string): Promise<void> {
|
|
222
232
|
if (state.runningByExecution.has(executionKey)) return
|
|
223
233
|
const q = queueForExecution(executionKey)
|
|
@@ -271,6 +281,9 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
271
281
|
resultText: result.text,
|
|
272
282
|
error: result.error,
|
|
273
283
|
toolEvents: result.toolEvents,
|
|
284
|
+
inputTokens: result.inputTokens,
|
|
285
|
+
outputTokens: result.outputTokens,
|
|
286
|
+
estimatedCost: result.estimatedCost,
|
|
274
287
|
})
|
|
275
288
|
} catch (mainLoopErr: any) {
|
|
276
289
|
log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
|
|
@@ -372,6 +385,19 @@ export interface EnqueueSessionRunResult {
|
|
|
372
385
|
abort: () => void
|
|
373
386
|
}
|
|
374
387
|
|
|
388
|
+
const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli'])
|
|
389
|
+
|
|
390
|
+
function computeEffectiveRunTimeoutMs(
|
|
391
|
+
baseTimeoutMs: number,
|
|
392
|
+
sessionTools: string[],
|
|
393
|
+
runtime: { claudeCodeTimeoutMs: number },
|
|
394
|
+
): number {
|
|
395
|
+
const hasLongTool = sessionTools.some(t => LONG_TOOL_NAMES.has(t))
|
|
396
|
+
if (!hasLongTool) return baseTimeoutMs
|
|
397
|
+
const toolTimeout = runtime.claudeCodeTimeoutMs + 120_000
|
|
398
|
+
return Math.max(baseTimeoutMs, toolTimeout)
|
|
399
|
+
}
|
|
400
|
+
|
|
375
401
|
export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSessionRunResult {
|
|
376
402
|
const internal = input.internal === true
|
|
377
403
|
const mode = normalizeMode(input.mode, internal)
|
|
@@ -379,9 +405,13 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
379
405
|
const executionKey = executionKeyForSession(input.sessionId)
|
|
380
406
|
const runtime = loadRuntimeSettings()
|
|
381
407
|
const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
|
|
408
|
+
const sessions = loadSessions()
|
|
409
|
+
const sessionData = sessions[input.sessionId]
|
|
410
|
+
const sessionTools: string[] = sessionData?.tools || []
|
|
411
|
+
const adjustedDefaultMs = computeEffectiveRunTimeoutMs(defaultMaxRuntimeMs, sessionTools, runtime)
|
|
382
412
|
const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
|
|
383
413
|
? input.maxRuntimeMs
|
|
384
|
-
:
|
|
414
|
+
: adjustedDefaultMs
|
|
385
415
|
|
|
386
416
|
const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
|
|
387
417
|
if (dedupe) {
|