重构会话管理,简化上下文存储逻辑

This commit is contained in:
2026-06-03 17:14:55 +08:00
parent 76d4b510f4
commit 04ded0ceb0
19 changed files with 420 additions and 235 deletions
+51 -66
View File
@@ -2,10 +2,7 @@ import { randomUUID } from "node:crypto";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import {
buildToolSessionScopeKey,
ToolSessionContextStore,
} from "../session/toolContextStore.js";
import { ToolSessionContextStore } from "../session/toolContextStore.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
export type SessionBinding = {
@@ -28,9 +25,6 @@ export type ChatRequestContext = SessionContext & {
};
export class ChatSessionBridge {
// runtime session 仅在单次请求生命周期内有效;线程连续性由 clientSessionId 对应的持久状态承担。
private readonly activeRuntimeSessions = new Map<string, string>();
private readonly activeSensitiveContexts = new Map<string, ChatRequestContext>();
private readonly abortControllers = new Map<string, AbortController>();
private readonly toolContextStore = new ToolSessionContextStore();
@@ -38,6 +32,7 @@ export class ChatSessionBridge {
async resolve(context: {
clientSessionId?: string;
sessionId?: string;
accessToken?: string;
projectId?: string;
traceId?: string;
@@ -48,70 +43,63 @@ export class ChatSessionBridge {
created: boolean;
}> {
const requestContext = this.buildRequestContext(context);
await this.abortActiveRuntime(requestContext.clientSessionId);
const existingSessionId = context.sessionId?.trim();
await this.abortActiveRuntime(requestContext.clientSessionId, existingSessionId);
const session = await this.runtime.createSession(requestContext.clientSessionId);
let sessionId = existingSessionId;
let created = false;
if (!sessionId) {
const session = await this.runtime.createSession(requestContext.clientSessionId);
sessionId = session.id;
created = true;
}
const binding: SessionBinding = {
clientSessionId: requestContext.clientSessionId,
sessionId: session.id,
sessionId,
startedAt: Date.now(),
};
const sessionScopeKey = buildToolSessionScopeKey(
requestContext.actorKey,
requestContext.projectKey,
requestContext.clientSessionId,
);
this.activeRuntimeSessions.set(requestContext.clientSessionId, session.id);
this.activeSensitiveContexts.set(sessionScopeKey, requestContext);
await this.toolContextStore.write({
accessToken: requestContext.accessToken,
actorKey: requestContext.actorKey,
allowLearningWrite: true,
clientSessionId: requestContext.clientSessionId,
learningMode: "interactive",
projectId: requestContext.projectId,
projectKey: requestContext.projectKey,
sessionId: session.id,
sessionScopeKey,
sessionId,
traceId: requestContext.traceId,
});
return { binding, requestContext, created: true };
return { binding, requestContext, created };
}
count(): number {
return this.activeRuntimeSessions.size;
return this.abortControllers.size;
}
createClientSessionId() {
return `agent-${randomUUID().slice(0, 12)}`;
}
getActiveSensitiveContext(sessionScopeKey: string) {
return this.activeSensitiveContexts.get(sessionScopeKey) ?? null;
}
registerAbortController(clientSessionId: string, controller: AbortController) {
this.abortControllers.set(clientSessionId, controller);
}
deleteAbortController(clientSessionId: string) {
finalizeRequest(clientSessionId: string) {
this.abortControllers.delete(clientSessionId);
}
async abort(context: {
clientSessionId?: string;
sessionId?: string;
}): Promise<SessionBinding | null> {
const clientSessionId = context.clientSessionId?.trim();
if (!clientSessionId) {
const sessionId = context.sessionId?.trim();
if (!clientSessionId || !sessionId) {
return null;
}
const sessionId = this.activeRuntimeSessions.get(clientSessionId);
if (!sessionId) {
return null;
}
await this.abortActiveRuntime(clientSessionId);
await this.abortActiveRuntime(clientSessionId, sessionId);
return {
clientSessionId,
sessionId,
@@ -119,18 +107,32 @@ export class ChatSessionBridge {
};
}
async releaseRuntimeSession(clientSessionId: string, sessionId: string) {
const activeSessionId = this.activeRuntimeSessions.get(clientSessionId);
if (activeSessionId === sessionId) {
this.activeRuntimeSessions.delete(clientSessionId);
async deleteConversationSession(context: {
clientSessionId: string;
sessionId: string;
}) {
const clientSessionId = context.clientSessionId.trim();
const sessionId = context.sessionId.trim();
const controller = this.abortControllers.get(clientSessionId);
if (controller) {
this.abortControllers.delete(clientSessionId);
controller.abort();
}
this.activeSensitiveContexts.delete(findScopeKey(this.activeSensitiveContexts, clientSessionId));
await this.runtime.abortSession(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed to abort conversation runtime session",
);
});
await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed while waiting for conversation runtime session to become idle",
);
});
await this.toolContextStore.remove(sessionId).catch((error) => {
logger.debug({ sessionId, err: error }, "failed to cleanup runtime tool context");
});
await this.runtime.abortSession(sessionId).catch((error) => {
logger.debug({ sessionId, err: error }, "failed to cleanup runtime session");
});
}
private buildRequestContext(context: {
@@ -151,44 +153,27 @@ export class ChatSessionBridge {
};
}
private async abortActiveRuntime(clientSessionId: string) {
const activeSessionId = this.activeRuntimeSessions.get(clientSessionId);
this.activeRuntimeSessions.delete(clientSessionId);
this.activeSensitiveContexts.delete(findScopeKey(this.activeSensitiveContexts, clientSessionId));
private async abortActiveRuntime(clientSessionId: string, sessionId?: string) {
const controller = this.abortControllers.get(clientSessionId);
if (controller) {
this.abortControllers.delete(clientSessionId);
controller.abort();
}
if (!activeSessionId) {
if (!sessionId) {
return;
}
await this.toolContextStore.remove(activeSessionId).catch(() => undefined);
await this.runtime.abortSession(activeSessionId).catch((error) => {
await this.runtime.abortSession(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId: activeSessionId, err: error },
"failed to abort previous active runtime session",
{ clientSessionId, sessionId, err: error },
"failed to abort active runtime session",
);
});
await this.runtime.waitForSessionIdle(activeSessionId).catch((error) => {
await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId: activeSessionId, err: error },
"failed while waiting for previous runtime session to become idle",
{ clientSessionId, sessionId, err: error },
"failed while waiting for active runtime session to become idle",
);
});
}
}
const findScopeKey = (
contexts: Map<string, ChatRequestContext>,
clientSessionId: string,
) => {
for (const [scopeKey, context] of contexts.entries()) {
if (context.clientSessionId === clientSessionId) {
return scopeKey;
}
}
return clientSessionId;
};
+22 -8
View File
@@ -6,6 +6,7 @@ import {
ensureDirectory,
readJsonFile,
removeFileIfExists,
toConversationScopeKey,
} from "../utils/fileStore.js";
export type ConversationStateRecord = {
@@ -15,6 +16,12 @@ export type ConversationStateRecord = {
branchGroups: unknown[];
};
type ConversationStateContext = {
actorKey: string;
projectKey: string;
sessionId: string;
};
export class ConversationStateStore {
constructor(private readonly baseDir = config.CONVERSATION_STATE_STORAGE_DIR) {}
@@ -22,20 +29,27 @@ export class ConversationStateStore {
await ensureDirectory(this.baseDir);
}
async read(sessionScopeKey: string) {
return await readJsonFile<ConversationStateRecord>(this.filePath(sessionScopeKey));
async read(context: ConversationStateContext) {
return await readJsonFile<ConversationStateRecord>(this.filePath(context));
}
async write(sessionScopeKey: string, state: ConversationStateRecord) {
await atomicWriteJson(this.filePath(sessionScopeKey), state);
async write(context: ConversationStateContext, state: ConversationStateRecord) {
await atomicWriteJson(this.filePath(context), state);
return state;
}
async remove(sessionScopeKey: string) {
await removeFileIfExists(this.filePath(sessionScopeKey));
async remove(context: ConversationStateContext) {
await removeFileIfExists(this.filePath(context));
}
private filePath(sessionScopeKey: string) {
return join(this.baseDir, `${sessionScopeKey}.json`);
private filePath(context: ConversationStateContext) {
return join(
this.baseDir,
`${toConversationScopeKey(
context.actorKey,
context.projectKey,
context.sessionId,
)}.json`,
);
}
}
+31 -18
View File
@@ -15,11 +15,11 @@ export type ConversationStatus = "active" | "archived";
export type ConversationRecord = {
sessionId: string;
sessionScopeKey: string;
actorKey: string;
ownerUserId?: string;
projectId?: string;
projectKey: string;
opencodeSessionId?: string;
parentSessionId?: string;
createdAt: string;
updatedAt: string;
@@ -48,12 +48,9 @@ export class ConversationStore {
async ensure(input: EnsureConversationInput) {
const sessionId = normalizeSessionId(input.sessionId) ?? createConversationSessionId();
const sessionScopeKey = toConversationScopeKey(
input.actorKey,
input.projectKey,
sessionId,
const existing = await readJsonFile<ConversationRecord>(
this.filePath(input.actorKey, input.projectKey, sessionId),
);
const existing = await readJsonFile<ConversationRecord>(this.filePath(sessionScopeKey));
if (existing) {
return { created: false, record: existing };
}
@@ -61,7 +58,6 @@ export class ConversationStore {
const now = new Date().toISOString();
const record: ConversationRecord = {
sessionId,
sessionScopeKey,
actorKey: input.actorKey,
ownerUserId: input.userId?.trim(),
projectId: input.projectId,
@@ -71,7 +67,10 @@ export class ConversationStore {
updatedAt: now,
status: "active",
};
await atomicWriteJson(this.filePath(sessionScopeKey), record);
await atomicWriteJson(
this.filePath(record.actorKey, record.projectKey, record.sessionId),
record,
);
return { created: true, record };
}
@@ -81,22 +80,23 @@ export class ConversationStore {
return null;
}
return await readJsonFile<ConversationRecord>(
this.filePath(
toConversationScopeKey(context.actorKey, context.projectKey, normalizedSessionId),
),
this.filePath(context.actorKey, context.projectKey, normalizedSessionId),
);
}
async touch(
record: ConversationRecord,
updates: Partial<Pick<ConversationRecord, "title" | "status">> = {},
updates: Partial<Pick<ConversationRecord, "title" | "status" | "opencodeSessionId">> = {},
) {
const next: ConversationRecord = {
...record,
...normalizeConversationUpdates(updates),
updatedAt: new Date().toISOString(),
};
await atomicWriteJson(this.filePath(record.sessionScopeKey), next);
await atomicWriteJson(
this.filePath(record.actorKey, record.projectKey, record.sessionId),
next,
);
return next;
}
@@ -116,11 +116,16 @@ export class ConversationStore {
}
async remove(record: ConversationRecord) {
await removeFileIfExists(this.filePath(record.sessionScopeKey));
await removeFileIfExists(
this.filePath(record.actorKey, record.projectKey, record.sessionId),
);
}
private filePath(sessionScopeKey: string) {
return join(this.baseDir, `${sessionScopeKey}.json`);
private filePath(actorKey: string, projectKey: string, sessionId: string) {
return join(
this.baseDir,
`${toConversationScopeKey(actorKey, projectKey, sessionId)}.json`,
);
}
}
@@ -132,9 +137,11 @@ const normalizeSessionId = (value?: string) => {
};
const normalizeConversationUpdates = (
updates: Partial<Pick<ConversationRecord, "title" | "status">>,
updates: Partial<Pick<ConversationRecord, "title" | "status" | "opencodeSessionId">>,
) => {
const normalized: Partial<Pick<ConversationRecord, "title" | "status">> = {};
const normalized: Partial<
Pick<ConversationRecord, "title" | "status" | "opencodeSessionId">
> = {};
if (updates.status === "active" || updates.status === "archived") {
normalized.status = updates.status;
}
@@ -144,5 +151,11 @@ const normalizeConversationUpdates = (
normalized.title = trimmed.slice(0, 120);
}
}
if (typeof updates.opencodeSessionId === "string") {
const trimmed = updates.opencodeSessionId.trim();
if (trimmed) {
normalized.opencodeSessionId = trimmed.slice(0, 256);
}
}
return normalized;
};
+1 -14
View File
@@ -9,10 +9,7 @@ import { LearningStateStore } from "./stateStore.js";
import { MemoryStore, type MemoryScope } from "../memory/store.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { SkillStore } from "../skills/store.js";
import {
buildToolSessionScopeKey,
ToolSessionContextStore,
} from "../session/toolContextStore.js";
import { ToolSessionContextStore } from "../session/toolContextStore.js";
import {
sanitizePersistentDocument,
sanitizePersistentLine,
@@ -153,11 +150,6 @@ export class LearningOrchestrator {
projectId: input.requestContext.projectId,
projectKey: input.requestContext.projectKey,
sessionId: gateSession.id,
sessionScopeKey: buildToolSessionScopeKey(
input.requestContext.actorKey,
input.requestContext.projectKey,
input.requestContext.clientSessionId,
),
traceId: input.requestContext.traceId,
});
await this.runtime.prompt(
@@ -247,11 +239,6 @@ export class LearningOrchestrator {
projectId: input.requestContext.projectId,
projectKey: input.requestContext.projectKey,
sessionId: reviewSession.id,
sessionScopeKey: buildToolSessionScopeKey(
input.requestContext.actorKey,
input.requestContext.projectKey,
input.requestContext.clientSessionId,
),
traceId: input.requestContext.traceId,
});
try {
+61 -16
View File
@@ -11,6 +11,7 @@ import { type ResultReferenceResolver } from "../results/resolver.js";
import { RESULT_REFERENCE_KIND } from "../results/store.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
import { type ConversationRecord } from "../conversations/store.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
import {
buildPromptWithLearningContext,
@@ -51,6 +52,12 @@ const conversationStateSchema = z.object({
branch_groups: z.array(z.unknown()).default([]),
});
const toConversationStateContext = (conversation: ConversationRecord) => ({
actorKey: conversation.actorKey,
projectKey: conversation.projectKey,
sessionId: conversation.sessionId,
});
export const buildChatRouter = (
sessionBridge: ChatSessionBridge,
runtime: OpencodeRuntimeAdapter,
@@ -145,7 +152,9 @@ export const buildChatRouter = (
return;
}
const state = await conversationStateStore.read(conversation.sessionScopeKey);
const state = await conversationStateStore.read(
toConversationStateContext(conversation),
);
res.json({
id: conversation.sessionId,
title: conversation.title ?? "新对话",
@@ -190,7 +199,7 @@ export const buildChatRouter = (
const nextRecord = await conversationStore.touch(record, {
...(parsed.data.title ? { title: parsed.data.title } : {}),
});
await conversationStateStore.write(nextRecord.sessionScopeKey, {
await conversationStateStore.write(toConversationStateContext(nextRecord), {
sessionId: nextRecord.sessionId,
isTitleManuallyEdited: parsed.data.is_title_manually_edited,
messages: parsed.data.messages,
@@ -231,13 +240,18 @@ export const buildChatRouter = (
return;
}
const nextConversation = await conversationStore.touch(conversation, { title });
const state = await conversationStateStore.read(nextConversation.sessionScopeKey);
const state = await conversationStateStore.read(
toConversationStateContext(nextConversation),
);
if (state) {
await conversationStateStore.write(nextConversation.sessionScopeKey, {
await conversationStateStore.write(
toConversationStateContext(nextConversation),
{
...state,
isTitleManuallyEdited:
isTitleManuallyEdited ?? state.isTitleManuallyEdited,
});
},
);
}
res.json({
id: nextConversation.sessionId,
@@ -264,7 +278,13 @@ export const buildChatRouter = (
res.status(204).end();
return;
}
await conversationStateStore.remove(conversation.sessionScopeKey);
await conversationStateStore.remove(toConversationStateContext(conversation));
if (conversation.opencodeSessionId) {
await sessionBridge.deleteConversationSession({
clientSessionId: conversation.sessionId,
sessionId: conversation.opencodeSessionId,
});
}
await conversationStore.remove(conversation);
res.status(204).end();
});
@@ -323,9 +343,20 @@ export const buildChatRouter = (
}
try {
const binding = await sessionBridge.abort({
clientSessionId: parsed.data.session_id,
});
const projectId = req.header("x-project-id") ?? undefined;
const userId = req.header("x-user-id") ?? undefined;
const actorKey = toActorKey(userId);
const projectKey = toProjectKey(projectId);
const conversation = await conversationStore.get(
{ actorKey, projectId, projectKey, userId },
parsed.data.session_id,
);
const binding = conversation?.opencodeSessionId
? await sessionBridge.abort({
clientSessionId: conversation.sessionId,
sessionId: conversation.opencodeSessionId,
})
: null;
if (!binding) {
res.status(204).end();
@@ -467,14 +498,22 @@ export const buildChatRouter = (
userId,
});
const activeConversation = await conversationStore.touch(conversation);
const hadExistingRuntimeSession = Boolean(activeConversation.opencodeSessionId);
const { binding, requestContext, created } = await sessionBridge.resolve({
clientSessionId: activeConversation.sessionId,
sessionId: activeConversation.opencodeSessionId,
accessToken,
projectId,
traceId,
userId,
});
const conversationWithRuntime =
created && binding.sessionId !== activeConversation.opencodeSessionId
? await conversationStore.touch(activeConversation, {
opencodeSessionId: binding.sessionId,
})
: activeConversation;
const historyContext = {
actorKey: requestContext.actorKey,
clientSessionId: requestContext.clientSessionId,
@@ -482,6 +521,9 @@ export const buildChatRouter = (
sessionId: requestContext.clientSessionId,
};
const recentTurns = await sessionHistoryStore.getRecentTurns(historyContext, 8);
const initialConversationState = await conversationStateStore.read(
toConversationStateContext(conversationWithRuntime),
);
logger.info(
{
@@ -521,8 +563,12 @@ export const buildChatRouter = (
memoryStore,
requestContext.actorKey,
requestContext.projectKey,
recentTurns,
parsed.data.message,
{
recentTurns,
persistedMessages: initialConversationState?.messages,
message: parsed.data.message,
restoreConversation: !hadExistingRuntimeSession,
},
);
const streamResult = await streamPromptResponse({
runtime,
@@ -550,10 +596,10 @@ export const buildChatRouter = (
const latestConversation =
(await conversationStore.get(
{ actorKey, projectId, projectKey, userId },
activeConversation.sessionId,
)) ?? activeConversation;
conversationWithRuntime.sessionId,
)) ?? conversationWithRuntime;
const latestConversationState = await conversationStateStore.read(
latestConversation.sessionScopeKey,
toConversationStateContext(latestConversation),
);
const existingSessionTitle = latestConversation.title;
let sessionTitle = existingSessionTitle;
@@ -606,8 +652,7 @@ export const buildChatRouter = (
}
}
} finally {
await sessionBridge.releaseRuntimeSession(clientSessionId, binding.sessionId);
sessionBridge.deleteAbortController(clientSessionId);
sessionBridge.finalizeRequest(clientSessionId);
streamClosed = true;
req.off("close", handleClientClose);
res.off("close", handleClientClose);
+65 -5
View File
@@ -192,15 +192,22 @@ export const buildPromptWithLearningContext = async (
memoryStore: MemoryStore,
actorKey: string,
projectKey: string,
recentTurns: SessionTurnRecord[],
message: string,
options: {
recentTurns: SessionTurnRecord[];
persistedMessages?: unknown[];
message: string;
restoreConversation?: boolean;
},
) => {
const snapshot = await memoryStore.buildPromptSnapshot({ actorKey, projectKey });
const restoredConversation = buildRestoredConversationContext(recentTurns);
const restoredConversation = options.restoreConversation === false
? ""
: buildRestoredConversationFromMessages(options.persistedMessages) ||
buildRestoredConversationContext(options.recentTurns);
if (!snapshot && !restoredConversation) {
return message;
return options.message;
}
return [snapshot, restoredConversation, `[Current user request]\n${message}`]
return [snapshot, restoredConversation, `[Current user request]\n${options.message}`]
.filter(Boolean)
.join("\n\n");
};
@@ -239,4 +246,57 @@ const compactMessage = (value: string) => {
return normalized.length > RESTORE_MESSAGE_CHAR_LIMIT
? `${normalized.slice(0, RESTORE_MESSAGE_CHAR_LIMIT - 3)}...`
: normalized;
};
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const isSyntheticAssistantError = (content: string) =>
/^⚠️\s*\*\*(请求已中断|错误[:]?)/.test(content);
const buildRestoredConversationFromMessages = (messages: unknown[] | undefined) => {
if (!Array.isArray(messages) || messages.length === 0) {
return "";
}
const formattedMessages = messages
.slice(-(RESTORE_TURN_LIMIT * 2 + 2))
.flatMap((message) => {
if (!isObjectRecord(message)) {
return [];
}
const role = message.role;
const content = message.content;
if ((role !== "user" && role !== "assistant") || typeof content !== "string") {
return [];
}
const normalizedContent = compactMessage(content);
if (!normalizedContent) {
return [];
}
if (role === "assistant" && isSyntheticAssistantError(normalizedContent)) {
return [];
}
return [`${role === "user" ? "用户" : "助手"}${normalizedContent}`];
});
if (formattedMessages.length === 0) {
return "";
}
const conversation = formattedMessages.join("\n");
const trimmedConversation =
conversation.length > RESTORE_CONTEXT_CHAR_LIMIT
? `${conversation.slice(0, RESTORE_CONTEXT_CHAR_LIMIT - 3)}...`
: conversation;
return [
"[Previous conversation context]",
"以下为当前前端对话线程中最近的历史对话,请延续其中已确认的目标、约束、结论与引用结果。",
trimmedConversation,
].join("\n");
};
+25 -43
View File
@@ -66,22 +66,13 @@ app.post("/internal/tools/dynamic-http-call", async (req, res) => {
return;
}
const sessionScopeKey =
typeof req.body?.sessionScopeKey === "string" ? req.body.sessionScopeKey : "";
const threadContext = await toolContextStore.read(sessionScopeKey);
const runtimeContext = sessionBridge.getActiveSensitiveContext(sessionScopeKey);
if (!threadContext && !runtimeContext) {
res.status(404).json({
message: "runtime or session context not found",
detail: sessionScopeKey,
});
return;
}
const context = runtimeContext ?? threadContext;
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const context = sessionId ? await toolContextStore.read(sessionId) : null;
if (!context) {
res.status(404).json({
message: "runtime or session context not found",
detail: sessionScopeKey,
message: "session context not found",
detail: sessionId,
});
return;
}
@@ -96,7 +87,7 @@ app.post("/internal/tools/dynamic-http-call", async (req, res) => {
arguments: req.body?.arguments,
},
{
accessToken: runtimeContext?.accessToken,
accessToken: context.accessToken,
actorKey: context.actorKey,
clientSessionId: context.clientSessionId,
projectId: context.projectId,
@@ -121,22 +112,13 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
return;
}
const sessionScopeKey =
typeof req.body?.sessionScopeKey === "string" ? req.body.sessionScopeKey : "";
const threadContext = await toolContextStore.read(sessionScopeKey);
const runtimeContext = sessionBridge.getActiveSensitiveContext(sessionScopeKey);
if (!threadContext && !runtimeContext) {
res.status(404).json({
message: "runtime or session context not found",
detail: sessionScopeKey,
});
return;
}
const context = runtimeContext ?? threadContext;
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const context = sessionId ? await toolContextStore.read(sessionId) : null;
if (!context) {
res.status(404).json({
message: "runtime or session context not found",
detail: sessionScopeKey,
message: "session context not found",
detail: sessionId,
});
return;
}
@@ -148,11 +130,11 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
}
const timeoutSec =
typeof req.body?.timeout === "number" && req.body.timeout > 0 ? req.body.timeout : 60;
typeof req.body?.timeout === "number" && req.body.timeout > 0 ? req.body.timeout : 120;
const authJson = JSON.stringify({
server: config.TJWATER_API_BASE_URL,
access_token: runtimeContext?.accessToken,
access_token: context.accessToken,
project_id: context.projectId,
network:"tjwater",
});
@@ -233,14 +215,14 @@ app.post("/internal/tools/fetch-result-ref", async (req, res) => {
return;
}
const sessionScopeKey =
typeof req.body?.sessionScopeKey === "string" ? req.body.sessionScopeKey : "";
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const resultRef = typeof req.body?.result_ref === "string" ? req.body.result_ref : "";
const context = await toolContextStore.read(sessionScopeKey);
const context = sessionId ? await toolContextStore.read(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
detail: sessionScopeKey,
detail: sessionId,
});
return;
}
@@ -276,14 +258,14 @@ app.post("/internal/tools/store-render-ref", async (req, res) => {
return;
}
const sessionScopeKey =
typeof req.body?.sessionScopeKey === "string" ? req.body.sessionScopeKey : "";
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
const context = await toolContextStore.read(sessionScopeKey);
const context = sessionId ? await toolContextStore.read(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
detail: sessionScopeKey,
detail: sessionId,
});
return;
}
@@ -326,14 +308,14 @@ app.post("/internal/tools/session-search", async (req, res) => {
return;
}
const sessionScopeKey =
typeof req.body?.sessionScopeKey === "string" ? req.body.sessionScopeKey : "";
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const query = typeof req.body?.query === "string" ? req.body.query : "";
const context = await toolContextStore.read(sessionScopeKey);
const context = sessionId ? await toolContextStore.read(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
detail: sessionScopeKey,
detail: sessionId,
});
return;
}
+1 -11
View File
@@ -7,9 +7,9 @@ import {
readJsonFile,
removeFileIfExists,
} from "../utils/fileStore.js";
import { toConversationScopeKey } from "../utils/fileStore.js";
export type ToolSessionContext = {
accessToken?: string;
actorKey: string;
allowLearningWrite?: boolean;
clientSessionId: string;
@@ -17,7 +17,6 @@ export type ToolSessionContext = {
projectId?: string;
projectKey: string;
sessionId: string;
sessionScopeKey: string;
traceId: string;
};
@@ -30,9 +29,6 @@ export class ToolSessionContextStore {
async write(context: ToolSessionContext) {
await atomicWriteJson(this.filePath(context.sessionId), context);
if (context.learningMode === "interactive" && context.sessionScopeKey) {
await atomicWriteJson(this.filePath(context.sessionScopeKey), context);
}
}
async read(sessionId: string) {
@@ -47,9 +43,3 @@ export class ToolSessionContextStore {
return join(this.baseDir, `${sessionId}.json`);
}
}
export const buildToolSessionScopeKey = (
actorKey: string,
projectKey: string,
clientSessionId: string,
) => toConversationScopeKey(actorKey, projectKey, clientSessionId);