feat: add scoped history and artifacts
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { constants } from "node:fs";
|
||||
import { open } from "node:fs/promises";
|
||||
import type { Router } from "express";
|
||||
import { getLocalAgentContext } from "../context/localContext.js";
|
||||
import { artifactFilename, openArtifactDirectory } from "../mcp/artifact-files.js";
|
||||
import type { SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||
|
||||
export function registerArtifactRoutes(router: Router, options: {
|
||||
sessionMetadataStore: SessionMetadataStore;
|
||||
runtime: Pick<OpencodeRuntimeAdapter, "ensureSessionWorkspace">;
|
||||
}) {
|
||||
router.get("/session/:session_id/files/:filename", async (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
if (!artifactFilename.test(filename)) { res.status(404).end(); return; }
|
||||
try {
|
||||
const record = await options.sessionMetadataStore.get(getLocalAgentContext(req), req.params.session_id);
|
||||
if (!record) { res.status(404).end(); return; }
|
||||
const workspace = await options.runtime.ensureSessionWorkspace(record.sessionId, record);
|
||||
const directory = await openArtifactDirectory(workspace, record.sessionId);
|
||||
try {
|
||||
const file = await open(`/proc/self/fd/${directory.fd}/${filename}`, constants.O_RDONLY | constants.O_NOFOLLOW);
|
||||
try {
|
||||
const stat = await file.stat();
|
||||
if (!stat.isFile() || stat.size > 5 * 1024 * 1024) { res.status(404).end(); return; }
|
||||
res.setHeader("Content-Type", filename.endsWith(".zip") ? "application/zip" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
res.setHeader("Cache-Control", "private, no-store");
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.send(await file.readFile());
|
||||
} finally { await file.close(); }
|
||||
} finally { await directory.close(); }
|
||||
} catch { res.status(404).end(); }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { getLocalAgentContext } from "../context/localContext.js";
|
||||
import { readPurpose, type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
|
||||
const nameSchema = z.object({ name: z.string().trim().min(1).max(80) }).strict();
|
||||
const membershipSchema = z.object({ topic_id: z.string().uuid().nullable() }).strict();
|
||||
|
||||
export function registerChatTopicRoutes(router: Router, sessions: SessionMetadataStore) {
|
||||
router.get("/topics", async (req, res) => {
|
||||
res.json({ topics: await sessions.topics.list(getLocalAgentContext(req)) });
|
||||
});
|
||||
router.post("/topics", async (req, res) => {
|
||||
const parsed = nameSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ message: "主题名称须为 1 至 80 个字符" }); return; }
|
||||
res.status(201).json(await sessions.topics.create(getLocalAgentContext(req), parsed.data.name));
|
||||
});
|
||||
router.patch("/topics/:topic_id", async (req, res) => {
|
||||
const parsed = nameSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ message: "主题名称须为 1 至 80 个字符" }); return; }
|
||||
const topic = await sessions.topics.rename(getLocalAgentContext(req), req.params.topic_id, parsed.data.name);
|
||||
if (!topic) { res.status(404).json({ message: "主题不存在" }); return; }
|
||||
res.json(topic);
|
||||
});
|
||||
router.delete("/topics/:topic_id", async (req, res) => {
|
||||
// Removing a grouping never deletes conversations. Old IDs resolve as unclassified.
|
||||
const removed = await sessions.topics.remove(getLocalAgentContext(req), req.params.topic_id);
|
||||
if (!removed) { res.status(404).json({ message: "主题不存在" }); return; }
|
||||
res.status(204).end();
|
||||
});
|
||||
router.patch("/session/:session_id/topic", async (req, res) => {
|
||||
const parsed = membershipSchema.safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ message: "invalid topic membership" }); return; }
|
||||
const context = getLocalAgentContext(req);
|
||||
const record = await sessions.get(context, req.params.session_id);
|
||||
if (!record || readPurpose(record) === "task_execution") { res.status(404).json({ message: "session not found" }); return; }
|
||||
if (parsed.data.topic_id && !await sessions.topics.get(context, parsed.data.topic_id)) {
|
||||
res.status(404).json({ message: "主题不存在" }); return;
|
||||
}
|
||||
await sessions.touch(record, { topicId: parsed.data.topic_id });
|
||||
res.json({ session_id: record.sessionId, topic_id: parsed.data.topic_id });
|
||||
});
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { registerResponseInputRoutes } from "./response-input-routes.js";
|
||||
import { registerMcpStatusRoutes } from "./mcpRoutes.js";
|
||||
import { registerChatSuggestionRoutes } from "./chatSuggestionRoutes.js";
|
||||
import { registerAgentMessageRoutes } from "./agent-message-routes.js";
|
||||
import { registerArtifactRoutes } from "./artifact-routes.js";
|
||||
import type { AgentMessageCoordinator } from "../chat/agent-message-coordinator.js";
|
||||
|
||||
export type BuildChatRouterOptions = {
|
||||
@@ -76,6 +77,7 @@ export const buildChatRouter = (options: BuildChatRouterOptions) => {
|
||||
models: agentModelOptions,
|
||||
});
|
||||
});
|
||||
registerArtifactRoutes(chatRouter, { sessionMetadataStore, runtime });
|
||||
|
||||
chatRouter.get("/ui-registry", (_req, res) => {
|
||||
res.json(uiRegistryResponse);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { registerChatTopicRoutes } from "./chat-topic-routes.js";
|
||||
import { matchHistoryText } from "../sessions/history-search.js";
|
||||
import { credentialRefreshCoordinator } from "../product/credential-refresh.js";
|
||||
import { type Router } from "express";
|
||||
import { z } from "zod";
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
} from "../sessions/metadataStore.js";
|
||||
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
|
||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||
import { HistoryCursorError, historyScope, sessionHistoryPage, sessionMessagePage } from "../sessions/history-page.js";
|
||||
import { type LearningOrchestrator } from "../learning/orchestrator.js";
|
||||
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
||||
import { type ChatRunState } from "./chatRunState.js";
|
||||
@@ -38,6 +41,7 @@ const createSessionPayloadSchema = z.object({
|
||||
session_id: z.string().max(128).optional(),
|
||||
parent_session_id: z.string().max(128).optional(),
|
||||
trigger_context: agentTriggerContextSchema.optional(),
|
||||
topic_id: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
const taskExecutionPayloadSchema = z.object({
|
||||
@@ -50,6 +54,19 @@ const taskExecutionPayloadSchema = z.object({
|
||||
const sessionListQuerySchema = z.object({
|
||||
purpose: z.enum(["ordinary_chat", "task_execution", "task_collaboration"]).optional(),
|
||||
task_id: z.string().uuid().optional(),
|
||||
include_collaboration: z.enum(["true", "false"]).optional(),
|
||||
q: z.string().trim().max(200).optional(),
|
||||
topic_id: z.union([z.string().uuid(), z.literal("unclassified")]).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
cursor: z.string().max(2048).optional(),
|
||||
});
|
||||
|
||||
const messageQuerySchema = z.object({
|
||||
message_limit: z.coerce.number().int().min(1).max(100).optional(),
|
||||
});
|
||||
const olderMessagesQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).default(40),
|
||||
before: z.string().max(2048).optional(),
|
||||
});
|
||||
|
||||
const forkPayloadSchema = z.object({
|
||||
@@ -91,6 +108,7 @@ export const registerChatSessionRoutes = (
|
||||
sessionTranscriptStore,
|
||||
sessionUiStateStore,
|
||||
} = options;
|
||||
registerChatTopicRoutes(chatRouter, sessionMetadataStore);
|
||||
const { activeRuns, lastRunStatuses } = runState;
|
||||
const getSessionRunStatus = runState.getStatus;
|
||||
|
||||
@@ -191,6 +209,9 @@ export const registerChatSessionRoutes = (
|
||||
const userId = authContext.userId;
|
||||
const actorKey = authContext.actorKey;
|
||||
const projectKey = authContext.projectKey;
|
||||
if (parsed.data.topic_id && !await sessionMetadataStore.topics.get(authContext, parsed.data.topic_id)) {
|
||||
res.status(404).json({ message: "主题不存在" }); return;
|
||||
}
|
||||
const trigger = parsed.data.trigger_context;
|
||||
if (trigger && !projectId) {
|
||||
res.status(403).json({ code: "PROJECT_CONTEXT_REQUIRED", message: "任务会话必须关联项目" });
|
||||
@@ -244,6 +265,7 @@ export const registerChatSessionRoutes = (
|
||||
const { record, created } = await sessionMetadataStore.ensure({
|
||||
actorKey,
|
||||
parentSessionId: parsed.data.parent_session_id,
|
||||
topicId: parsed.data.topic_id,
|
||||
projectId,
|
||||
projectKey,
|
||||
sessionId,
|
||||
@@ -267,6 +289,7 @@ export const registerChatSessionRoutes = (
|
||||
status: record.status,
|
||||
title: record.title,
|
||||
parent_session_id: record.parentSessionId,
|
||||
topic_id: record.topicId ?? null,
|
||||
created,
|
||||
event_duplicate: false,
|
||||
trigger_id: record.triggerId,
|
||||
@@ -460,19 +483,52 @@ export const registerChatSessionRoutes = (
|
||||
const userId = authContext.userId;
|
||||
const actorKey = authContext.actorKey;
|
||||
const projectKey = authContext.projectKey;
|
||||
const records = await sessionMetadataStore.list({
|
||||
const includeCollaboration = query.data.include_collaboration === "true";
|
||||
let records = await sessionMetadataStore.list({
|
||||
actorKey,
|
||||
projectId,
|
||||
projectKey,
|
||||
userId,
|
||||
}, {
|
||||
purpose: query.data.purpose ?? "ordinary_chat",
|
||||
purpose: includeCollaboration ? undefined : query.data.purpose ?? "ordinary_chat",
|
||||
taskId: query.data.task_id,
|
||||
});
|
||||
if (includeCollaboration) records = records.filter((record) => ["ordinary_chat", "task_collaboration"].includes(readPurpose(record)));
|
||||
const topics = new Map((await sessionMetadataStore.topics.list(authContext)).map((topic) => [topic.id, topic]));
|
||||
const topicId = query.data.topic_id;
|
||||
if (topicId && topicId !== "unclassified" && !topics.has(topicId)) {
|
||||
res.status(404).json({ message: "主题不存在" }); return;
|
||||
}
|
||||
records = records.filter((record) => !topicId || (topicId === "unclassified"
|
||||
? !record.topicId || !topics.has(record.topicId) : record.topicId === topicId));
|
||||
const searchable: Array<SessionRecord & { searchSnippet?: string }> = [];
|
||||
for (const record of records) {
|
||||
const titleMatches = (record.title ?? "新对话").toLocaleLowerCase().includes((query.data.q ?? "").toLocaleLowerCase());
|
||||
const state = query.data.q && !titleMatches ? await sessionUiStateStore.read(toSessionUiStateContext(record)) : null;
|
||||
const messages = activeRuns.get(record.sessionId)?.messages ?? state?.messages ?? [];
|
||||
searchable.push({ ...record, searchSnippet: query.data.q && !titleMatches ? matchHistoryText(messages, query.data.q) : undefined });
|
||||
}
|
||||
let page;
|
||||
try {
|
||||
page = sessionHistoryPage(searchable, {
|
||||
scope: historyScope([actorKey, projectKey, userId, query.data.purpose ?? "ordinary_chat", query.data.task_id, topicId, includeCollaboration]),
|
||||
q: query.data.q, limit: query.data.limit ?? (query.data.q || query.data.cursor ? 30 : Math.max(records.length, 1)),
|
||||
cursor: query.data.cursor,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof HistoryCursorError)) throw error;
|
||||
res.status(400).json({ message: error.message });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
sessions: records.map((record) => ({
|
||||
has_more: page.has_more,
|
||||
next_cursor: page.next_cursor,
|
||||
sessions: page.sessions.map((record) => ({
|
||||
id: record.sessionId,
|
||||
title: record.title ?? "新对话",
|
||||
topic_id: record.topicId && topics.has(record.topicId) ? record.topicId : null,
|
||||
topic_name: record.topicId ? topics.get(record.topicId)?.name : undefined,
|
||||
search_snippet: record.searchSnippet,
|
||||
created_at: record.createdAt,
|
||||
updated_at: record.updatedAt,
|
||||
status: record.status,
|
||||
@@ -490,7 +546,27 @@ export const registerChatSessionRoutes = (
|
||||
});
|
||||
});
|
||||
|
||||
chatRouter.get("/session/:session_id/messages", async (req, res) => {
|
||||
const query = olderMessagesQuerySchema.safeParse(req.query);
|
||||
if (!query.success) { res.status(400).json({ message: "invalid message pagination" }); return; }
|
||||
const context = getLocalAgentContext(req);
|
||||
const record = await sessionMetadataStore.get(context, req.params.session_id);
|
||||
if (!record) { res.status(404).json({ message: "session not found" }); return; }
|
||||
const state = await sessionUiStateStore.read(toSessionUiStateContext(record));
|
||||
const messages = readPurpose(record) === "task_collaboration" ? state?.messages ?? []
|
||||
: activeRuns.get(record.sessionId)?.messages ?? state?.messages ?? [];
|
||||
try {
|
||||
res.json(sessionMessagePage(messages, { scope: historyScope([record.actorKey, record.projectKey, record.sessionId]),
|
||||
...query.data }));
|
||||
} catch (error) {
|
||||
if (!(error instanceof HistoryCursorError)) throw error;
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
chatRouter.get("/session/:session_id", async (req, res) => {
|
||||
const query = messageQuerySchema.safeParse(req.query);
|
||||
if (!query.success) { res.status(400).json({ message: "invalid message pagination" }); return; }
|
||||
const sessionId = req.params.session_id?.trim();
|
||||
const authContext = getLocalAgentContext(req);
|
||||
const projectId = authContext.projectId;
|
||||
@@ -519,15 +595,21 @@ export const registerChatSessionRoutes = (
|
||||
const state = await sessionUiStateStore.read(
|
||||
toSessionUiStateContext(sessionRecord),
|
||||
);
|
||||
const messages = readPurpose(sessionRecord) === "task_collaboration" ? state?.messages ?? []
|
||||
: activeRuns.get(sessionRecord.sessionId)?.messages ?? state?.messages ?? [];
|
||||
const messagePayload = query.data.message_limit ? sessionMessagePage(messages, {
|
||||
scope: historyScope([sessionRecord.actorKey, sessionRecord.projectKey, sessionId]), limit: query.data.message_limit,
|
||||
}) : { messages };
|
||||
res.json({
|
||||
id: sessionRecord.sessionId,
|
||||
title: sessionRecord.title ?? "新对话",
|
||||
topic_id: sessionRecord.topicId && await sessionMetadataStore.topics.get(authContext, sessionRecord.topicId) ? sessionRecord.topicId : null,
|
||||
is_title_manually_edited: state?.isTitleManuallyEdited ?? false,
|
||||
created_at: sessionRecord.createdAt,
|
||||
updated_at: sessionRecord.updatedAt,
|
||||
status: sessionRecord.status,
|
||||
session_id: sessionRecord.sessionId,
|
||||
messages: state?.messages ?? [],
|
||||
...messagePayload,
|
||||
execution_plans: state?.executionPlans ?? [],
|
||||
parent_session_id: sessionRecord.parentSessionId,
|
||||
is_streaming: activeRuns.get(sessionRecord.sessionId)?.status === "running",
|
||||
@@ -538,6 +620,8 @@ export const registerChatSessionRoutes = (
|
||||
});
|
||||
|
||||
chatRouter.get("/session/:session_id/stream", async (req, res) => {
|
||||
const query = messageQuerySchema.safeParse(req.query);
|
||||
if (!query.success) { res.status(400).json({ message: "invalid message pagination" }); return; }
|
||||
const sessionId = req.params.session_id?.trim();
|
||||
const authContext = getLocalAgentContext(req);
|
||||
const projectId = authContext.projectId;
|
||||
@@ -567,12 +651,15 @@ export const registerChatSessionRoutes = (
|
||||
|
||||
const run = activeRuns.get(sessionRecord.sessionId);
|
||||
const state = await sessionUiStateStore.read(toSessionUiStateContext(sessionRecord));
|
||||
const messages = readPurpose(sessionRecord) === "task_collaboration" ? state?.messages ?? []
|
||||
: run?.messages ?? state?.messages ?? [];
|
||||
const messagePayload = query.data.message_limit ? sessionMessagePage(messages, {
|
||||
scope: historyScope([sessionRecord.actorKey, sessionRecord.projectKey, sessionId]), limit: query.data.message_limit,
|
||||
}) : { messages };
|
||||
res.write(
|
||||
toSse("state", {
|
||||
session_id: sessionRecord.sessionId,
|
||||
messages: readPurpose(sessionRecord) === "task_collaboration"
|
||||
? state?.messages ?? []
|
||||
: run?.messages ?? state?.messages ?? [],
|
||||
...messagePayload,
|
||||
execution_plans: run ? undefined : state?.executionPlans,
|
||||
is_streaming: run?.status === "running",
|
||||
run_status: getSessionRunStatus(sessionRecord.sessionId),
|
||||
@@ -740,6 +827,7 @@ export const registerChatSessionRoutes = (
|
||||
const { record: targetSessionRecord } = await sessionMetadataStore.ensure({
|
||||
actorKey,
|
||||
parentSessionId: sourceSessionId,
|
||||
topicId: sourceSessionRecord?.topicId ?? undefined,
|
||||
projectId,
|
||||
projectKey,
|
||||
sessionId: forkSession.id,
|
||||
|
||||
@@ -181,8 +181,8 @@ const toLegacyMessages = (messages: UIMessage[]) =>
|
||||
];
|
||||
});
|
||||
|
||||
const createFallbackUserUiMessage = (content: string): UIMessage => ({
|
||||
id: `user-${Date.now().toString(36)}`,
|
||||
const createFallbackUserUiMessage = (content: string, id?: string): UIMessage => ({
|
||||
id: id ?? `user-${randomUUID()}`,
|
||||
role: "user",
|
||||
parts: [{ type: "text", text: content }],
|
||||
});
|
||||
@@ -326,6 +326,9 @@ export const registerChatStreamRoute = (
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (input.tjwater.topic_id && !await sessionMetadataStore.topics.get(authContext, input.tjwater.topic_id)) {
|
||||
res.status(404).json({ message: "主题不存在" }); return;
|
||||
}
|
||||
const requestedSessionId = resolveRequestedStreamSessionId({
|
||||
sessionId: parsed.data.session_id,
|
||||
promptText,
|
||||
@@ -403,6 +406,7 @@ export const registerChatStreamRoute = (
|
||||
projectId,
|
||||
projectKey,
|
||||
sessionId: binding.sessionId,
|
||||
topicId: existingSessionRecord?.topicId ?? input.tjwater.topic_id,
|
||||
userId,
|
||||
});
|
||||
const activeSessionRecord = await sessionMetadataStore.touch(ensuredSessionRecord);
|
||||
@@ -454,6 +458,10 @@ export const registerChatStreamRoute = (
|
||||
toSessionUiStateContext(activeSessionRecord),
|
||||
);
|
||||
const persistedUiMessages = toUiMessages(initialSessionState?.messages);
|
||||
if (input.tjwater.client_message_id && persistedUiMessages.some((message) => message.id === input.tjwater.client_message_id)) {
|
||||
res.status(409).json({ message: "client_message_id already exists in this conversation" });
|
||||
return;
|
||||
}
|
||||
let executionPlanHistory = initialSessionState?.executionPlans ?? readExecutionPlans(initialSessionState?.messages ?? []);
|
||||
const visiblePersistedMessages = sessionPurpose === "task_collaboration"
|
||||
? toCollaborationMessages(persistedUiMessages)
|
||||
@@ -466,7 +474,7 @@ export const registerChatStreamRoute = (
|
||||
? visibleRequestMessages
|
||||
: [
|
||||
...visiblePersistedMessages,
|
||||
createFallbackUserUiMessage(promptText),
|
||||
createFallbackUserUiMessage(promptText, input.tjwater.client_message_id),
|
||||
];
|
||||
let latestUiMessages = requestMessages;
|
||||
const baseMessages = toLegacyMessages(requestMessages);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getRuntimeSessionContext } from "../runtime/sessionContext.js";
|
||||
import { type McpRuntimeReport, type McpSessionPolicy } from "../runtime/mcp-config.js";
|
||||
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import { allowedQueryTools } from "../runtime/query-mcp-policy.js";
|
||||
import { PRODUCT_MCP_WRITE_TOOL_IDS } from "../runtime/product-mcp-tools.js";
|
||||
import type { QueryOwner } from "../product/mcp-query-client.js";
|
||||
|
||||
export function registerMcpPolicyBridge(app: Express, options: {
|
||||
@@ -24,8 +25,8 @@ export function registerMcpPolicyBridge(app: Express, options: {
|
||||
const grants = new Set(readonlyTools);
|
||||
const policy: McpSessionPolicy = {
|
||||
mode: context.learningMode === "review" ? "disabled" : !direct || context.purpose === "task_execution" ? "readonly" : "interactive",
|
||||
readonly_tools: readonlyTools,
|
||||
query_tools: allowedQueryTools(context, !direct).filter((name) => grants.has(name)),
|
||||
readonly_tools: readonlyTools.filter((name) => !PRODUCT_MCP_WRITE_TOOL_IDS.includes(name)),
|
||||
query_tools: allowedQueryTools(context, !direct).filter((name) => grants.has(name) || PRODUCT_MCP_WRITE_TOOL_IDS.includes(name)),
|
||||
};
|
||||
res.json(policy);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ const update = z.object({ delivery: z.enum(["steer", "queue"]) }).strict();
|
||||
export function registerResponseInputRoutes(router: Router, options: {
|
||||
runtime: OpencodeRuntimeAdapter; sessionMetadataStore: SessionMetadataStore; runState: ChatRunState;
|
||||
}) {
|
||||
for (const method of ["get", "patch"] as const) {
|
||||
for (const method of ["get", "patch", "delete"] as const) {
|
||||
router[method](method === "get" ? "/session/:session_id/inputs" : "/session/:session_id/inputs/:input_id", async (req, res) => {
|
||||
const sessionId = String(req.params.session_id);
|
||||
try {
|
||||
@@ -22,6 +22,12 @@ export function registerResponseInputRoutes(router: Router, options: {
|
||||
return;
|
||||
}
|
||||
const inputId = id.parse((req.params as Record<string, string>).input_id);
|
||||
if (method === "delete") {
|
||||
const input = await options.runtime.cancelPendingUserInput(sessionId, inputId);
|
||||
options.runState.activeRuns.get(sessionId)?.inputs?.observe("session.inbox.cancelled", { inboxID: inputId });
|
||||
res.json(input);
|
||||
return;
|
||||
}
|
||||
const { delivery } = update.parse(req.body);
|
||||
const input = await options.runtime.updatePendingUserInput(sessionId, inputId, delivery);
|
||||
options.runState.activeRuns.get(sessionId)?.inputs?.observe("session.inbox.delivery.changed", { inboxID: inputId, delivery });
|
||||
|
||||
Reference in New Issue
Block a user