claude-comms 1.0.44 → 1.0.46
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/.agents/tools/agent-job/__pycache__/agent_monitor.cpython-312.pyc +0 -0
- package/.agents/tools/agent-job/agent_job.py +71 -1
- package/.agents/tools/workflow/CONSUMER_SETUP.md +114 -0
- package/.agents/tools/workflow/ONBOARD.md +167 -0
- package/.agents/tools/workflow/README.md +169 -0
- package/.agents/tools/workflow/cli.ts +726 -0
- package/.agents/tools/workflow/config.example.json +16 -0
- package/.agents/tools/workflow/init.ts +76 -0
- package/.agents/tools/workflow/lib/file-tracker.test.ts +81 -0
- package/.agents/tools/workflow/lib/file-tracker.ts +387 -0
- package/.agents/tools/workflow/lib/harness-executor.ts +649 -0
- package/.agents/tools/workflow/lib/prompts.scenario.test.ts +262 -0
- package/.agents/tools/workflow/lib/prompts.ts +548 -0
- package/.agents/tools/workflow/lib/streams.test.ts +140 -0
- package/.agents/tools/workflow/lib/streams.ts +254 -0
- package/.agents/tools/workflow/runner.ts +994 -0
- package/.agents/tools/workflow/templates/document.md +42 -0
- package/.agents/tools/workflow/templates/implement.md +117 -0
- package/.agents/tools/workflow/templates/plan.md +54 -0
- package/.agents/tools/workflow/templates/pm.md +84 -0
- package/.agents/tools/workflow/templates/product-owner.md +211 -0
- package/.agents/tools/workflow/templates/review.md +56 -0
- package/.agents/tools/workflow/templates/uat.md +71 -0
- package/.claude/agents/core/engineer.md +2 -13
- package/.claude/commands/action-feedback-item.md +14 -9
- package/.claude/hooks/comms/__pycache__/register_subagent.cpython-312.pyc +0 -0
- package/.claude/hooks/comms/__pycache__/update_subagent_completion.cpython-312.pyc +0 -0
- package/.claude/hooks/utils/__pycache__/server_config.cpython-312.pyc +0 -0
- package/.claude/settings.local.json +46 -0
- package/CLAUDE.md +1 -1
- package/bin/claude-setup.js +3 -4
- package/package.json +1 -1
- package/src/orchestrator/installer.js +5 -63
- package/.claude/agents/core/architect.md +0 -316
- package/.claude/agents/core/consultants/codex.md +0 -39
- package/.claude/agents/core/consultants/gemini.md +0 -39
- package/.claude/agents/core/designer.md +0 -181
- package/.claude/agents/core/planner.md +0 -251
- package/.claude/agents/core/uat.md +0 -147
- package/.claude/agents/util/feedback-executor.md +0 -29
- package/.claude/agents/util/meta-agent-builder.md +0 -309
- package/.claude/agents/util/session-review.md +0 -228
- package/.claude/commands/analyse-document.md +0 -10
- package/.claude/commands/await-feedback-orchestrator.md +0 -12
- package/.claude/commands/await-feedback.md +0 -21
- package/.claude/commands/cook.md +0 -119
- package/.claude/commands/feedback.md +0 -140
- package/.claude/commands/hello-world.md +0 -1
- package/.claude/commands/uat-refine.md +0 -121
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Workflow Runner Daemon
|
|
4
|
+
*
|
|
5
|
+
* Subscribes to Convex and executes jobs as they become ready.
|
|
6
|
+
* Runs forever, reacting to database changes.
|
|
7
|
+
*
|
|
8
|
+
* Supports parallel job execution within groups:
|
|
9
|
+
* - Scheduler returns ALL pending jobs in a ready group
|
|
10
|
+
* - Runner executes them in parallel
|
|
11
|
+
* - When group completes, aggregated results go to PM
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* nohup npx tsx runner.ts > /tmp/runner.log 2>&1 &
|
|
15
|
+
*
|
|
16
|
+
* Config via config.json in same directory.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ConvexClient } from "convex/browser";
|
|
20
|
+
import { anyApi } from "convex/server";
|
|
21
|
+
import { readFileSync } from "fs";
|
|
22
|
+
import { dirname, join } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
|
|
25
|
+
// Prompt building (extracted module)
|
|
26
|
+
import {
|
|
27
|
+
Assignment,
|
|
28
|
+
Job,
|
|
29
|
+
JobGroup,
|
|
30
|
+
ChatJobContext,
|
|
31
|
+
AccumulatedJobResult,
|
|
32
|
+
buildPrompt,
|
|
33
|
+
buildChatPrompt,
|
|
34
|
+
parseChatContext,
|
|
35
|
+
isChatJob,
|
|
36
|
+
determinePromptType,
|
|
37
|
+
} from "./lib/prompts.js";
|
|
38
|
+
|
|
39
|
+
// Harness executor (file-based event streaming for crash resilience)
|
|
40
|
+
import { HarnessExecutor, Harness } from "./lib/harness-executor.js";
|
|
41
|
+
|
|
42
|
+
// Use anyApi for dynamic function references (works with ConvexClient)
|
|
43
|
+
const api = anyApi;
|
|
44
|
+
|
|
45
|
+
// Chat job from chatJobs table (separate from assignment jobs)
|
|
46
|
+
interface ChatJob {
|
|
47
|
+
_id: string;
|
|
48
|
+
_creationTime: number;
|
|
49
|
+
threadId: string;
|
|
50
|
+
namespaceId: string;
|
|
51
|
+
harness: "claude" | "codex" | "gemini";
|
|
52
|
+
context: string;
|
|
53
|
+
status: "pending" | "running" | "complete" | "failed";
|
|
54
|
+
result?: string;
|
|
55
|
+
startedAt?: number;
|
|
56
|
+
completedAt?: number;
|
|
57
|
+
createdAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Config
|
|
61
|
+
interface Config {
|
|
62
|
+
convexUrl: string;
|
|
63
|
+
namespace: string;
|
|
64
|
+
timeoutMs: number;
|
|
65
|
+
harnessDefaults: {
|
|
66
|
+
default: Harness;
|
|
67
|
+
[jobType: string]: Harness;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the harness for a job type from config
|
|
73
|
+
*/
|
|
74
|
+
function getHarnessForJobType(jobType: string): Harness {
|
|
75
|
+
return config.harnessDefaults[jobType] || config.harnessDefaults.default;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
79
|
+
const configPath = join(__dirname, "config.json");
|
|
80
|
+
const config: Config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
81
|
+
|
|
82
|
+
// Project root is 3 levels up from .agents/tools/workflow
|
|
83
|
+
const projectRoot = join(__dirname, "..", "..", "..");
|
|
84
|
+
|
|
85
|
+
// State
|
|
86
|
+
let client: ConvexClient | null = null;
|
|
87
|
+
let unsubscribeJobs: (() => void) | null = null;
|
|
88
|
+
let unsubscribeChatJobs: (() => void) | null = null;
|
|
89
|
+
|
|
90
|
+
// Harness executor (handles process spawning, file-based event streaming, orphan recovery)
|
|
91
|
+
const executor = new HarnessExecutor({
|
|
92
|
+
timeoutMs: config.timeoutMs,
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const METRICS_HEARTBEAT_MS = 10_000;
|
|
97
|
+
const CODEX_TOOL_ITEM_TYPES = new Set([
|
|
98
|
+
"command_execution",
|
|
99
|
+
"tool_call",
|
|
100
|
+
"tool_use",
|
|
101
|
+
"function_call",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
interface MetricsState {
|
|
105
|
+
toolCallCount: number;
|
|
106
|
+
subagentCount: number;
|
|
107
|
+
totalTokens: number | null;
|
|
108
|
+
lastEventAt: number | null;
|
|
109
|
+
lastFlushedToolCallCount: number;
|
|
110
|
+
lastFlushedSubagentCount: number;
|
|
111
|
+
lastFlushedEventAt: number | null;
|
|
112
|
+
heartbeat: NodeJS.Timeout | null;
|
|
113
|
+
seenToolIds: Set<string>;
|
|
114
|
+
seenSubagentIds: Set<string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createMetricsState(): MetricsState {
|
|
118
|
+
return {
|
|
119
|
+
toolCallCount: 0,
|
|
120
|
+
subagentCount: 0,
|
|
121
|
+
totalTokens: null,
|
|
122
|
+
lastEventAt: null,
|
|
123
|
+
lastFlushedToolCallCount: 0,
|
|
124
|
+
lastFlushedSubagentCount: 0,
|
|
125
|
+
lastFlushedEventAt: null,
|
|
126
|
+
heartbeat: null,
|
|
127
|
+
seenToolIds: new Set(),
|
|
128
|
+
seenSubagentIds: new Set(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractToolCallInfo(
|
|
133
|
+
harness: Harness,
|
|
134
|
+
event: Record<string, unknown>
|
|
135
|
+
): { ids: string[]; hadToolUse: boolean } {
|
|
136
|
+
const type = event.type as string | undefined;
|
|
137
|
+
switch (harness) {
|
|
138
|
+
case "claude": {
|
|
139
|
+
if (type !== "assistant") return { ids: [], hadToolUse: false };
|
|
140
|
+
const message = event.message as { content?: Array<{ type?: string; id?: string }> } | undefined;
|
|
141
|
+
if (!message?.content) return { ids: [], hadToolUse: false };
|
|
142
|
+
const ids: string[] = [];
|
|
143
|
+
let hadToolUse = false;
|
|
144
|
+
for (const block of message.content) {
|
|
145
|
+
if (block?.type === "tool_use") {
|
|
146
|
+
hadToolUse = true;
|
|
147
|
+
if (block.id) ids.push(String(block.id));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { ids, hadToolUse };
|
|
151
|
+
}
|
|
152
|
+
case "codex": {
|
|
153
|
+
if (type !== "item.started" && type !== "item.completed") {
|
|
154
|
+
return { ids: [], hadToolUse: false };
|
|
155
|
+
}
|
|
156
|
+
const item = event.item as { type?: string; id?: string } | undefined;
|
|
157
|
+
if (!item?.type || !CODEX_TOOL_ITEM_TYPES.has(item.type)) {
|
|
158
|
+
return { ids: [], hadToolUse: false };
|
|
159
|
+
}
|
|
160
|
+
const ids = item.id ? [String(item.id)] : [];
|
|
161
|
+
return { ids, hadToolUse: true };
|
|
162
|
+
}
|
|
163
|
+
case "gemini": {
|
|
164
|
+
if (type !== "tool_use") return { ids: [], hadToolUse: false };
|
|
165
|
+
const toolId =
|
|
166
|
+
(event.tool_id as string | undefined) ??
|
|
167
|
+
(event.toolId as string | undefined) ??
|
|
168
|
+
(event.toolID as string | undefined);
|
|
169
|
+
return { ids: toolId ? [String(toolId)] : [], hadToolUse: true };
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
return { ids: [], hadToolUse: false };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractSubagentInfo(
|
|
177
|
+
harness: Harness,
|
|
178
|
+
event: Record<string, unknown>
|
|
179
|
+
): { ids: string[]; hadSubagent: boolean } {
|
|
180
|
+
const type = event.type as string | undefined;
|
|
181
|
+
switch (harness) {
|
|
182
|
+
case "claude": {
|
|
183
|
+
if (type !== "assistant") return { ids: [], hadSubagent: false };
|
|
184
|
+
const message = event.message as { content?: Array<{ type?: string; id?: string; name?: string }> } | undefined;
|
|
185
|
+
if (!message?.content) return { ids: [], hadSubagent: false };
|
|
186
|
+
const ids: string[] = [];
|
|
187
|
+
let hadSubagent = false;
|
|
188
|
+
for (const block of message.content) {
|
|
189
|
+
if (block?.type === "tool_use" && block.name === "Task") {
|
|
190
|
+
hadSubagent = true;
|
|
191
|
+
if (block.id) ids.push(String(block.id));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { ids, hadSubagent };
|
|
195
|
+
}
|
|
196
|
+
case "gemini": {
|
|
197
|
+
if (type !== "tool_use") return { ids: [], hadSubagent: false };
|
|
198
|
+
const toolName = event.tool_name as string | undefined;
|
|
199
|
+
if (toolName !== "delegate_to_agent") return { ids: [], hadSubagent: false };
|
|
200
|
+
const toolId =
|
|
201
|
+
(event.tool_id as string | undefined) ??
|
|
202
|
+
(event.toolId as string | undefined) ??
|
|
203
|
+
(event.toolID as string | undefined);
|
|
204
|
+
return { ids: toolId ? [String(toolId)] : [], hadSubagent: true };
|
|
205
|
+
}
|
|
206
|
+
case "codex":
|
|
207
|
+
default:
|
|
208
|
+
return { ids: [], hadSubagent: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractTotalTokens(
|
|
213
|
+
harness: Harness,
|
|
214
|
+
event: Record<string, unknown>
|
|
215
|
+
): number | null {
|
|
216
|
+
const type = event.type as string | undefined;
|
|
217
|
+
if (harness === "claude" && type === "result") {
|
|
218
|
+
const usage = event.usage as { input_tokens?: number; output_tokens?: number } | undefined;
|
|
219
|
+
if (typeof usage?.input_tokens === "number" && typeof usage?.output_tokens === "number") {
|
|
220
|
+
return usage.input_tokens + usage.output_tokens;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (harness === "codex" && type === "turn.completed") {
|
|
225
|
+
const usage = event.usage as { input_tokens?: number; output_tokens?: number } | undefined;
|
|
226
|
+
if (typeof usage?.input_tokens === "number" && typeof usage?.output_tokens === "number") {
|
|
227
|
+
return usage.input_tokens + usage.output_tokens;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (harness === "gemini" && type === "result") {
|
|
232
|
+
const stats = event.stats as {
|
|
233
|
+
total_tokens?: number;
|
|
234
|
+
input_tokens?: number;
|
|
235
|
+
output_tokens?: number;
|
|
236
|
+
} | undefined;
|
|
237
|
+
if (typeof stats?.total_tokens === "number") return stats.total_tokens;
|
|
238
|
+
if (typeof stats?.input_tokens === "number" && typeof stats?.output_tokens === "number") {
|
|
239
|
+
return stats.input_tokens + stats.output_tokens;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function recordMetricsEvent(
|
|
247
|
+
harness: Harness,
|
|
248
|
+
event: Record<string, unknown>,
|
|
249
|
+
metrics: MetricsState
|
|
250
|
+
): void {
|
|
251
|
+
metrics.lastEventAt = Date.now();
|
|
252
|
+
|
|
253
|
+
const toolInfo = extractToolCallInfo(harness, event);
|
|
254
|
+
if (toolInfo.hadToolUse) {
|
|
255
|
+
if (toolInfo.ids.length === 0) {
|
|
256
|
+
metrics.toolCallCount += 1;
|
|
257
|
+
} else {
|
|
258
|
+
for (const id of toolInfo.ids) {
|
|
259
|
+
if (metrics.seenToolIds.has(id)) continue;
|
|
260
|
+
metrics.seenToolIds.add(id);
|
|
261
|
+
metrics.toolCallCount += 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const subagentInfo = extractSubagentInfo(harness, event);
|
|
267
|
+
if (subagentInfo.hadSubagent) {
|
|
268
|
+
if (subagentInfo.ids.length === 0) {
|
|
269
|
+
metrics.subagentCount += 1;
|
|
270
|
+
} else {
|
|
271
|
+
for (const id of subagentInfo.ids) {
|
|
272
|
+
if (metrics.seenSubagentIds.has(id)) continue;
|
|
273
|
+
metrics.seenSubagentIds.add(id);
|
|
274
|
+
metrics.subagentCount += 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const totalTokens = extractTotalTokens(harness, event);
|
|
280
|
+
if (totalTokens !== null) {
|
|
281
|
+
metrics.totalTokens = totalTokens;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Save assistant response to chat thread
|
|
286
|
+
async function saveChatResponse(threadId: string, content: string): Promise<void> {
|
|
287
|
+
try {
|
|
288
|
+
await client!.mutation(api.chatMessages.add, {
|
|
289
|
+
threadId: threadId as any,
|
|
290
|
+
role: "assistant",
|
|
291
|
+
content,
|
|
292
|
+
});
|
|
293
|
+
console.log(`[Chat] Saved assistant response to thread ${threadId}`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.error(`[Chat] Failed to save response:`, e);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Save Claude session_id to thread for session resume
|
|
300
|
+
async function saveSessionId(threadId: string, sessionId: string): Promise<void> {
|
|
301
|
+
try {
|
|
302
|
+
await client!.mutation(api.chatThreads.updateSessionId, {
|
|
303
|
+
id: threadId as any,
|
|
304
|
+
sessionId,
|
|
305
|
+
});
|
|
306
|
+
console.log(`[Chat] Saved session_id ${sessionId} to thread ${threadId}`);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.error(`[Chat] Failed to save session_id:`, e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Save lastPromptMode to thread for differential prompting
|
|
313
|
+
async function saveLastPromptMode(
|
|
314
|
+
threadId: string,
|
|
315
|
+
mode: "jam" | "cook"
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
try {
|
|
318
|
+
await client!.mutation(api.chatThreads.updateLastPromptMode, {
|
|
319
|
+
id: threadId as any,
|
|
320
|
+
lastPromptMode: mode,
|
|
321
|
+
});
|
|
322
|
+
console.log(`[Chat] Saved lastPromptMode ${mode} to thread ${threadId}`);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.error(`[Chat] Failed to save lastPromptMode:`, e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Guardian Mode: Trigger PO evaluation when PM completes
|
|
329
|
+
interface ChatThread {
|
|
330
|
+
_id: string;
|
|
331
|
+
mode: "jam" | "cook" | "guardian";
|
|
332
|
+
assignmentId?: string;
|
|
333
|
+
claudeSessionId?: string;
|
|
334
|
+
namespaceId: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function triggerGuardianEvaluation(
|
|
338
|
+
assignment: Assignment,
|
|
339
|
+
pmResult: string
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
try {
|
|
342
|
+
const guardianThread: ChatThread | null = await client!.query(
|
|
343
|
+
api.chatThreads.getGuardianThread,
|
|
344
|
+
{ assignmentId: assignment._id as any }
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!guardianThread) {
|
|
348
|
+
console.log(`[Guardian] No guardian thread for assignment ${assignment._id}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`[Guardian] Found guardian thread ${guardianThread._id}, triggering evaluation`);
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await client!.mutation(api.chatMessages.add, {
|
|
356
|
+
threadId: guardianThread._id as any,
|
|
357
|
+
role: "pm",
|
|
358
|
+
content: pmResult,
|
|
359
|
+
});
|
|
360
|
+
console.log(`[Guardian] Inserted PM response as message in thread`);
|
|
361
|
+
} catch (msgError) {
|
|
362
|
+
console.error(`[Guardian] Failed to insert PM message:`, msgError);
|
|
363
|
+
throw new Error(`Guardian PM message insert failed: ${msgError}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await client!.mutation(api.chatJobs.trigger, {
|
|
368
|
+
threadId: guardianThread._id as any,
|
|
369
|
+
harness: getHarnessForJobType("chat"),
|
|
370
|
+
isGuardianEvaluation: true,
|
|
371
|
+
});
|
|
372
|
+
console.log(`[Guardian] Triggered PO evaluation chatJob`);
|
|
373
|
+
} catch (triggerError) {
|
|
374
|
+
console.error(`[Guardian] Failed to trigger evaluation job:`, triggerError);
|
|
375
|
+
throw new Error(`Guardian evaluation trigger failed: ${triggerError}`);
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.error(`[Guardian] Evaluation failed for assignment ${assignment._id}:`, e);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Job execution (uses HarnessExecutor for crash-resilient file-based streaming)
|
|
383
|
+
async function executeJob(
|
|
384
|
+
job: Job,
|
|
385
|
+
group: JobGroup,
|
|
386
|
+
assignment: Assignment,
|
|
387
|
+
accumulatedResults: AccumulatedJobResult[],
|
|
388
|
+
previousNonPmGroupResults: AccumulatedJobResult[],
|
|
389
|
+
r1GroupResults: AccumulatedJobResult[]
|
|
390
|
+
): Promise<void> {
|
|
391
|
+
const jobId = job._id;
|
|
392
|
+
const isChat = isChatJob(job);
|
|
393
|
+
let chatContext: ChatJobContext | null = null;
|
|
394
|
+
|
|
395
|
+
console.log(`[${jobId}] Starting ${job.jobType} job (${job.harness})${isChat ? ' [CHAT]' : ''}`);
|
|
396
|
+
|
|
397
|
+
// Build prompt FIRST (before marking as running, so we can save it)
|
|
398
|
+
let prompt: string;
|
|
399
|
+
|
|
400
|
+
if (isChat) {
|
|
401
|
+
chatContext = parseChatContext(job.context || "{}");
|
|
402
|
+
if (!chatContext) {
|
|
403
|
+
console.error(`[${jobId}] Invalid chat context, failing job`);
|
|
404
|
+
await client!.mutation(api.jobs.fail, {
|
|
405
|
+
id: jobId,
|
|
406
|
+
result: "Invalid chat context provided",
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
prompt = buildChatPrompt(chatContext, config.namespace);
|
|
411
|
+
const resumeInfo = chatContext.claudeSessionId
|
|
412
|
+
? ` (resuming session ${chatContext.claudeSessionId.slice(0, 8)}...)`
|
|
413
|
+
: ' (new session)';
|
|
414
|
+
console.log(`[${jobId}] Chat mode: ${chatContext.mode}, thread: ${chatContext.threadId}${resumeInfo}`);
|
|
415
|
+
} else {
|
|
416
|
+
prompt = buildPrompt(
|
|
417
|
+
group,
|
|
418
|
+
assignment,
|
|
419
|
+
job,
|
|
420
|
+
accumulatedResults,
|
|
421
|
+
previousNonPmGroupResults,
|
|
422
|
+
r1GroupResults
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Mark job as running with prompt for visibility
|
|
427
|
+
await client!.mutation(api.jobs.start, { id: jobId, prompt });
|
|
428
|
+
|
|
429
|
+
const metrics = createMetricsState();
|
|
430
|
+
|
|
431
|
+
const flushMetrics = async (): Promise<void> => {
|
|
432
|
+
if (!client) return;
|
|
433
|
+
const update: {
|
|
434
|
+
id: string;
|
|
435
|
+
toolCallCount?: number;
|
|
436
|
+
subagentCount?: number;
|
|
437
|
+
lastEventAt?: number;
|
|
438
|
+
} = { id: jobId };
|
|
439
|
+
let changed = false;
|
|
440
|
+
|
|
441
|
+
if (metrics.toolCallCount !== metrics.lastFlushedToolCallCount) {
|
|
442
|
+
update.toolCallCount = metrics.toolCallCount;
|
|
443
|
+
metrics.lastFlushedToolCallCount = metrics.toolCallCount;
|
|
444
|
+
changed = true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (metrics.subagentCount !== metrics.lastFlushedSubagentCount) {
|
|
448
|
+
update.subagentCount = metrics.subagentCount;
|
|
449
|
+
metrics.lastFlushedSubagentCount = metrics.subagentCount;
|
|
450
|
+
changed = true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (metrics.lastEventAt !== metrics.lastFlushedEventAt) {
|
|
454
|
+
update.lastEventAt = metrics.lastEventAt ?? undefined;
|
|
455
|
+
metrics.lastFlushedEventAt = metrics.lastEventAt;
|
|
456
|
+
changed = true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!changed) return;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
await client!.mutation(api.jobs.updateMetrics, update);
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error(`[${jobId}] Failed to update job metrics:`, e);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const ensureHeartbeat = (): void => {
|
|
469
|
+
if (metrics.heartbeat) return;
|
|
470
|
+
metrics.heartbeat = setInterval(() => {
|
|
471
|
+
void flushMetrics();
|
|
472
|
+
}, METRICS_HEARTBEAT_MS);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const stopHeartbeat = (): void => {
|
|
476
|
+
if (!metrics.heartbeat) return;
|
|
477
|
+
clearInterval(metrics.heartbeat);
|
|
478
|
+
metrics.heartbeat = null;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Execute via HarnessExecutor (file-based streaming, crash-resilient)
|
|
482
|
+
executor.execute(
|
|
483
|
+
{
|
|
484
|
+
jobId,
|
|
485
|
+
harness: job.harness as Harness,
|
|
486
|
+
prompt,
|
|
487
|
+
sessionId: isChat && chatContext?.claudeSessionId ? chatContext.claudeSessionId : undefined,
|
|
488
|
+
env: {
|
|
489
|
+
WORKFLOW_ASSIGNMENT_ID: assignment._id,
|
|
490
|
+
WORKFLOW_GROUP_ID: group._id,
|
|
491
|
+
WORKFLOW_JOB_ID: job._id,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
onEvent: (event) => {
|
|
496
|
+
recordMetricsEvent(job.harness as Harness, event, metrics);
|
|
497
|
+
ensureHeartbeat();
|
|
498
|
+
},
|
|
499
|
+
onComplete: async (result, sessionId) => {
|
|
500
|
+
stopHeartbeat();
|
|
501
|
+
try {
|
|
502
|
+
await client!.mutation(api.jobs.complete, {
|
|
503
|
+
id: jobId,
|
|
504
|
+
result,
|
|
505
|
+
toolCallCount: metrics.toolCallCount,
|
|
506
|
+
subagentCount: metrics.subagentCount,
|
|
507
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
508
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
509
|
+
});
|
|
510
|
+
if (isChat && chatContext) {
|
|
511
|
+
await saveChatResponse(chatContext.threadId, result);
|
|
512
|
+
if (sessionId) {
|
|
513
|
+
await saveSessionId(chatContext.threadId, sessionId);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
await handleGroupCompletion(group, assignment, false);
|
|
517
|
+
}
|
|
518
|
+
} catch (e) {
|
|
519
|
+
console.error(`[${jobId}] Error in onComplete:`, e);
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
onFail: async (reason, partialResult) => {
|
|
523
|
+
stopHeartbeat();
|
|
524
|
+
try {
|
|
525
|
+
await client!.mutation(api.jobs.fail, {
|
|
526
|
+
id: jobId,
|
|
527
|
+
result: partialResult || reason,
|
|
528
|
+
toolCallCount: metrics.toolCallCount,
|
|
529
|
+
subagentCount: metrics.subagentCount,
|
|
530
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
531
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
532
|
+
});
|
|
533
|
+
if (isChat && chatContext) {
|
|
534
|
+
await saveChatResponse(chatContext.threadId,
|
|
535
|
+
`I encountered an issue while processing your request. Please try again or rephrase your question.\n\nError details: ${partialResult || reason}`
|
|
536
|
+
);
|
|
537
|
+
} else {
|
|
538
|
+
await handleGroupCompletion(group, assignment, true);
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.error(`[${jobId}] Error in onFail:`, e);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
onTimeout: async (partialResult) => {
|
|
545
|
+
stopHeartbeat();
|
|
546
|
+
try {
|
|
547
|
+
await client!.mutation(api.jobs.fail, {
|
|
548
|
+
id: jobId,
|
|
549
|
+
result: `Timeout after ${config.timeoutMs}ms. Partial result:\n${partialResult}`,
|
|
550
|
+
toolCallCount: metrics.toolCallCount,
|
|
551
|
+
subagentCount: metrics.subagentCount,
|
|
552
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
553
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
554
|
+
});
|
|
555
|
+
if (isChat && chatContext) {
|
|
556
|
+
await saveChatResponse(chatContext.threadId,
|
|
557
|
+
`I apologize, but I ran into a timeout. Here's what I was able to process:\n\n${partialResult || "(no output)"}`
|
|
558
|
+
);
|
|
559
|
+
} else {
|
|
560
|
+
await handleGroupCompletion(group, assignment, true);
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.error(`[${jobId}] Error in onTimeout:`, e);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Handle group completion - check if all jobs done, trigger PM if needed
|
|
571
|
+
async function handleGroupCompletion(
|
|
572
|
+
group: JobGroup,
|
|
573
|
+
assignment: Assignment,
|
|
574
|
+
anyFailed: boolean
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
// Re-fetch group with jobs to get current status
|
|
577
|
+
const currentGroup = await client!.query(api.jobs.getGroupWithJobs, { id: group._id });
|
|
578
|
+
if (!currentGroup) {
|
|
579
|
+
console.error(`[${group._id}] Group not found`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// If group not yet complete, another job is still running
|
|
584
|
+
if (currentGroup.status !== "complete" && currentGroup.status !== "failed") {
|
|
585
|
+
console.log(`[${group._id}] Group still in progress (status: ${currentGroup.status})`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
console.log(`[${group._id}] Group completed (status: ${currentGroup.status})`);
|
|
590
|
+
|
|
591
|
+
// Check job types in this group to determine behavior
|
|
592
|
+
const jobs = currentGroup.jobs || [];
|
|
593
|
+
const hasPMJob = jobs.some((j: Job) => j.jobType === "pm");
|
|
594
|
+
const hasDocumentJob = jobs.some((j: Job) => j.jobType === "document");
|
|
595
|
+
|
|
596
|
+
// PM groups: always trigger guardian evaluation (even if PM inserted next job)
|
|
597
|
+
if (hasPMJob) {
|
|
598
|
+
// PM completed - trigger guardian evaluation if applicable
|
|
599
|
+
// Find the PM job's result (not aggregatedResult with headers)
|
|
600
|
+
const pmJob = jobs.find((j: Job) => j.jobType === "pm" && j.result);
|
|
601
|
+
const pmResult = pmJob?.result || "";
|
|
602
|
+
await triggerGuardianEvaluation(assignment, pmResult);
|
|
603
|
+
// If PM didn't insert a next job, check if assignment is done
|
|
604
|
+
if (!currentGroup.nextGroupId) {
|
|
605
|
+
await checkAndCompleteAssignment(assignment._id, group._id);
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (hasDocumentJob) {
|
|
611
|
+
// Document job auto-completes the assignment
|
|
612
|
+
if (!currentGroup.nextGroupId) {
|
|
613
|
+
await checkAndCompleteAssignment(assignment._id, group._id);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Regular group: if next group exists, let scheduler handle it
|
|
619
|
+
if (currentGroup.nextGroupId) {
|
|
620
|
+
console.log(`[${group._id}] Next group exists, scheduler will pick it up`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// No next group and not PM/document - trigger PM review
|
|
625
|
+
const failed = currentGroup.status === "failed";
|
|
626
|
+
await triggerPMGroup(group, assignment, failed);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Check if all groups in assignment are done, if so mark complete
|
|
630
|
+
async function checkAndCompleteAssignment(assignmentId: string, completedGroupId: string): Promise<void> {
|
|
631
|
+
const groups = await client!.query(api.jobs.listGroups, { assignmentId: assignmentId as any });
|
|
632
|
+
|
|
633
|
+
const hasIncompleteGroups = groups.some(
|
|
634
|
+
(g: JobGroup) => g.status === "pending" || g.status === "running"
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (hasIncompleteGroups) {
|
|
638
|
+
console.log(`[${completedGroupId}] Assignment ${assignmentId} still has incomplete groups`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
console.log(`[${completedGroupId}] All groups done, marking assignment ${assignmentId} as complete`);
|
|
643
|
+
await client!.mutation(api.assignments.complete, {
|
|
644
|
+
id: assignmentId,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// PM group triggering
|
|
649
|
+
async function triggerPMGroup(
|
|
650
|
+
completedGroup: JobGroup,
|
|
651
|
+
assignment: Assignment,
|
|
652
|
+
failed: boolean
|
|
653
|
+
): Promise<void> {
|
|
654
|
+
const jobType = "pm";
|
|
655
|
+
const context = failed
|
|
656
|
+
? `Previous group failed. Diagnose issues, decide recovery, and choose next job(s).`
|
|
657
|
+
: undefined;
|
|
658
|
+
|
|
659
|
+
console.log(`[${completedGroup._id}] Triggering ${jobType} group`);
|
|
660
|
+
|
|
661
|
+
await client!.mutation(api.jobs.insertGroupAfter, {
|
|
662
|
+
afterGroupId: completedGroup._id,
|
|
663
|
+
jobs: [{
|
|
664
|
+
jobType,
|
|
665
|
+
harness: getHarnessForJobType(jobType),
|
|
666
|
+
context,
|
|
667
|
+
}],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Scheduler interfaces
|
|
672
|
+
interface ReadyJob {
|
|
673
|
+
job: Job;
|
|
674
|
+
group: JobGroup;
|
|
675
|
+
assignment: Assignment;
|
|
676
|
+
accumulatedResults: AccumulatedJobResult[];
|
|
677
|
+
previousNonPmGroupResults: AccumulatedJobResult[];
|
|
678
|
+
r1GroupResults: AccumulatedJobResult[];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
interface ReadyChatJob {
|
|
682
|
+
chatJob: ChatJob;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function processQueue(readyJobs: ReadyJob[]): Promise<void> {
|
|
686
|
+
if (readyJobs.length === 0) return;
|
|
687
|
+
|
|
688
|
+
// Filter out jobs we're already running (tracked by executor)
|
|
689
|
+
const newJobs = readyJobs.filter((r) => !executor.isTracking(r.job._id));
|
|
690
|
+
if (newJobs.length === 0) return;
|
|
691
|
+
|
|
692
|
+
console.log(`[Queue] ${newJobs.length} new jobs to execute (parallel if same group)`);
|
|
693
|
+
|
|
694
|
+
// Execute all ready jobs - scheduler already handles group logic
|
|
695
|
+
for (const {
|
|
696
|
+
job,
|
|
697
|
+
group,
|
|
698
|
+
assignment,
|
|
699
|
+
accumulatedResults,
|
|
700
|
+
previousNonPmGroupResults,
|
|
701
|
+
r1GroupResults,
|
|
702
|
+
} of newJobs) {
|
|
703
|
+
// Double-check assignment status before executing
|
|
704
|
+
const currentAssignment = await client!.query(api.assignments.get, {
|
|
705
|
+
id: assignment._id,
|
|
706
|
+
});
|
|
707
|
+
if (currentAssignment?.status === "blocked") {
|
|
708
|
+
console.log(`[${job._id}] Assignment ${assignment._id} is blocked, skipping`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
executeJob(
|
|
713
|
+
job,
|
|
714
|
+
group,
|
|
715
|
+
assignment,
|
|
716
|
+
accumulatedResults,
|
|
717
|
+
previousNonPmGroupResults,
|
|
718
|
+
r1GroupResults
|
|
719
|
+
).catch((e) => {
|
|
720
|
+
console.error(`Error executing job ${job._id}:`, e);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Chat job execution (separate from assignment jobs, uses HarnessExecutor)
|
|
726
|
+
async function executeChatJob(chatJob: ChatJob): Promise<void> {
|
|
727
|
+
const jobId = chatJob._id;
|
|
728
|
+
console.log(`[${jobId}] Starting chat job (${chatJob.harness})`);
|
|
729
|
+
|
|
730
|
+
const chatContext = parseChatContext(chatJob.context);
|
|
731
|
+
if (!chatContext) {
|
|
732
|
+
console.error(`[${jobId}] Invalid chat context, failing job`);
|
|
733
|
+
await client!.mutation(api.chatJobs.fail, {
|
|
734
|
+
id: jobId,
|
|
735
|
+
result: "Invalid chat context provided",
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const promptType = determinePromptType(chatContext);
|
|
741
|
+
const prompt = buildChatPrompt(chatContext, config.namespace);
|
|
742
|
+
const resumeInfo = chatContext.claudeSessionId
|
|
743
|
+
? ` (resuming session ${chatContext.claudeSessionId.slice(0, 8)}...)`
|
|
744
|
+
: " (new session)";
|
|
745
|
+
console.log(`[${jobId}] Chat mode: ${chatContext.mode}, prompt: ${promptType}, thread: ${chatContext.threadId}${resumeInfo}`);
|
|
746
|
+
|
|
747
|
+
await client!.mutation(api.chatJobs.start, { id: jobId, prompt });
|
|
748
|
+
|
|
749
|
+
const metrics = createMetricsState();
|
|
750
|
+
|
|
751
|
+
const flushMetrics = async (): Promise<void> => {
|
|
752
|
+
if (!client) return;
|
|
753
|
+
const update: {
|
|
754
|
+
id: string;
|
|
755
|
+
toolCallCount?: number;
|
|
756
|
+
subagentCount?: number;
|
|
757
|
+
lastEventAt?: number;
|
|
758
|
+
} = { id: jobId };
|
|
759
|
+
let changed = false;
|
|
760
|
+
|
|
761
|
+
if (metrics.toolCallCount !== metrics.lastFlushedToolCallCount) {
|
|
762
|
+
update.toolCallCount = metrics.toolCallCount;
|
|
763
|
+
metrics.lastFlushedToolCallCount = metrics.toolCallCount;
|
|
764
|
+
changed = true;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (metrics.subagentCount !== metrics.lastFlushedSubagentCount) {
|
|
768
|
+
update.subagentCount = metrics.subagentCount;
|
|
769
|
+
metrics.lastFlushedSubagentCount = metrics.subagentCount;
|
|
770
|
+
changed = true;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (metrics.lastEventAt !== metrics.lastFlushedEventAt) {
|
|
774
|
+
update.lastEventAt = metrics.lastEventAt ?? undefined;
|
|
775
|
+
metrics.lastFlushedEventAt = metrics.lastEventAt;
|
|
776
|
+
changed = true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!changed) return;
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
await client!.mutation(api.chatJobs.updateMetrics, update);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
console.error(`[${jobId}] Failed to update chat job metrics:`, e);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const ensureHeartbeat = (): void => {
|
|
789
|
+
if (metrics.heartbeat) return;
|
|
790
|
+
metrics.heartbeat = setInterval(() => {
|
|
791
|
+
void flushMetrics();
|
|
792
|
+
}, METRICS_HEARTBEAT_MS);
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const stopHeartbeat = (): void => {
|
|
796
|
+
if (!metrics.heartbeat) return;
|
|
797
|
+
clearInterval(metrics.heartbeat);
|
|
798
|
+
metrics.heartbeat = null;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Execute via HarnessExecutor (file-based streaming, crash-resilient)
|
|
802
|
+
executor.execute(
|
|
803
|
+
{
|
|
804
|
+
jobId,
|
|
805
|
+
harness: chatJob.harness as Harness,
|
|
806
|
+
prompt,
|
|
807
|
+
sessionId: chatContext.claudeSessionId || undefined,
|
|
808
|
+
env: {
|
|
809
|
+
WORKFLOW_THREAD_ID: chatContext.threadId,
|
|
810
|
+
WORKFLOW_NAMESPACE_ID: chatContext.namespaceId,
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
onEvent: (event) => {
|
|
815
|
+
recordMetricsEvent(chatJob.harness as Harness, event, metrics);
|
|
816
|
+
ensureHeartbeat();
|
|
817
|
+
},
|
|
818
|
+
onComplete: async (result, sessionId) => {
|
|
819
|
+
stopHeartbeat();
|
|
820
|
+
try {
|
|
821
|
+
await client!.mutation(api.chatJobs.complete, {
|
|
822
|
+
id: jobId,
|
|
823
|
+
result,
|
|
824
|
+
toolCallCount: metrics.toolCallCount,
|
|
825
|
+
subagentCount: metrics.subagentCount,
|
|
826
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
827
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
828
|
+
});
|
|
829
|
+
await saveChatResponse(chatContext.threadId, result);
|
|
830
|
+
if (sessionId) {
|
|
831
|
+
await saveSessionId(chatContext.threadId, sessionId);
|
|
832
|
+
}
|
|
833
|
+
if (!chatContext.isGuardianEvaluation) {
|
|
834
|
+
await saveLastPromptMode(chatContext.threadId, chatContext.effectivePromptMode);
|
|
835
|
+
}
|
|
836
|
+
} catch (e) {
|
|
837
|
+
console.error(`[${jobId}] Error in onComplete:`, e);
|
|
838
|
+
try {
|
|
839
|
+
await saveChatResponse(chatContext.threadId,
|
|
840
|
+
`I completed processing but encountered a system error saving the result. Please check the logs.`
|
|
841
|
+
);
|
|
842
|
+
} catch {
|
|
843
|
+
// Ignore - best effort
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
onFail: async (reason, partialResult) => {
|
|
848
|
+
stopHeartbeat();
|
|
849
|
+
try {
|
|
850
|
+
await client!.mutation(api.chatJobs.fail, {
|
|
851
|
+
id: jobId,
|
|
852
|
+
result: partialResult || reason,
|
|
853
|
+
toolCallCount: metrics.toolCallCount,
|
|
854
|
+
subagentCount: metrics.subagentCount,
|
|
855
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
856
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
857
|
+
});
|
|
858
|
+
await saveChatResponse(chatContext.threadId,
|
|
859
|
+
`I encountered an issue while processing your request. Please try again or rephrase your question.\n\nError details: ${partialResult || reason}`
|
|
860
|
+
);
|
|
861
|
+
} catch (e) {
|
|
862
|
+
console.error(`[${jobId}] Error in onFail:`, e);
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
onTimeout: async (partialResult) => {
|
|
866
|
+
stopHeartbeat();
|
|
867
|
+
try {
|
|
868
|
+
await client!.mutation(api.chatJobs.fail, {
|
|
869
|
+
id: jobId,
|
|
870
|
+
result: `Timeout after ${config.timeoutMs}ms. Partial result:\n${partialResult}`,
|
|
871
|
+
toolCallCount: metrics.toolCallCount,
|
|
872
|
+
subagentCount: metrics.subagentCount,
|
|
873
|
+
totalTokens: metrics.totalTokens ?? undefined,
|
|
874
|
+
lastEventAt: metrics.lastEventAt ?? undefined,
|
|
875
|
+
});
|
|
876
|
+
await saveChatResponse(chatContext.threadId,
|
|
877
|
+
`I apologize, but I ran into a timeout. Here's what I was able to process:\n\n${partialResult || "(no output)"}`
|
|
878
|
+
);
|
|
879
|
+
} catch (e) {
|
|
880
|
+
console.error(`[${jobId}] Error in onTimeout:`, e);
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function processChatQueue(readyChatJobs: ReadyChatJob[]): Promise<void> {
|
|
888
|
+
if (readyChatJobs.length === 0) return;
|
|
889
|
+
|
|
890
|
+
// Filter out jobs we're already running (tracked by executor)
|
|
891
|
+
const newJobs = readyChatJobs.filter((r) => !executor.isTracking(r.chatJob._id));
|
|
892
|
+
if (newJobs.length === 0) return;
|
|
893
|
+
|
|
894
|
+
for (const { chatJob } of newJobs) {
|
|
895
|
+
executeChatJob(chatJob).catch((e) => {
|
|
896
|
+
console.error(`Error executing chat job ${chatJob._id}:`, e);
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Main
|
|
902
|
+
async function startRunner() {
|
|
903
|
+
console.log(`Workflow runner starting for namespace: ${config.namespace}`);
|
|
904
|
+
console.log(`Convex URL: ${config.convexUrl}`);
|
|
905
|
+
|
|
906
|
+
client = new ConvexClient(config.convexUrl);
|
|
907
|
+
|
|
908
|
+
const namespace = await client.query(api.namespaces.getByName, {
|
|
909
|
+
name: config.namespace,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
if (!namespace) {
|
|
913
|
+
console.error(`Error: Namespace "${config.namespace}" not found in database.`);
|
|
914
|
+
console.error("Run 'npx tsx init.ts' to initialize the namespace first.");
|
|
915
|
+
process.exit(1);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const namespaceId = namespace._id;
|
|
919
|
+
console.log(`Found namespace ID: ${namespaceId}`);
|
|
920
|
+
|
|
921
|
+
// Reconcile any orphaned jobs from previous runner crash
|
|
922
|
+
await executor.reconcileOrphans(async (result) => {
|
|
923
|
+
console.log(`[Reconcile] ${result.jobId}: ${result.finalStatus}`);
|
|
924
|
+
// Note: We don't update Convex here because orphaned jobs may have already
|
|
925
|
+
// been marked as failed/timed out by the scheduler. The file-based status
|
|
926
|
+
// is the authoritative record for the TUI monitor.
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Subscribe to assignment-based jobs
|
|
930
|
+
unsubscribeJobs = client.onUpdate(
|
|
931
|
+
api.scheduler.getReadyJobs,
|
|
932
|
+
{ namespaceId },
|
|
933
|
+
(readyJobs: ReadyJob[]) => {
|
|
934
|
+
console.log(`Queue update: ${readyJobs.length} ready jobs`);
|
|
935
|
+
processQueue(readyJobs).catch((e) => {
|
|
936
|
+
console.error("Error processing queue:", e);
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
(error: Error) => {
|
|
940
|
+
console.error("Jobs subscription error:", error);
|
|
941
|
+
cleanup();
|
|
942
|
+
setTimeout(startRunner, 5000);
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Subscribe to chat jobs (separate from assignments)
|
|
947
|
+
unsubscribeChatJobs = client.onUpdate(
|
|
948
|
+
api.scheduler.getReadyChatJobs,
|
|
949
|
+
{ namespaceId },
|
|
950
|
+
(readyChatJobs: ReadyChatJob[]) => {
|
|
951
|
+
if (readyChatJobs.length > 0) {
|
|
952
|
+
console.log(`Chat queue update: ${readyChatJobs.length} ready chat jobs`);
|
|
953
|
+
}
|
|
954
|
+
processChatQueue(readyChatJobs).catch((e) => {
|
|
955
|
+
console.error("Error processing chat queue:", e);
|
|
956
|
+
});
|
|
957
|
+
},
|
|
958
|
+
(error: Error) => {
|
|
959
|
+
console.error("Chat jobs subscription error:", error);
|
|
960
|
+
cleanup();
|
|
961
|
+
setTimeout(startRunner, 5000);
|
|
962
|
+
}
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function cleanup() {
|
|
967
|
+
unsubscribeJobs?.();
|
|
968
|
+
unsubscribeJobs = null;
|
|
969
|
+
unsubscribeChatJobs?.();
|
|
970
|
+
unsubscribeChatJobs = null;
|
|
971
|
+
|
|
972
|
+
// Kill all active jobs via executor
|
|
973
|
+
executor.killAll();
|
|
974
|
+
|
|
975
|
+
client?.close();
|
|
976
|
+
client = null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
startRunner();
|
|
980
|
+
|
|
981
|
+
process.on("SIGINT", () => {
|
|
982
|
+
console.log("\nShutting down...");
|
|
983
|
+
cleanup();
|
|
984
|
+
process.exit(0);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
process.on("SIGTERM", () => {
|
|
988
|
+
console.log("\nShutting down...");
|
|
989
|
+
cleanup();
|
|
990
|
+
process.exit(0);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Keep alive
|
|
994
|
+
setInterval(() => {}, 1000);
|