forked from bai/curriculum-project-hub
feat(hub): raise agent turns/time limits and notify teachers on failure
Defaults and silo env go to 150 turns / 1800s wall clock. Run completion appends a clear Feishu notice for max-turns, timeout, and other failures (partial answer kept). Startup process-restart kills notify the bound chat. Release v0.0.38.
This commit is contained in:
+50
-8
@@ -1,7 +1,7 @@
|
||||
import Fastify from "fastify";
|
||||
import { registerAdminPlugin } from "./admin/plugin.js";
|
||||
import { prisma } from "./db.js";
|
||||
import { createLarkClient, startFeishuListenerWithClient } from "./feishu/client.js";
|
||||
import { createLarkClient, sendText, startFeishuListenerWithClient, type FeishuRuntime } from "./feishu/client.js";
|
||||
import { archiveFeishuBindingForLifecycleEvent } from "./feishu/bindingLifecycle.js";
|
||||
import { makeTriggerHandler } from "./feishu/trigger.js";
|
||||
import { removeAbandonedMessageResourceStages } from "./feishu/resourceStaging.js";
|
||||
@@ -118,15 +118,22 @@ export async function startHub(): Promise<void> {
|
||||
const publicBaseUrl = process.env["HUB_PUBLIC_BASE_URL"] ?? "http://127.0.0.1:8788";
|
||||
const bind = readServerBinding();
|
||||
|
||||
// Startup reset: clear stale locks + mark dead runs as FAILED.
|
||||
await prisma.projectAgentLock.deleteMany({});
|
||||
await prisma.agentRun.updateMany({
|
||||
// Startup reset: clear stale locks + mark dead runs as FAILED. Capture the
|
||||
// killed runs first so we can tell their Feishu chats after the listener is up.
|
||||
const interruptedRuns = await prisma.agentRun.findMany({
|
||||
where: { status: "ACTIVE" },
|
||||
data: { status: "FAILED", error: "process restart", finishedAt: new Date() },
|
||||
select: { id: true, projectId: true },
|
||||
});
|
||||
app.log.info("startup: cleared stale locks + dead runs");
|
||||
await prisma.projectAgentLock.deleteMany({});
|
||||
if (interruptedRuns.length > 0) {
|
||||
await prisma.agentRun.updateMany({
|
||||
where: { id: { in: interruptedRuns.map((run) => run.id) } },
|
||||
data: { status: "FAILED", error: "process restart", finishedAt: new Date() },
|
||||
});
|
||||
}
|
||||
app.log.info({ killedRuns: interruptedRuns.length }, "startup: cleared stale locks + dead runs");
|
||||
|
||||
let feishuRuntime: { readonly isListenerReady?: () => boolean } | undefined;
|
||||
let feishuRuntime: FeishuRuntime | undefined;
|
||||
app.get("/api/healthz", async (_request, reply) => {
|
||||
const feishuReady = feishuRuntime?.isListenerReady?.() ?? !booleanEnv("HUB_FEISHU_LISTENER_ENABLED", true);
|
||||
if (!feishuReady) return reply.status(503).send({ ok: false, feishuReady, ts: Date.now() });
|
||||
@@ -171,7 +178,7 @@ export async function startHub(): Promise<void> {
|
||||
allowLegacyFeishuIdentity: false,
|
||||
maxFeishuEventsPerMinute: feishuEventsPerMinute,
|
||||
});
|
||||
feishuRuntime = await startFeishuListenerWithClient(
|
||||
const runtime = await startFeishuListenerWithClient(
|
||||
feishuConfig,
|
||||
larkClient,
|
||||
app.log,
|
||||
@@ -186,6 +193,8 @@ export async function startHub(): Promise<void> {
|
||||
app.log.info({ ...event, archived: result.archived, projectId: result.projectId }, "feishu binding lifecycle event handled");
|
||||
},
|
||||
);
|
||||
feishuRuntime = runtime;
|
||||
await notifyBoundChatsOfInterruptedRuns(runtime, prisma, interruptedRuns, app.log);
|
||||
} else {
|
||||
app.log.info("feishu listener disabled by HUB_FEISHU_LISTENER_ENABLED");
|
||||
}
|
||||
@@ -193,6 +202,39 @@ export async function startHub(): Promise<void> {
|
||||
app.log.info({ address }, "hub listening");
|
||||
}
|
||||
|
||||
async function notifyBoundChatsOfInterruptedRuns(
|
||||
rt: FeishuRuntime,
|
||||
db: typeof prisma,
|
||||
interruptedRuns: ReadonlyArray<{ readonly id: string; readonly projectId: string }>,
|
||||
logger: { info: (obj: unknown, msg?: string) => void; warn: (obj: unknown, msg?: string) => void },
|
||||
): Promise<void> {
|
||||
if (interruptedRuns.length === 0) return;
|
||||
const byProject = new Map<string, string[]>();
|
||||
for (const run of interruptedRuns) {
|
||||
const list = byProject.get(run.projectId) ?? [];
|
||||
list.push(run.id);
|
||||
byProject.set(run.projectId, list);
|
||||
}
|
||||
for (const [projectId, runIds] of byProject) {
|
||||
const binding = await db.projectGroupBinding.findFirst({
|
||||
where: { projectId, archivedAt: null },
|
||||
select: { chatId: true },
|
||||
});
|
||||
if (binding === null) continue;
|
||||
const n = runIds.length;
|
||||
const text =
|
||||
n === 1
|
||||
? `\u26A0\uFE0F \u670D\u52A1\u521A\u521A\u91CD\u542F\uFF0C\u4E0A\u4E00\u4E2A\u6B63\u5728\u8FDB\u884C\u7684\u4EFB\u52A1\u88AB\u4E2D\u65AD\u4E86\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77\u8BF7\u6C42\uFF08run: ${runIds[0]}\uFF09\u3002`
|
||||
: `\u26A0\uFE0F \u670D\u52A1\u521A\u521A\u91CD\u542F\uFF0C${n} \u4E2A\u6B63\u5728\u8FDB\u884C\u7684\u4EFB\u52A1\u88AB\u4E2D\u65AD\u4E86\u3002\u8BF7\u91CD\u65B0\u53D1\u8D77\u8BF7\u6C42\u3002`;
|
||||
try {
|
||||
await sendText(rt, binding.chatId, text);
|
||||
logger.info({ projectId, chatId: binding.chatId, runIds }, "startup: notified chat of interrupted runs");
|
||||
} catch (error) {
|
||||
logger.warn({ projectId, chatId: binding.chatId, err: String(error) }, "startup: failed to notify chat of interrupted runs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function positiveIntegerEnv(name: string): number {
|
||||
const raw = requireEnv(name);
|
||||
const value = Number(raw);
|
||||
|
||||
Reference in New Issue
Block a user