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
+24 -11
View File
@@ -173,7 +173,7 @@ export class OrganizationAgentConfiguration {
where: { id: skill.id },
data: { disabledAt: new Date() },
});
await archiveRoleSessions(
await invalidateRoleSessionClaudeIds(
tx,
input.organizationId,
skill.roleBindings.map((binding) => binding.role.roleId),
@@ -271,7 +271,7 @@ export class OrganizationAgentConfiguration {
},
});
if (previous !== null && previous.contentDigest !== skill.contentDigest) {
await archiveRoleSessions(
await invalidateRoleSessionClaudeIds(
tx,
input.organizationId,
skill.roleBindings.map((binding) => binding.role.roleId),
@@ -379,7 +379,7 @@ export class OrganizationAgentConfiguration {
if (activeDefaultCount !== 1) {
throw new Error(`organization ${input.organizationId} must have exactly one active default role`);
}
if (executionSurfaceChanged) await archiveRoleSessions(tx, input.organizationId, [input.roleId]);
if (executionSurfaceChanged) await invalidateRoleSessionClaudeIds(tx, input.organizationId, [input.roleId]);
await tx.auditEntry.create({
data: {
organizationId: input.organizationId,
@@ -449,7 +449,7 @@ export class OrganizationAgentConfiguration {
})),
});
}
await archiveRoleSessions(tx, input.organizationId, [input.roleId]);
await invalidateRoleSessionClaudeIds(tx, input.organizationId, [input.roleId]);
await tx.auditEntry.create({
data: {
organizationId: input.organizationId,
@@ -472,7 +472,22 @@ export class OrganizationAgentConfiguration {
}
}
async function archiveRoleSessions(
/**
* Invalidate the provider session cursor (e.g. `claudeSessionId`) for every
* active session of the given roles, WITHOUT archiving the session.
*
* Execution-surface changes (role model/systemPrompt/tools, skill content or
* binding changes) make a stale provider session cursor unsafe to resume: the
* prior turns were produced under a different config. But the conversation
* history itself (AgentMessage rows) is still valuable and the logical Hub
* session should stay continuous — the next run re-seeds context from the
* transcript instead of resuming the old provider session. So we drop only
* the cursor, not the session.
*
* `userResumable` is cleared because the session is no longer backed by a
* live provider cursor the user can drop back into.
*/
async function invalidateRoleSessionClaudeIds(
tx: Prisma.TransactionClient,
organizationId: string,
roleIds: readonly string[],
@@ -482,20 +497,18 @@ async function archiveRoleSessions(
where: {
roleId: { in: [...new Set(roleIds)] },
project: { organizationId },
archivedAt: null,
},
select: { id: true, archivedAt: true, metadata: true },
select: { id: true, metadata: true },
});
const archivedAt = new Date();
for (const session of sessions) {
const metadata = typeof session.metadata === "object" && session.metadata !== null && !Array.isArray(session.metadata)
? session.metadata as Prisma.JsonObject
: {};
const { claudeSessionId: _drop, ...rest } = metadata;
await tx.agentSession.update({
where: { id: session.id },
data: {
...(session.archivedAt === null ? { archivedAt } : {}),
metadata: { ...metadata, userResumable: false },
},
data: { metadata: { ...rest, userResumable: false } },
});
}
}