diff --git a/pi-worker/src/config.ts b/pi-worker/src/config.ts index 7218045..b046b3a 100644 --- a/pi-worker/src/config.ts +++ b/pi-worker/src/config.ts @@ -17,6 +17,7 @@ export interface Config { extensionsPath: string; agentsPath: string; logDir: string; + projectsPath: string; // Timing pollIntervalSec: number; @@ -46,6 +47,7 @@ export function loadConfig(): Config { extensionsPath: process.env.EXTENSIONS_PATH || "../extensions", agentsPath: process.env.AGENTS_PATH || "../.pi/agents", logDir: process.env.LOG_DIR || "./logs", + projectsPath: process.env.PROJECTS_PATH || "./projects", pollIntervalSec: parseInt(process.env.POLL_INTERVAL_SEC || "120"), improvementIntervalSec: parseInt(process.env.IMPROVEMENT_INTERVAL_SEC || "3600"), diff --git a/pi-worker/src/health/server.ts b/pi-worker/src/health/server.ts index 7b3092c..03e1990 100644 --- a/pi-worker/src/health/server.ts +++ b/pi-worker/src/health/server.ts @@ -79,16 +79,16 @@ function startWatchdog(healthPort: number) { return; } - // Notify systemd via NOTIFY_SOCKET - const notifySocket = process.env.NOTIFY_SOCKET; - if (notifySocket) { - const { createSocket } = await import("dgram"); - const socketPath = notifySocket.startsWith("@") - ? `\0${notifySocket.slice(1)}` - : notifySocket; - const client = createSocket("unix_dgram" as any); - const msg = Buffer.from("WATCHDOG=1"); - client.send(msg, 0, msg.length, socketPath, () => client.close()); + // Notify systemd via systemd-notify command + try { + const proc = Bun.spawn({ + cmd: ["systemd-notify", "WATCHDOG=1"], + stdout: "ignore", + stderr: "ignore", + }); + await proc.exited; + } catch { + // systemd-notify not available — not critical } } catch (e) { logger.error("health", `Watchdog ping failed: ${e}`); diff --git a/pi-worker/src/pi/executor.ts b/pi-worker/src/pi/executor.ts index 81ea8c3..4f481bf 100644 --- a/pi-worker/src/pi/executor.ts +++ b/pi-worker/src/pi/executor.ts @@ -13,7 +13,7 @@ export class PiExecutor { this.config = config; } - async execute(task: string, context?: string, timeoutMs?: number): Promise { + async execute(task: string, context?: string, timeoutMs?: number, cwd?: string): Promise { const startTime = Date.now(); let output = ""; let toolCalls = 0; @@ -33,9 +33,11 @@ export class PiExecutor { fullPrompt, ]; + const workingDir = cwd || this.config.targetProjectPath; + logger.info("pi-executor", `Executing task: ${task.substring(0, 100)}...`, { model: this.config.piModel, - cwd: this.config.targetProjectPath, + cwd: workingDir, }); try { @@ -45,7 +47,7 @@ export class PiExecutor { const proc = spawn({ cmd: args, - cwd: this.config.targetProjectPath, + cwd: workingDir, stdout: "pipe", stderr: "pipe", env: cleanEnv, diff --git a/pi-worker/src/scheduler/lock.ts b/pi-worker/src/scheduler/lock.ts index e0329dd..499560e 100644 --- a/pi-worker/src/scheduler/lock.ts +++ b/pi-worker/src/scheduler/lock.ts @@ -77,7 +77,12 @@ export class FileLock { writeFileSync(this.lockPath, Date.now().toString(), { flag: 'wx' }); this.held = true; return true; - } catch { + } catch (e) { + const code = (e as NodeJS.ErrnoException).code; + if (code !== "EEXIST") { + // Unexpected error (permissions, disk full, etc.) — log it + process.stderr.write(`[lock] acquire failed for ${this.lockPath}: ${code || e}\n`); + } return false; } } diff --git a/pi-worker/src/scheduler/task-loop.ts b/pi-worker/src/scheduler/task-loop.ts index 7a2bab1..c708307 100644 --- a/pi-worker/src/scheduler/task-loop.ts +++ b/pi-worker/src/scheduler/task-loop.ts @@ -1,3 +1,5 @@ +import { readdirSync, existsSync } from "fs"; +import { join } from "path"; import { AsanaClient } from "../asana/client.js"; import { PiExecutor } from "../pi/executor.js"; import { logger } from "../logger.js"; @@ -5,10 +7,58 @@ import { FileLock } from "./lock.js"; import type { Config } from "../config.js"; import type { AsanaTask, AsanaSection } from "../asana/types.js"; +/** + * Section classification for the Bug Intake board. + * + * PICKUP = sections we actively pull tasks FROM to execute + * EXECUTE = "Under Review" / "In Progress" — where we move tasks during execution + * DONE = where completed tasks land + * SKIP = sections we never touch (deferred, won't fix) + */ const SECTIONS = { - TODO: ["Pi Worker", "To Do", "New Bugs", "Ready for Development", "Recently Assigned", "Awaiting More Info"], - IN_PROGRESS: ["In Progress", "Under Review"], - DONE: ["Done", "Resolved", "Complete", "Completed"], + PICKUP: ["New Bugs", "Ready for Development"], + EXECUTE: ["Under Review", "In Progress"], + DONE: ["Resolved", "Done", "Complete", "Completed"], + SKIP: ["Deferred", "Won't Fix", "Awaiting More Info"], +}; + +/** + * Map of keywords in task names/notes → project directory names. + * Used to route tasks to the correct codebase for execution. + */ +const PROJECT_KEYWORDS: Record = { + "legacy-donation-system-laravel": [ + "laravel", "legacy", "donation system", "receipt", "email receipt", + "pledge", "donor", "donation", "fundrais", "campaign", "appeal", + "checkout", "payment", "stripe", "paypal", "gocardless", + "cross sell", "team page", "ben nevis", + ], + "charity-right-uk-v2": [ + "charity-right", "charityright", "website", "frontend", "next.js", + "nextjs", "landing", "home page", "UI", "layout", "styling", + ], + "checkout-v2": [ + "checkout-v2", "checkout v2", "new checkout", "payment flow", + ], + "donation-dashboard": [ + "dashboard", "admin", "analytics", "reporting", "reports", + "export", "data export", + ], + "command-center": [ + "command center", "command-center", "ops", "monitoring", + ], + "charityright-sync": [ + "sync", "n3o", "engage", "import", "county", "state", "mapping", + ], + "enthuse-db-sync-v2": [ + "enthuse", "enthuse sync", "db sync", + ], + "launchgood-sync": [ + "launchgood", "launch good", + ], + "marketing-site": [ + "marketing", "blog", "wordpress", "seo", "content", + ], }; export class TaskLoop { @@ -22,13 +72,17 @@ export class TaskLoop { private workspaceGid: string = ""; // Cache: track tasks we already verified as "not done" to avoid re-checking - private verifiedNotDone: Map = new Map(); // gid -> timestamp + private verifiedNotDone: Map = new Map(); // gid → timestamp private readonly VERIFY_CACHE_TTL = 30 * 60 * 1000; // 30 min // Cache: sections per project private sectionCache: Map = new Map(); private readonly SECTION_CACHE_TTL = 5 * 60 * 1000; // 5 min + // Track consecutive failures to back off + private consecutiveFailures = 0; + private readonly MAX_BACKOFF_MULTIPLIER = 5; + constructor(config: Config) { this.config = config; this.asana = new AsanaClient(config.asanaAccessToken); @@ -41,12 +95,17 @@ export class TaskLoop { logger.info("task-loop", "Starting Asana task polling loop", { interval: `${this.config.pollIntervalSec}s`, project: this.config.asanaProjectGid, + projectsPath: this.getProjectsDir(), }); if (!this.config.asanaProjectGid) { await this.discoverProject(); } + // Log available projects for debugging + const projects = this.getAvailableProjects(); + logger.info("task-loop", `Available codebases: ${projects.join(", ") || "NONE"}`); + await this.poll(); this.timer = setInterval(() => this.poll(), this.config.pollIntervalSec * 1000); } @@ -61,6 +120,74 @@ export class TaskLoop { this.lock.release(); } + // ─── Project resolution ────────────────────────────────────────────── + + private getProjectsDir(): string { + return join(this.config.targetProjectPath, this.config.projectsPath); + } + + private getAvailableProjects(): string[] { + const dir = this.getProjectsDir(); + if (!existsSync(dir)) return []; + return readdirSync(dir, { withFileTypes: true }) + .filter((d) => d.isDirectory() && !d.name.startsWith(".")) + .map((d) => d.name); + } + + /** + * Determine which project codebase a task should execute against. + * Returns the absolute path to the project directory, or the + * targetProjectPath as fallback. + */ + private resolveProjectForTask(task: AsanaTask): { projectDir: string; projectName: string } { + const searchText = `${task.name} ${task.notes || ""}`.toLowerCase(); + const available = this.getAvailableProjects(); + + // First: check if the task name contains a [project] tag (from improvement loop) + const tagMatch = task.name.match(/\[([^\]]+)\]/); + if (tagMatch) { + const tagName = tagMatch[1].toLowerCase().replace(/\s+/g, "-"); + const exact = available.find((p) => p.toLowerCase() === tagName); + if (exact) { + return { + projectDir: join(this.getProjectsDir(), exact), + projectName: exact, + }; + } + } + + // Second: keyword matching + let bestMatch: { project: string; score: number } | null = null; + + for (const [project, keywords] of Object.entries(PROJECT_KEYWORDS)) { + if (!available.includes(project)) continue; + let score = 0; + for (const keyword of keywords) { + if (searchText.includes(keyword.toLowerCase())) { + score += keyword.length; // Longer matches score higher + } + } + if (score > 0 && (!bestMatch || score > bestMatch.score)) { + bestMatch = { project, score }; + } + } + + if (bestMatch) { + return { + projectDir: join(this.getProjectsDir(), bestMatch.project), + projectName: bestMatch.project, + }; + } + + // Fallback: use the main project path + return { + projectDir: this.config.targetProjectPath, + projectName: "unknown", + }; + } + + // ─── Discovery ─────────────────────────────────────────────────────── + private async discoverProject() { try { logger.info("task-loop", "No project GID configured, discovering..."); @@ -98,6 +225,8 @@ export class TaskLoop { } } + // ─── Main poll loop ────────────────────────────────────────────────── + private async poll() { if (!this.running || !this.config.asanaProjectGid) return; if (!this.lock.acquire()) { @@ -106,15 +235,25 @@ export class TaskLoop { } try { - // Step 1: Quick cleanup - move completed My Tasks to Done sections + // Step 1: Quick cleanup — move completed My Tasks to Done sections await this.cleanupCompletedMyTasks(); - // Step 2: Sweep incomplete tasks for already-done work (with caching) - await this.sweepIncompleteTasks(); + // Step 2: Pick up tasks to execute + // Check PICKUP sections first (New Bugs, Ready for Development) + // Then check EXECUTE sections (Under Review) for tasks that haven't been attempted + logger.info("task-loop", "Polling for Asana tasks..."); - // Step 3: Pick up new tasks to execute - logger.info("task-loop", "Polling for new Asana tasks..."); - const tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.TODO); + let tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.PICKUP); + + if (tasks.length === 0) { + // Also check Under Review — these are real bugs waiting for someone to fix them + tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.EXECUTE); + + // Filter out tasks we recently attempted (check comments for Pi Worker activity) + if (tasks.length > 0) { + tasks = await this.filterUnatttemptedTasks(tasks); + } + } if (tasks.length === 0) { logger.info("task-loop", "No pending tasks found"); @@ -122,17 +261,53 @@ export class TaskLoop { return; } + // Pick the first task const task = tasks[0]; - logger.info("task-loop", `Picked task: ${task.name}`, { gid: task.gid }); - await this.executeTask(task); + const { projectDir, projectName } = this.resolveProjectForTask(task); + logger.info("task-loop", `Picked task: "${task.name}"`, { + gid: task.gid, + project: projectName, + projectDir, + }); + + await this.executeTask(task, projectDir, projectName); } catch (error: any) { logger.error("task-loop", "Poll failed", { error: error.message }); + this.consecutiveFailures++; } finally { this.lock.release(); } } - // Quick: just move completed tasks out of Recently Assigned. No Pi calls. + /** + * Filter out tasks that Pi Worker has already commented on (attempted). + * Prevents re-attempting failed tasks every cycle. + */ + private async filterUnatttemptedTasks(tasks: AsanaTask[]): Promise { + const unattempted: AsanaTask[] = []; + + for (const task of tasks) { + try { + const comments = await this.asana.getComments(task.gid); + const hasWorkerComment = comments.some( + (c) => c.text && c.text.includes("Pi Worker") + ); + if (!hasWorkerComment) { + unattempted.push(task); + } else { + logger.debug("task-loop", `Skipping already-attempted task: "${task.name}"`); + } + } catch { + // If we can't read comments, include the task + unattempted.push(task); + } + } + + return unattempted; + } + + // ─── Cleanup ───────────────────────────────────────────────────────── + private async cleanupCompletedMyTasks() { if (!this.userTaskListGid) return; @@ -146,7 +321,6 @@ export class TaskLoop { for (const task of completedTasks) { try { - // Move to Done section in the task's project if (task.memberships && task.memberships.length > 0) { for (const membership of task.memberships) { const projectGid = membership.project?.gid; @@ -166,7 +340,6 @@ export class TaskLoop { } } - // Move out of Recently Assigned await this.asana.updateTask(task.gid, { assignee_status: "later" }); } catch (e: any) { logger.debug("task-loop", `Could not clean task ${task.name}: ${e.message}`); @@ -177,131 +350,50 @@ export class TaskLoop { } } - // Sweep: verify incomplete tasks - are they already done? Uses cache to skip recently checked. - private async sweepIncompleteTasks() { + // ─── Task execution ───────────────────────────────────────────────── + + private async executeTask(task: AsanaTask, projectDir: string, projectName: string) { try { - const sweepSections = [...SECTIONS.TODO, ...SECTIONS.IN_PROGRESS]; - const allTasks: AsanaTask[] = []; + // Step 1: Move to Under Review + const executeSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.EXECUTE); + if (executeSection) await this.asana.moveTaskToSection(task.gid, executeSection.gid); - for (const sectionName of sweepSections) { - const section = await this.findSectionCached(this.config.asanaProjectGid, sectionName); - if (!section) continue; - const tasks = await this.asana.getSectionTasks(section.gid); - allTasks.push(...tasks.filter((t) => !t.completed)); - } + await this.asana.addComment( + task.gid, + `🤖 Pi Worker picked up this task.\nProject: \`${projectName}\`\nStarted: ${new Date().toISOString()}` + ); - // Also get incomplete My Tasks - if (this.userTaskListGid) { - try { - const myTasks = await this.asana.getUserTasks(this.userTaskListGid); - allTasks.push(...myTasks.filter((t) => !t.completed)); - } catch {} - } + // Step 2: Build context with project awareness + const context = this.buildTaskContext(task, projectDir, projectName); - // Deduplicate - const seen = new Set(); - const uniqueTasks = allTasks.filter((t) => { - if (seen.has(t.gid)) return false; - seen.add(t.gid); - return true; - }); + // Step 3: Pull latest code + await this.gitPull(projectDir, projectName); - // Filter out recently verified tasks (cache hit) - const now = Date.now(); - const tasksToVerify = uniqueTasks.filter((t) => { - const lastCheck = this.verifiedNotDone.get(t.gid); - if (lastCheck && now - lastCheck < this.VERIFY_CACHE_TTL) return false; - return true; - }); + // Step 4: Execute with Pi in the correct project directory + const result = await this.pi.execute( + task.name + (task.notes ? `\n\nDetails:\n${task.notes}` : ""), + context, + undefined, + projectDir, // Execute in the project directory + ); - if (tasksToVerify.length === 0) { - logger.debug("task-loop", `Sweep: ${uniqueTasks.length} tasks all recently verified, skipping`); - return; - } - - logger.info("task-loop", `Sweeping ${tasksToVerify.length} tasks (${uniqueTasks.length - tasksToVerify.length} cached, skipped)`); - - for (const task of tasksToVerify) { - const check = await this.verifyTaskAlreadyDone(task); - - if (check.done) { - logger.info("task-loop", `Sweep: "${task.name}" is already done`, { evidence: check.evidence }); - - await this.asana.addComment( - task.gid, - `✅ Pi Worker sweep: This task is **already completed**.\n\nEvidence:\n${check.evidence}` - ); - - // Move to done section if task has project membership - if (task.memberships && task.memberships.length > 0) { - for (const membership of task.memberships) { - const projectGid = membership.project?.gid; - if (projectGid) { - const doneSection = await this.findDoneSection(projectGid); - if (doneSection) { - await this.asana.moveTaskToSection(task.gid, doneSection.gid); - } - } - } - } - - await this.asana.updateTask(task.gid, { completed: true }); - logger.info("task-loop", `Sweep: Marked done: ${task.name}`); - } else { - // Cache as "not done" to avoid re-verifying next cycle - this.verifiedNotDone.set(task.gid, now); - logger.debug("task-loop", `Sweep: "${task.name}" not yet done`); - } - } - - // Clean expired cache entries - for (const [gid, ts] of this.verifiedNotDone) { - if (now - ts > this.VERIFY_CACHE_TTL) this.verifiedNotDone.delete(gid); - } - } catch (error: any) { - logger.error("task-loop", `Sweep error: ${error.message}`); - } - } - - private async executeTask(task: AsanaTask) { - try { - // Step 1: Quick pre-verify - const alreadyDone = await this.verifyTaskAlreadyDone(task); - - if (alreadyDone.done) { - logger.info("task-loop", `Task already done: ${task.name}`); - await this.asana.addComment(task.gid, `✅ Pi Worker verified this task is **already completed**.\n\nEvidence:\n${alreadyDone.evidence}`); - const doneSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.DONE); - if (doneSection) await this.asana.moveTaskToSection(task.gid, doneSection.gid); - await this.asana.updateTask(task.gid, { completed: true }); - return; - } - - // Step 2: Move to In Progress - const inProgressSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.IN_PROGRESS); - if (inProgressSection) await this.asana.moveTaskToSection(task.gid, inProgressSection.gid); - - await this.asana.addComment(task.gid, `🤖 Pi Worker picked up this task.\nStarting execution at ${new Date().toISOString()}`); - - const context = this.buildTaskContext(task); - - // Step 3: Execute - const result = await this.pi.execute(task.name + (task.notes ? `\n\nDetails:\n${task.notes}` : ""), context); - - // Step 4: Post-verify + // Step 5: Post-verify let verified = false; if (result.success && result.toolCalls > 0) { - const postCheck = await this.verifyTaskAlreadyDone(task); + const postCheck = await this.verifyTaskDone(task, projectDir); verified = postCheck.done; } const statusEmoji = result.success ? (verified ? "✅" : "⚠️") : "❌"; const verificationNote = result.success - ? (verified ? "Post-execution verification: PASSED" : "Post-execution verification: COULD NOT CONFIRM — please review manually") + ? (verified + ? "Post-execution verification: **PASSED**" + : "Post-execution verification: COULD NOT CONFIRM — please review manually") : ""; const comment = [ `${statusEmoji} Pi Worker ${result.success ? "completed" : "failed"} this task.`, + `Project: \`${projectName}\``, verificationNote, `Duration: ${(result.durationMs / 1000).toFixed(1)}s`, `Tool calls: ${result.toolCalls}`, @@ -319,23 +411,27 @@ export class TaskLoop { const doneSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.DONE); if (doneSection) await this.asana.moveTaskToSection(task.gid, doneSection.gid); await this.asana.updateTask(task.gid, { completed: true }); - logger.info("task-loop", `Task completed: ${task.name}`, { verified }); + logger.info("task-loop", `Task completed: ${task.name}`, { verified, project: projectName }); + this.consecutiveFailures = 0; } else { - const todoSection = await this.findFirstSection(this.config.asanaProjectGid, SECTIONS.TODO); - if (todoSection) await this.asana.moveTaskToSection(task.gid, todoSection.gid); - logger.error("task-loop", `Task failed: ${task.name}`, { error: result.error }); + // Leave in Under Review — don't move back to New Bugs + // The filterUnattemptedTasks check will skip it on future polls + logger.error("task-loop", `Task failed: ${task.name}`, { error: result.error, project: projectName }); + this.consecutiveFailures++; } } catch (error: any) { logger.error("task-loop", `Task execution error: ${task.name}`, { error: error.message }); + this.consecutiveFailures++; try { await this.asana.addComment(task.gid, `❌ Pi Worker crashed: ${error.message}`); } catch {} } } - private async verifyTaskAlreadyDone(task: AsanaTask): Promise<{ done: boolean; evidence: string }> { + private async verifyTaskDone(task: AsanaTask, projectDir: string): Promise<{ done: boolean; evidence: string }> { try { - const verifyPrompt = `CHECK if this task is already done. Be FAST - spend max 30 seconds investigating. Do NOT fix anything. Do NOT make changes. Just check and report. + const verifyPrompt = `CHECK if this task is already done. Be FAST — spend max 30 seconds. +Do NOT fix anything. Do NOT make changes. Just check and report. Task: ${task.name} ${task.notes ? `Details: ${task.notes}` : ""} @@ -343,7 +439,8 @@ ${task.notes ? `Details: ${task.notes}` : ""} Quick checks only: - ls and grep relevant files/dirs - Check if a fix/feature is already in the code -- Check running services if relevant (curl, systemctl) +- Check git log for recent relevant commits +- Check running services if relevant (curl localhost) - Do NOT run find on large directories - Do NOT clone repos or install anything @@ -351,7 +448,7 @@ RESPOND EXACTLY: DONE: true or false EVIDENCE: one line why`; - const result = await this.pi.execute(verifyPrompt, undefined, 120_000); + const result = await this.pi.execute(verifyPrompt, undefined, 120_000, projectDir); if (!result.success) { return { done: false, evidence: "Verification check failed to run" }; @@ -371,7 +468,22 @@ EVIDENCE: one line why`; } } - // Cached section lookup per project + private async gitPull(projectDir: string, projectName: string) { + try { + const proc = Bun.spawn({ + cmd: ["git", "-C", projectDir, "pull", "--ff-only"], + stdout: "pipe", + stderr: "pipe", + }); + await proc.exited; + logger.debug("task-loop", `Git pull: ${projectName}`); + } catch { + logger.debug("task-loop", `Git pull failed for ${projectName}`); + } + } + + // ─── Section helpers ───────────────────────────────────────────────── + private async findSectionCached(projectGid: string, sectionName: string): Promise { const cached = this.sectionCache.get(projectGid); let sections: AsanaSection[]; @@ -403,22 +515,37 @@ EVIDENCE: one line why`; } private async getTasksFromSections(projectGid: string, sectionNames: string[]): Promise { + const allTasks: AsanaTask[] = []; for (const name of sectionNames) { const section = await this.findSectionCached(projectGid, name); if (!section) continue; const tasks = await this.asana.getSectionTasks(section.gid); const incomplete = tasks.filter((t) => !t.completed); - if (incomplete.length > 0) return incomplete; + allTasks.push(...incomplete); } - return []; + return allTasks; } - private buildTaskContext(task: AsanaTask): string { + // ─── Context building ─────────────────────────────────────────────── + + private buildTaskContext(task: AsanaTask, projectDir: string, projectName: string): string { const parts = [ `Asana Task: ${task.name}`, task.notes ? `Description: ${task.notes}` : "", task.due_on ? `Due: ${task.due_on}` : "", task.tags?.length > 0 ? `Tags: ${task.tags.map((t) => t.name).join(", ")}` : "", + "", + `You are working on the "${projectName}" codebase.`, + `The code is at: ${projectDir}`, + "", + `IMPORTANT RULES:`, + `- Read relevant files before making any changes`, + `- Make small, focused changes that fix the specific bug`, + `- Test your changes (run type checks, linters, or curl endpoints)`, + `- Commit with a clear message referencing the Asana task`, + `- Do NOT modify .env files or credentials`, + `- Do NOT run destructive commands (rm -rf, DROP DATABASE)`, + `- If the bug is in the database data (not code), describe what SQL fix is needed but do NOT run it`, ]; return parts.filter(Boolean).join("\n"); } diff --git a/telegram-bot/bot.err b/telegram-bot/bot.err index e69de29..41b5212 100644 --- a/telegram-bot/bot.err +++ b/telegram-bot/bot.err @@ -0,0 +1,46 @@ +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The operation timed out. +Network error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the sec +Network error: The operation timed out. +Network error: The operation timed out. diff --git a/telegram-bot/bot.log b/telegram-bot/bot.log index 273f1dd..419c8e1 100644 --- a/telegram-bot/bot.log +++ b/telegram-bot/bot.log @@ -1,2 +1,9 @@ 🤖 CharityRight DevOps Bot starting... ✅ Bot online — polling for messages +🤖 CharityRight DevOps Bot starting... +✅ Bot online — polling for messages +🤖 CharityRight DevOps Bot starting... +✅ Bot online — polling for messages +🤖 CharityRight DevOps Bot starting... +✅ Bot online — polling for messages +⚠️ 409 conflict — waiting 10s...