fix: generate distinct task collaboration titles

Use verified task names until Agent generates a specific conversation title.
Repair legacy names without changing manual titles or history ordering.
Bound page repairs to 30 entries and a shared two-second deadline, and
re-read titles after task access validation to avoid stale reuse receipts.

Validation: bun run check; 40 focused tests across six files.
This commit is contained in:
2026-10-29 16:55:10 +08:00
parent c3050b602b
commit fbe273e90a
8 changed files with 285 additions and 12 deletions
+58 -9
View File
@@ -14,6 +14,7 @@ import {
import { type FrontendActionCoordinator } from "../frontendAction/coordinator.js";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import type { ProductCredential } from "../product/server-auth-client.js";
import {
type SessionMetadataStore,
type SessionRecord,
@@ -27,7 +28,7 @@ import { HistoryCursorError, historyScope, sessionHistoryPage, sessionMessagePag
import { type LearningOrchestrator } from "../learning/orchestrator.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
import { type ChatRunState } from "./chatRunState.js";
import { buildForkedSessionUiState } from "./chatSession.js";
import { buildForkedSessionUiState, isGenericTaskCollaborationTitle } from "./chatSession.js";
import { type StreamSubscriber } from "./chatUiState.js";
import { agentTriggerContextSchema } from "./agentTrigger.js";
import {
@@ -112,6 +113,36 @@ export const registerChatSessionRoutes = (
const { activeRuns, lastRunStatuses } = runState;
const getSessionRunStatus = runState.getStatus;
const refreshCollaborationTitle = async (record: SessionRecord, credential: ProductCredential | null, knownTaskTitle?: string, signal?: AbortSignal) => {
if (readPurpose(record) !== "task_collaboration" || !record.taskId
|| !isGenericTaskCollaborationTitle(record.title)) return record;
if ((!credential && !knownTaskTitle) || signal?.aborted) return record;
try {
const state = await sessionUiStateStore.read(toSessionUiStateContext(record));
if (state?.isTitleManuallyEdited) return record;
let taskTitle = knownTaskTitle;
if (!taskTitle && credential) {
const titleSessionId = `task-title-${crypto.randomUUID()}`;
bindOperationalTaskCredential(titleSessionId, credential);
try {
taskTitle = (await operationalTaskClient.getAgentContext(titleSessionId, record.taskId, undefined, signal)).task.title;
} finally {
releaseOperationalTaskCredential(titleSessionId);
clearOperationalTaskLease(titleSessionId);
}
}
if (!taskTitle?.trim()) return record;
const latestState = await sessionUiStateStore.read(toSessionUiStateContext(record));
if (latestState?.isTitleManuallyEdited) return record;
return await sessionMetadataStore.touch(record, { title: taskTitle }, {
preserveUpdatedAt: true, expectedTitle: record.title ?? "",
});
} catch (error) {
if (!signal?.aborted) logger.warn({ err: error, sessionId: record.sessionId }, "failed to refresh task collaboration title");
return record;
}
};
const startTaskExecution = (
record: SessionRecord,
credential: NonNullable<ReturnType<typeof getTrustedProductCredential>>,
@@ -321,10 +352,11 @@ export const registerChatSessionRoutes = (
taskId.data,
"task_collaboration",
);
const validationSessionId = existing?.sessionId ?? `task-access-${crypto.randomUUID()}`;
const validationSessionId = `task-access-${crypto.randomUUID()}`;
let taskTitle: string;
bindOperationalTaskCredential(validationSessionId, credential);
try {
await operationalTaskClient.getAgentContext(validationSessionId, taskId.data);
taskTitle = (await operationalTaskClient.getAgentContext(validationSessionId, taskId.data)).task.title;
} catch (error) {
res.status(error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : 502).json({
message: error instanceof Error ? error.message : "task access validation failed",
@@ -332,14 +364,19 @@ export const registerChatSessionRoutes = (
return;
} finally {
releaseOperationalTaskCredential(validationSessionId);
clearOperationalTaskLease(validationSessionId);
}
if (existing) {
const current = await sessionMetadataStore.get(metadataContext, existing.sessionId);
if (!current) { res.status(404).json({ message: "session not found" }); return; }
const titled = await refreshCollaborationTitle(current, credential, taskTitle);
res.json({
session_id: existing.sessionId,
purpose: readPurpose(existing),
task_id: existing.taskId,
session_id: titled.sessionId,
title: titled.title,
purpose: readPurpose(titled),
task_id: titled.taskId,
created: false,
status: existing.status,
status: titled.status,
});
return;
}
@@ -350,9 +387,10 @@ export const registerChatSessionRoutes = (
purpose: "task_collaboration",
taskId: taskId.data,
});
const titled = await sessionMetadataStore.touch(record, { title: "任务协作" });
const titled = await sessionMetadataStore.touch(record, { title: taskTitle });
res.status(201).json({
session_id: titled.sessionId,
title: titled.title,
purpose: readPurpose(titled),
task_id: titled.taskId,
created: true,
@@ -520,6 +558,15 @@ export const registerChatSessionRoutes = (
res.status(400).json({ message: error.message });
return;
}
const credential = getTrustedProductCredential(req);
const titleSignal = AbortSignal.timeout(2_000);
const titleLimit = Math.min(page.sessions.length, 30);
for (let offset = 0; offset < titleLimit && !titleSignal.aborted; offset += 4) {
const batch = await Promise.all(page.sessions.slice(offset, Math.min(offset + 4, titleLimit)).map(async (record) => ({
...record, ...await refreshCollaborationTitle(record, credential, undefined, titleSignal),
})));
page.sessions.splice(offset, batch.length, ...batch);
}
res.json({
has_more: page.has_more,
next_cursor: page.next_cursor,
@@ -578,7 +625,7 @@ export const registerChatSessionRoutes = (
return;
}
const sessionRecord = await sessionMetadataStore.get(
let sessionRecord = await sessionMetadataStore.get(
{
actorKey,
projectId,
@@ -592,6 +639,8 @@ export const registerChatSessionRoutes = (
return;
}
sessionRecord = await refreshCollaborationTitle(sessionRecord, getTrustedProductCredential(req));
const state = await sessionUiStateStore.read(
toSessionUiStateContext(sessionRecord),
);