diff --git a/hub/src/feishu/trigger.ts b/hub/src/feishu/trigger.ts index 28ff748..0793b00 100644 --- a/hub/src/feishu/trigger.ts +++ b/hub/src/feishu/trigger.ts @@ -156,7 +156,7 @@ export function makeTriggerHandler(deps: TriggerDeps) { // ADR-0002: if locked by another run, reply busy. const existing = await currentLockRunId(deps.prisma, projectId); if (existing !== null) { - await sendText(rt, chatId, "项目正在处理中,请稍候。"); + await reactToMessage(rt, msg.message_id, "Hourglass"); return; } @@ -243,7 +243,7 @@ export function makeTriggerHandler(deps: TriggerDeps) { data: { status: "FAILED", error: "lock race", finishedAt: new Date() }, }); await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.lock_race", metadata: {} }); - await sendText(rt, chatId, "项目正在处理中,请稍候。"); + await reactToMessage(rt, msg.message_id, "Hourglass"); return; } @@ -257,17 +257,23 @@ export function makeTriggerHandler(deps: TriggerDeps) { // Emoji reaction on the user's message (acknowledge receipt). await reactToMessage(rt, msg.message_id, "THUMBSUP"); - // Stream agent response as text-as-card messages — patchable, markdown, - // no truncation. Lower throttle (400ms) for smoother streaming. - let currentMsgId = await sendTextMessage(rt, chatId, ""); + // Stream agent response as text-as-card messages. Lazy-init: don't send + // anything until the first text-delta arrives (avoids empty placeholder). + let currentMsgId: string | null = null; let accText = ""; let lastPatch = 0; const PATCH_INTERVAL = 400; const MAX_MSG_LEN = 4000; const flushText = async () => { + if (currentMsgId === null) { + // First chunk — create the message immediately (no throttle on init). + currentMsgId = await sendTextMessage(rt, chatId, accText); + lastPatch = Date.now(); + return; + } const now = Date.now(); - if (now - lastPatch < PATCH_INTERVAL || currentMsgId === null) return; + if (now - lastPatch < PATCH_INTERVAL) return; lastPatch = now; if (accText.length > MAX_MSG_LEN) { await patchTextMessage(rt, currentMsgId, accText.slice(0, MAX_MSG_LEN)); @@ -294,9 +300,14 @@ export function makeTriggerHandler(deps: TriggerDeps) { }, }) .then(async (result) => { - // Final flush: make sure the last message has complete text. + // Final flush: patch the last message with complete text, or send + // the full response if no streaming message was created yet. if (currentMsgId !== null && accText.length > 0) { await patchTextMessage(rt, currentMsgId, accText); + } else if (currentMsgId === null && result.text.length > 0) { + // No text-delta was received (e.g. model returned all at once). + // Send the full text now. + await sendTextMessage(rt, chatId, result.text); } await deps.prisma.agentRun.update({ where: { id: run.id }, diff --git a/hub/src/server.ts b/hub/src/server.ts index 798f675..d78c60c 100644 --- a/hub/src/server.ts +++ b/hub/src/server.ts @@ -42,6 +42,16 @@ async function main(): Promise { const app = Fastify({ logger: true }); + // Startup reset: clear stale locks + mark dead runs as FAILED. + await prisma.projectAgentLock.deleteMany({}); + await prisma.agentRun.updateMany({ + where: { status: "ACTIVE" }, + data: { status: "FAILED", error: "process restart", finishedAt: new Date() }, + }); + app.log.info("startup: cleared stale locks + dead runs"); + + + app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() })); // Model registry (role-as-data, ADR-0017). Claude Code SDK uses model