Skip to content

Commit d29ab5a

Browse files
committed
More progress
1 parent 05dac0e commit d29ab5a

File tree

5 files changed

+228
-23
lines changed

5 files changed

+228
-23
lines changed

apps/cloud-agents/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ DATABASE_URL=postgresql://postgres:password@localhost:5433/cloud_agents
22
REDIS_URL=redis://localhost:6380
33
GITHUB_WEBHOOK_SECRET=your-webhook-secret-here
44
OPENROUTER_API_KEY=your-openrouter-api-key
5+
SLACK_API_TOKEN=xoxb-...

apps/cloud-agents/src/lib/job.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm"
22
import { Job } from "bullmq"
33

44
import { db, cloudJobs, type UpdateCloudJob } from "@/db"
5-
import type { JobTypes, JobType, JobStatus, JobParams } from "@/types"
5+
import type { JobType, JobStatus, JobParams } from "@/types"
66

77
import { fixGitHubIssue } from "./jobs/fixGitHubIssue"
88

@@ -15,7 +15,7 @@ export async function processJob<T extends JobType>({ data: { type, payload, job
1515

1616
switch (type) {
1717
case "github.issue.fix":
18-
result = await fixGitHubIssue(jobId, payload as JobTypes["github.issue.fix"])
18+
result = await fixGitHubIssue(payload)
1919
break
2020
default:
2121
throw new Error(`Unknown job type: ${type}`)
Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,48 @@
11
import * as path from "path"
22
import * as os from "node:os"
33

4-
import type { JobTypes } from "@/types"
4+
import type { JobType, JobPayload } from "@/types"
55

66
import { runTask } from "../runTask"
77
import { Logger } from "../logger"
88

9-
export async function fixGitHubIssue(
10-
jobId: number,
11-
payload: JobTypes["github.issue.fix"],
12-
): Promise<{
9+
const jobType: JobType = "github.issue.fix"
10+
11+
type FixGitHubIssueJobPayload = JobPayload<"github.issue.fix">
12+
13+
export async function fixGitHubIssue(jobPayload: FixGitHubIssueJobPayload): Promise<{
1314
repo: string
1415
issue: number
1516
result: unknown
1617
}> {
1718
const prompt = `
1819
Fix the following GitHub issue:
1920
20-
Repository: ${payload.repo}
21-
Issue #${payload.issue}: ${payload.title}
21+
Repository: ${jobPayload.repo}
22+
Issue #${jobPayload.issue}: ${jobPayload.title}
2223
2324
Description:
24-
${payload.body}
25+
${jobPayload.body}
2526
26-
${payload.labels && payload.labels.length > 0 ? `Labels: ${payload.labels.join(", ")}` : ""}
27+
${jobPayload.labels && jobPayload.labels.length > 0 ? `Labels: ${jobPayload.labels.join(", ")}` : ""}
2728
2829
Please analyze the issue, understand what needs to be fixed, and implement a solution.
29-
If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool.
30-
You'll first need to create a new branch for the pull request.
3130
32-
Make sure to reference the issue in the pull request description.
31+
If you're reasonably satisfied with the solution then create and submit a pull request using the "gh" command line tool:
32+
gh pr create --title "Fixes #${jobPayload.issue}\n\n[Your PR description here.]" --fill --template "pull_request_template.md"
33+
34+
You'll first need to create a new branch for the pull request.
3335
`.trim()
3436

37+
const { repo, issue } = jobPayload
38+
3539
const result = await runTask({
40+
jobType,
41+
jobPayload,
3642
prompt,
3743
publish: async () => {},
38-
logger: new Logger({
39-
logDir: path.resolve(os.tmpdir(), "logs"),
40-
filename: "worker.log",
41-
tag: "worker",
42-
}),
44+
logger: new Logger({ logDir: path.resolve(os.tmpdir(), "logs"), filename: "worker.log", tag: "worker" }),
4345
})
4446

45-
return { repo: payload.repo, issue: payload.issue, result }
47+
return { repo, issue, result }
4648
}

apps/cloud-agents/src/lib/runTask.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import { execa } from "execa"
88
import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types"
99
import { IpcClient } from "@roo-code/ipc"
1010

11+
import type { JobPayload, JobType } from "@/types"
12+
1113
import { Logger } from "./logger"
1214
import { isDockerContainer } from "./utils"
15+
import { SlackNotifier } from "./slack"
1316

1417
const TIMEOUT = 30 * 60 * 1_000
1518

@@ -20,13 +23,21 @@ class SubprocessTimeoutError extends Error {
2023
}
2124
}
2225

23-
type RunTaskOptions = {
26+
type RunTaskOptions<T extends JobType> = {
27+
jobType: T
28+
jobPayload: JobPayload<T>
2429
prompt: string
2530
publish: (taskEvent: TaskEvent) => Promise<void>
2631
logger: Logger
2732
}
2833

29-
export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => {
34+
export const runTask = async <T extends JobType>({
35+
jobType,
36+
jobPayload,
37+
prompt,
38+
publish,
39+
logger,
40+
}: RunTaskOptions<T>) => {
3041
const workspacePath = "/Users/cte/Documents/Roomote-Control" // findGitRoot(process.cwd())
3142
const ipcSocketPath = path.resolve(os.tmpdir(), `${crypto.randomUUID().slice(0, 8)}.sock`)
3243
const env = { ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath }
@@ -73,13 +84,16 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => {
7384
}
7485
}
7586

76-
let taskStartedAt = Date.now() // eslint-disable-line @typescript-eslint/no-unused-vars
87+
let taskStartedAt = Date.now()
7788
let taskFinishedAt: number | undefined
7889
let taskAbortedAt: number | undefined
7990
let taskTimedOut: boolean = false
8091
let rooTaskId: string | undefined
8192
let isClientDisconnected = false
8293

94+
const slackNotifier = new SlackNotifier(logger)
95+
let slackThreadTs: string | null = null
96+
8397
const ignoreEvents: Record<"broadcast" | "log", RooCodeEventName[]> = {
8498
broadcast: [RooCodeEventName.Message],
8599
log: [RooCodeEventName.TaskTokenUsageUpdated, RooCodeEventName.TaskAskResponded],
@@ -105,14 +119,23 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => {
105119
if (eventName === RooCodeEventName.TaskStarted) {
106120
taskStartedAt = Date.now()
107121
rooTaskId = payload[0]
122+
slackThreadTs = await slackNotifier.postTaskStarted({ jobType, jobPayload, rooTaskId })
108123
}
109124

110125
if (eventName === RooCodeEventName.TaskAborted) {
111126
taskAbortedAt = Date.now()
127+
128+
if (slackThreadTs) {
129+
await slackNotifier.postTaskUpdated(slackThreadTs, "Task was aborted", "warning")
130+
}
112131
}
113132

114133
if (eventName === RooCodeEventName.TaskCompleted) {
115134
taskFinishedAt = Date.now()
135+
136+
if (slackThreadTs) {
137+
await slackNotifier.postTaskCompleted(slackThreadTs, true, taskFinishedAt - taskStartedAt, rooTaskId)
138+
}
116139
}
117140
})
118141

@@ -142,6 +165,10 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => {
142165
taskTimedOut = true
143166
logger.error("time limit reached")
144167

168+
if (slackThreadTs) {
169+
await slackNotifier.postTaskUpdated(slackThreadTs, "Task timed out after 30 minutes", "error")
170+
}
171+
145172
if (rooTaskId && !isClientDisconnected) {
146173
logger.info("cancelling task")
147174
client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId })
@@ -153,6 +180,11 @@ export const runTask = async ({ prompt, publish, logger }: RunTaskOptions) => {
153180

154181
if (!taskFinishedAt && !taskTimedOut) {
155182
logger.error("client disconnected before task finished")
183+
184+
if (slackThreadTs) {
185+
await slackNotifier.postTaskUpdated(slackThreadTs, "Client disconnected before task completion", "error")
186+
}
187+
156188
throw new Error("Client disconnected before task completion.")
157189
}
158190

apps/cloud-agents/src/lib/slack.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { JobPayload, JobType } from "@/types"
2+
import { Logger } from "./logger"
3+
4+
export interface SlackMessage {
5+
text: string
6+
blocks?: unknown[]
7+
attachments?: unknown[]
8+
thread_ts?: string
9+
channel?: string
10+
}
11+
12+
// Example response:
13+
// {
14+
// "ok": true,
15+
// "channel": "C123ABC456",
16+
// "ts": "1503435956.000247",
17+
// "message": {
18+
// "text": "Here's a message for you",
19+
// "username": "ecto1",
20+
// "bot_id": "B123ABC456",
21+
// "attachments": [
22+
// {
23+
// "text": "This is an attachment",
24+
// "id": 1,
25+
// "fallback": "This is an attachment's fallback"
26+
// }
27+
// ],
28+
// "type": "message",
29+
// "subtype": "bot_message",
30+
// "ts": "1503435956.000247"
31+
// }
32+
// }
33+
34+
export interface SlackResponse {
35+
ok: boolean
36+
channel?: string
37+
ts?: string
38+
error?: string
39+
message?: Record<string, unknown>
40+
}
41+
42+
export class SlackNotifier {
43+
private readonly logger: Logger
44+
private readonly token: string
45+
46+
constructor(logger: Logger, token: string = process.env.SLACK_API_TOKEN!) {
47+
this.logger = logger
48+
this.token = token
49+
}
50+
51+
private async postMessage(message: SlackMessage): Promise<string | null> {
52+
try {
53+
const messageWithChannel = { ...message, channel: message.channel || "#roomote-control" }
54+
55+
const response = await fetch("https://slack.com/api/chat.postMessage", {
56+
method: "POST",
57+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}` },
58+
body: JSON.stringify(messageWithChannel),
59+
})
60+
61+
if (!response.ok) {
62+
this.logger.error(`Slack API failed: ${response.status} ${response.statusText}`)
63+
return null
64+
}
65+
66+
const result: SlackResponse = await response.json()
67+
68+
if (!result.ok) {
69+
if (result.error === "not_in_channel") {
70+
this.logger.error(
71+
`Slack bot is not a member of channel "${messageWithChannel.channel}". ` +
72+
`Please add the bot to the channel or ensure the channel exists. ` +
73+
`Error: ${result.error}`,
74+
)
75+
} else {
76+
this.logger.error(`Slack API error: ${result.error}`)
77+
}
78+
return null
79+
}
80+
81+
return result.ts ?? null
82+
} catch (error) {
83+
this.logger.error("Failed to send Slack message:", error)
84+
return null
85+
}
86+
}
87+
88+
public async postTaskStarted<T extends JobType>({
89+
jobType,
90+
jobPayload,
91+
rooTaskId,
92+
}: {
93+
jobType: T
94+
jobPayload: JobPayload<T>
95+
rooTaskId: string
96+
}) {
97+
switch (jobType) {
98+
case "github.issue.fix":
99+
return await this.postMessage({
100+
text: `🚀 Task Started`,
101+
blocks: [
102+
{
103+
type: "header",
104+
text: { type: "plain_text", text: "🚀 Roo Code Task Started" },
105+
},
106+
{
107+
type: "section",
108+
text: {
109+
type: "mrkdwn",
110+
text: `Creating a pull request for <https://github.com/RooCodeInc/Roo-Code/issues/${jobPayload.issue}|GitHub Issue #${jobPayload.issue}>`,
111+
},
112+
},
113+
{
114+
type: "context",
115+
elements: [
116+
{
117+
type: "mrkdwn",
118+
text: `jobType: ${jobType}, rooTaskId: ${rooTaskId}`,
119+
},
120+
],
121+
},
122+
],
123+
})
124+
default:
125+
throw new Error(`Unknown job type: ${jobType}`)
126+
}
127+
}
128+
129+
public async postTaskUpdated(
130+
threadTs: string,
131+
text: string,
132+
status?: "info" | "success" | "warning" | "error",
133+
): Promise<void> {
134+
const emoji = { info: "ℹ️", success: "✅", warning: "⚠️", error: "❌" }[status || "info"]
135+
await this.postMessage({ text: `${emoji} ${text}`, thread_ts: threadTs })
136+
}
137+
138+
public async postTaskCompleted(
139+
threadTs: string,
140+
success: boolean,
141+
duration: number,
142+
taskId?: string,
143+
): Promise<void> {
144+
const status = success ? "✅ Completed" : "❌ Failed"
145+
const durationText = `${Math.round(duration / 1000)}s`
146+
147+
await this.postMessage({
148+
text: `${status} Task finished in ${durationText}`,
149+
blocks: [
150+
{
151+
type: "section",
152+
text: {
153+
type: "mrkdwn",
154+
text: `*${status}*\n*Task ID:* ${taskId || "Unknown"}\n*Duration:* ${durationText}`,
155+
},
156+
},
157+
{
158+
type: "context",
159+
elements: [
160+
{
161+
type: "mrkdwn",
162+
text: `Finished at: <!date^${Math.floor(Date.now() / 1000)}^{date_short_pretty} at {time}|${new Date().toISOString()}>`,
163+
},
164+
],
165+
},
166+
],
167+
thread_ts: threadTs,
168+
})
169+
}
170+
}

0 commit comments

Comments
 (0)