fix(hub): agent 会话记忆在配置变更/发版后丢失 + Feishu thread 400 (#19)

Co-authored-by: Hong Jiarong <me@jrhim.com>
Co-committed-by: Hong Jiarong <me@jrhim.com>
This commit is contained in:
2026-07-20 22:45:42 +08:00
committed by 洪佳荣
parent 5f668d71a2
commit 54b9fee22c
5 changed files with 85 additions and 21 deletions
+45 -1
View File
@@ -198,8 +198,19 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
if (req.abortController !== undefined) options.abortController = req.abortController;
if (req.onSdkStderr !== undefined) options.stderr = req.onSdkStderr;
// When there is no provider session cursor to resume (first run, or after a
// role/skill/model config change invalidated claudeSessionId), re-seed the
// conversation from this Hub session's prior AgentMessage rows. This keeps
// the logical session continuous across config changes and restarts — the
// agent still "remembers" the earlier turns even though the SDK starts a
// fresh provider session. The resume path above is preferred when available
// (it carries tool calls/results natively and avoids re-sending tokens).
const promptForAgent = req.resumeSessionId === undefined
? await withSessionHistory(req, req.prompt)
: req.prompt;
const conversation = query({
prompt: req.prompt,
prompt: promptForAgent,
options,
});
@@ -333,6 +344,39 @@ export async function runAgent(req: RunRequest): Promise<RunResult> {
}
}
/**
* Re-seed a run's prompt with this Hub session's prior conversation when the
* SDK cannot resume a provider session (no `resumeSessionId`). Pulls prior
* `AgentMessage` rows for this session — excluding the current run's own user
* message, which was already persisted before `runAgent` called this — and
* frames them as `<session_history>` so the model treats them as prior turns,
* not new instructions. The current run's prompt follows as the live request.
*
* Best-effort: if the history query fails, the run proceeds with the bare
* prompt rather than aborting. A cap (`MAX_HISTORY_TURNS`) bounds token cost;
* older turns beyond the cap are dropped, preserving the most recent context.
*/
async function withSessionHistory(req: RunRequest, prompt: string): Promise<string> {
const MAX_HISTORY_TURNS = 40;
try {
const messages = await req.prisma.agentMessage.findMany({
where: { sessionId: req.sessionId, runId: { not: req.runId } },
orderBy: { createdAt: "asc" },
select: { role: true, content: true },
take: MAX_HISTORY_TURNS * 2, // user+assistant per turn
});
if (messages.length === 0) return prompt;
const turns: string[] = [];
for (const message of messages) {
const label = message.role === "assistant" ? "Assistant" : "User";
turns.push(`${label}: ${message.content}`);
}
return `<session_history>\nThis is the prior conversation in this session, replayed because the provider session could not be resumed. Treat these as earlier turns you produced or received.\n\n${turns.join("\n\n")}\n</session_history>\n\n${prompt}`;
} catch {
return prompt;
}
}
async function persistAgentMessage(req: RunRequest, role: string, content: string): Promise<void> {
if (content === "") return;
try {