cfd38d82fc
* refactor delegation system and add PR summary comments Delegation system: - replace mode-based delegation with select_mode → delegate two-step flow - orchestrator crafts self-contained subagent prompts (clean context — no system/repo/event instructions leak) - add role-based tool filtering via FastMCP authenticate hook (?role=subagent hides orchestrator-only tools) - add select_mode tool for orchestrator guidance per mode - add ask_question tool for lightweight research subagents - extract shared subagent lifecycle into subagent.ts (create, complete, stdout, instructions) - route set_output to per-subagent state when activeSubagentId is set - track per-subagent state (SubagentState Map) replacing boolean delegationActive flag - capture and aggregate AgentUsage across all agents (claude, codex, gemini, opencode) - write usage summary table to GitHub job summary - block built-in subagent spawning (Task for Claude, Task(*) for Cursor) - increase activity timeout from 60s to 300s (subagent thinking phases) - fix gh CLI misguidance in system prompt — explicitly forbid usage PR summary comments: - add prSummaryComment trigger (DB schema + migrations + Zod + UI toggle) - dispatch mini-effort summary job alongside PR review on pr.created - add update_pull_request_body MCP tool - add defaultEffort option to webhook dispatch Hardening: - rewrite delegate/selectMode tests with simulated state management - add toolFiltering.test.ts for role extraction, canAccess, set_output routing - remove non-null assertions for PULLFROG_TEMP_DIR (proper error throws) - use fetchWithRetry for direct tarball downloads - DRY fix for rate limit check in test runner Co-authored-by: Cursor <cursoragent@cursor.com> * fix: add type keyword to Effort import in handleWebhook.ts Co-authored-by: Cursor <cursoragent@cursor.com> * clean up delegation system, improve code quality across the codebase - simplify delegate tool to instructions + effort params with subagent lifecycle in subagent.ts - add select_mode and ask_question orchestrator-only tools with canAccess filtering - replace delegate.test.ts/selectMode.test.ts with toolFiltering.test.ts (live MCP integration) - add set_output routing for subagent context and AgentUsage tracking across all agents - add PR summary comment trigger (schema, UI, webhook dispatch with silent flag) - add update_pull_request_body MCP tool - fix changed-agents.sh to always include claude canary for non-agent action changes - fix cursor pagination bug in getSelectedInstallationReposPage - remove destructuring patterns, inline type definitions, and unsafe type casts - replace non-null assertions with explicit checks in install.ts - convert multi-param functions to single param objects (postCleanup, runActionLocal, etc.) - use isHttpError helper in API routes instead of catch-any patterns - add adhoc test fixtures for delegation scenarios (context isolation, error handling, synthesis, etc.) Co-authored-by: Cursor <cursoragent@cursor.com> * no subagent mutation, one mcp per subagent * address review feedback: parallel-safe usage tracking, subagent isolation, minor improvements * fix subagent state isolation: replace Object.freeze with shallow copy Object.freeze throws TypeErrors when subagent tools (checkout_pr, report_progress) write scalar properties to toolState. A shallow copy achieves the same isolation for scalar fields while allowing tools to work normally. Shared references (subagents Map, usageEntries array) remain shared for coordination. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { performance } from "node:perf_hooks";
|
|
import { log } from "./log.ts";
|
|
|
|
export const DEFAULT_ACTIVITY_TIMEOUT_MS = 300_000;
|
|
export const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5_000;
|
|
|
|
type ActivityTimeoutContext = {
|
|
timeoutMs: number;
|
|
checkIntervalMs: number;
|
|
};
|
|
|
|
export type ActivityTimeout = {
|
|
promise: Promise<never>;
|
|
stop: () => void;
|
|
};
|
|
|
|
type OutputMonitorContext = {
|
|
timeoutMs: number;
|
|
checkIntervalMs: number;
|
|
onTimeout: (idleMs: number) => void;
|
|
};
|
|
|
|
type OutputMonitor = {
|
|
stop: () => void;
|
|
};
|
|
|
|
type WriteCallback = (error?: Error | null) => void;
|
|
type WriteFunction = {
|
|
(chunk: string | Uint8Array, cb?: WriteCallback): boolean;
|
|
(chunk: string | Uint8Array, encoding?: BufferEncoding, cb?: WriteCallback): boolean;
|
|
};
|
|
|
|
// module-level activity tracking - allows agents to mark activity on any event
|
|
let _lastActivity = performance.now();
|
|
|
|
/**
|
|
* mark activity to reset the no-output timeout.
|
|
* call this whenever the agent emits any event, even if it isn't logged to stdout.
|
|
*/
|
|
export function markActivity(): void {
|
|
_lastActivity = performance.now();
|
|
}
|
|
|
|
/**
|
|
* get the time since last activity in milliseconds
|
|
*/
|
|
export function getIdleMs(): number {
|
|
return Math.round(performance.now() - _lastActivity);
|
|
}
|
|
|
|
function wrapWrite(original: WriteFunction, onActivity: () => void): WriteFunction {
|
|
const wrapped: WriteFunction = (
|
|
chunk: string | Uint8Array,
|
|
encodingOrCb?: BufferEncoding | WriteCallback,
|
|
cb?: WriteCallback
|
|
): boolean => {
|
|
onActivity();
|
|
if (typeof encodingOrCb === "function") {
|
|
return original(chunk, encodingOrCb);
|
|
}
|
|
return original(chunk, encodingOrCb, cb);
|
|
};
|
|
return wrapped;
|
|
}
|
|
|
|
function startProcessOutputMonitor(ctx: OutputMonitorContext): OutputMonitor {
|
|
let timedOut = false;
|
|
|
|
const originalStdoutWrite: WriteFunction = process.stdout.write.bind(process.stdout);
|
|
const originalStderrWrite: WriteFunction = process.stderr.write.bind(process.stderr);
|
|
|
|
// stdout/stderr writes also mark activity
|
|
process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
|
|
process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
|
|
|
|
log.debug(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
|
|
|
|
const intervalId = setInterval(() => {
|
|
const idleMs = getIdleMs();
|
|
log.debug(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
|
|
if (timedOut || idleMs <= ctx.timeoutMs) return;
|
|
timedOut = true;
|
|
ctx.onTimeout(idleMs);
|
|
}, ctx.checkIntervalMs);
|
|
|
|
function stop(): void {
|
|
clearInterval(intervalId);
|
|
process.stdout.write = originalStdoutWrite;
|
|
process.stderr.write = originalStderrWrite;
|
|
}
|
|
|
|
return { stop };
|
|
}
|
|
|
|
export function createProcessOutputActivityTimeout(ctx: ActivityTimeoutContext): ActivityTimeout {
|
|
markActivity(); // reset baseline
|
|
|
|
let rejectFn: ((error: Error) => void) | null = null;
|
|
const promise = new Promise<never>((_, reject) => {
|
|
rejectFn = reject;
|
|
});
|
|
|
|
let monitor: OutputMonitor | null = null;
|
|
monitor = startProcessOutputMonitor({
|
|
timeoutMs: ctx.timeoutMs,
|
|
checkIntervalMs: ctx.checkIntervalMs,
|
|
onTimeout: (idleMs) => {
|
|
if (!rejectFn) return;
|
|
const idleSec = Math.round(idleMs / 1000);
|
|
if (monitor) {
|
|
monitor.stop();
|
|
}
|
|
rejectFn(new Error(`activity timeout: no output for ${idleSec}s`));
|
|
},
|
|
});
|
|
|
|
return {
|
|
promise,
|
|
stop: monitor.stop,
|
|
};
|
|
}
|