From 85858b93b430ceec4ec8c4c4713dadb3a51c4172 Mon Sep 17 00:00:00 2001 From: Fodesu Date: Mon, 7 Sep 2026 14:53:00 +0800 Subject: [PATCH 1/7] feat(queue): add live steer and follow-up session input queues Steer and follow-up queues for a session's active run, kept in the configured session runtime backend (memory or Redis) rather than PostgreSQL. - Separate steer and follow-up Go types, runtime methods, and Redis keys, with accepted/claimed/applied/canceled lifecycle, reorder, edit, cancel, promote, and invocation replay. - Steer claims are fenced to the active run's owner, generation, and fencing token and consumed at the step commit barrier; a final step that finds no steer seals the run. - Follow-ups are claimed at the run's terminal boundary and started through ordinary turn admission. - HTTP queue routes, channel /steer and /queue slash commands, split-runtime RPC, web queue panel, and live steer projection in the session runtime. Squashed from the PostgreSQL-backed iterations of this branch. Co-Authored-By: Claude Fable 5.1 --- .../src/composables/api/useChat.chat-api.ts | 110 ++- apps/web/src/composables/api/useChat.types.ts | 19 + apps/web/src/i18n/locales/en.json | 21 + apps/web/src/i18n/locales/ja.json | 21 + apps/web/src/i18n/locales/zh.json | 21 + .../src/pages/home/components/chat-pane.vue | 97 +- .../session-follow-up-queue-item.test.ts | 79 ++ .../session-follow-up-queue-item.vue | 79 ++ .../components/session-follow-up-queue.vue | 96 ++ .../session-queue-submission.test.ts | 53 ++ .../components/session-queue-submission.ts | 57 ++ .../use-session-follow-up-queue.test.ts | 134 +++ .../components/use-session-follow-up-queue.ts | 202 +++++ .../home/composables/useChatScroll.test.ts | 70 ++ .../pages/home/composables/useChatScroll.ts | 39 +- .../home/composables/useQueueTurnAnchors.ts | 57 ++ .../web/src/store/chat/runtime-client.test.ts | 137 ++- apps/web/src/store/chat/runtime-client.ts | 45 +- .../src/store/chat/runtime-projection.test.ts | 116 +++ apps/web/src/store/chat/runtime-projection.ts | 142 ++- .../store/chat/runtime-transcript-merge.ts | 55 ++ .../src/store/chat/transcript-race.test.ts | 37 + apps/web/src/store/chat/transcript.test.ts | 107 +++ apps/web/src/store/chat/transcript.ts | 55 +- apps/web/src/store/chat/types.test.ts | 32 + apps/web/src/store/chat/types.ts | 35 + cmd/agent/http_providers.go | 4 + cmd/agent/module.go | 1 + cmd/agent/rpc.go | 5 +- cmd/internal/channel/module.go | 7 + cmd/internal/channel/providers.go | 9 +- db/postgres/queries/user_input.sql | 30 + docs/design/session-input-queues.md | 51 ++ .../agent/adapter/channelqueue/adapter.go | 66 ++ internal/agent/application/contract.go | 24 +- .../agent/application/queue_continuation.go | 100 ++ .../agent/application/queue_step_binding.go | 71 ++ .../application/queue_step_transaction.go | 190 ++++ .../agent/application/runtime_decision.go | 28 +- ...me_decision_continuation_lifecycle_test.go | 4 + .../application/runtime_decision_test.go | 24 + internal/agent/application/service.go | 7 +- .../agent/application/service_retry_edit.go | 24 + internal/agent/application/service_stream.go | 79 +- .../application/service_tool_approval.go | 52 +- internal/agent/application/service_trigger.go | 1 + .../agent/application/service_user_input.go | 68 +- internal/agent/application/session_queue.go | 108 +++ internal/agent/application/step_commit.go | 150 ++- .../agent/application/step_commit_test.go | 83 -- .../agent/application/step_persister_test.go | 37 + internal/agent/application/turn_admission.go | 47 +- internal/agent/application/turn_service.go | 24 + .../agent/decision/input/interaction_test.go | 57 +- internal/agent/decision/input/service.go | 41 + .../decision/input/service_store_test.go | 10 + internal/agent/event/event.go | 6 + internal/agent/runtime/native/agent.go | 54 +- internal/agent/runtime/native/stream.go | 1 + internal/agent/runtime/native/stream_test.go | 68 +- internal/agent/runtime/native/types.go | 10 + internal/agent/runtime/session/commands.go | 11 +- internal/agent/runtime/session/live_queue.go | 338 +++++++ .../runtime/session/live_queue_manager.go | 142 +++ .../runtime/session/live_queue_redis_test.go | 145 +++ .../agent/runtime/session/live_queue_test.go | 210 +++++ internal/agent/runtime/session/manager.go | 198 +++- internal/agent/runtime/session/memory.go | 44 + .../runtime/session/memory_live_queue.go | 518 +++++++++++ internal/agent/runtime/session/memory_test.go | 43 + internal/agent/runtime/session/queue/queue.go | 37 + internal/agent/runtime/session/reaper.go | 22 + internal/agent/runtime/session/recovery.go | 5 +- internal/agent/runtime/session/redis.go | 37 + .../agent/runtime/session/redis_live_queue.go | 630 +++++++++++++ internal/agent/runtime/session/state.go | 5 +- .../runtime/session/terminal_observer_test.go | 47 + internal/agent/runtime/session/types.go | 83 +- internal/agent/runtime/session/user_turns.go | 189 ++++ .../agent/runtime/session/user_turns_test.go | 264 ++++++ internal/agent/turn/grpctransport/client.go | 5 + internal/agent/turn/grpctransport/server.go | 5 + internal/agent/turn/grpctransport/wire.go | 4 + internal/agent/turn/turn.go | 30 +- internal/apperror/error.go | 25 + internal/channel/inbound/channel.go | 373 ++++---- internal/channel/inbound/channel_test.go | 274 +++++- .../inbound/continuation_output_test.go | 4 +- internal/channel/inbound/dispatcher.go | 310 ------- internal/channel/inbound/dispatcher_test.go | 369 -------- internal/channel/inbound/queue_command.go | 70 ++ ...runtime_fence_postgres_integration_test.go | 77 ++ internal/chat/message/step_commit.go | 189 ++-- internal/chat/message/types.go | 34 + internal/command/menu.go | 2 + internal/config/config_test.go | 1 + internal/db/postgres/sqlc/user_input.sql.go | 152 ++++ internal/db/store/queries.go | 3 + internal/handlers/local_channel.go | 14 +- internal/handlers/session_queue.go | 607 +++++++++++++ internal/handlers/session_queue_test.go | 82 ++ internal/i18n/locales/en.json | 14 +- internal/i18n/locales/ja.json | 14 +- internal/i18n/locales/zh.json | 14 +- internal/rpc/serverruntime/serverruntime.go | 44 +- .../serverruntime/serverruntime_queue_test.go | 74 ++ internal/slash/classifier.go | 18 +- internal/slash/classifier_test.go | 60 +- packages/sdk/src/types.gen.ts | 572 ++++++++++++ spec/docs.go | 855 +++++++++++++++++- spec/swagger.json | 855 +++++++++++++++++- spec/swagger.yaml | 531 +++++++++++ 112 files changed, 10746 insertions(+), 1360 deletions(-) create mode 100644 apps/web/src/pages/home/components/session-follow-up-queue-item.test.ts create mode 100644 apps/web/src/pages/home/components/session-follow-up-queue-item.vue create mode 100644 apps/web/src/pages/home/components/session-follow-up-queue.vue create mode 100644 apps/web/src/pages/home/components/session-queue-submission.test.ts create mode 100644 apps/web/src/pages/home/components/session-queue-submission.ts create mode 100644 apps/web/src/pages/home/components/use-session-follow-up-queue.test.ts create mode 100644 apps/web/src/pages/home/components/use-session-follow-up-queue.ts create mode 100644 apps/web/src/pages/home/composables/useQueueTurnAnchors.ts create mode 100644 apps/web/src/store/chat/runtime-transcript-merge.ts create mode 100644 apps/web/src/store/chat/types.test.ts create mode 100644 docs/design/session-input-queues.md create mode 100644 internal/agent/adapter/channelqueue/adapter.go create mode 100644 internal/agent/application/queue_continuation.go create mode 100644 internal/agent/application/queue_step_binding.go create mode 100644 internal/agent/application/queue_step_transaction.go create mode 100644 internal/agent/application/session_queue.go delete mode 100644 internal/agent/application/step_commit_test.go create mode 100644 internal/agent/application/step_persister_test.go create mode 100644 internal/agent/runtime/session/live_queue.go create mode 100644 internal/agent/runtime/session/live_queue_manager.go create mode 100644 internal/agent/runtime/session/live_queue_redis_test.go create mode 100644 internal/agent/runtime/session/live_queue_test.go create mode 100644 internal/agent/runtime/session/memory_live_queue.go create mode 100644 internal/agent/runtime/session/queue/queue.go create mode 100644 internal/agent/runtime/session/redis_live_queue.go create mode 100644 internal/agent/runtime/session/user_turns.go create mode 100644 internal/agent/runtime/session/user_turns_test.go delete mode 100644 internal/channel/inbound/dispatcher.go delete mode 100644 internal/channel/inbound/dispatcher_test.go create mode 100644 internal/channel/inbound/queue_command.go create mode 100644 internal/handlers/session_queue.go create mode 100644 internal/handlers/session_queue_test.go create mode 100644 internal/rpc/serverruntime/serverruntime_queue_test.go diff --git a/apps/web/src/composables/api/useChat.chat-api.ts b/apps/web/src/composables/api/useChat.chat-api.ts index d11087eee2..31e800d23f 100644 --- a/apps/web/src/composables/api/useChat.chat-api.ts +++ b/apps/web/src/composables/api/useChat.chat-api.ts @@ -17,8 +17,23 @@ import { patchBotsByBotIdSessionsBySessionIdAcpRuntimeMode, patchBotsByBotIdSessionsBySessionIdAcpRuntimeModel, patchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoning, + getBotsByBotIdSessionsBySessionIdQueue, + postBotsByBotIdSessionsBySessionIdSteerQueue, + postBotsByBotIdSessionsBySessionIdFollowUpQueue, + postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer, + putBotsByBotIdSessionsBySessionIdSteerQueueReorder, + putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder, + patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId, + patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, + deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId, + deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, +} from '@memohai/sdk' +import type { + AcpagentRuntimeStatus, + HandlersFollowUpQueueItemResponse, + HandlersSessionQueueResponse, + HandlersSteerQueueItemResponse, } from '@memohai/sdk' -import type { AcpagentRuntimeStatus } from '@memohai/sdk' import type { Bot, SessionSummary } from './useChat.types' export interface CreateSessionOptions { @@ -51,6 +66,99 @@ export interface CreateACPRuntimeOptions { projectPath?: string } +/** + * The two queues share one wire shape for the fields the composer renders. + * The server keeps them as separate response types; the union here is only a + * read-side convenience and never crosses back into a request. + */ +export type SessionQueueItem = Pick< + HandlersSteerQueueItemResponse & HandlersFollowUpQueueItemResponse, + 'item_id' | 'status' | 'position' | 'text' +> + +export type SessionQueuesResponse = HandlersSessionQueueResponse + +export function queueItemText(item: SessionQueueItem): string { + return item.text ?? '' +} + +const queuePath = (botId: string, sessionId: string) => ({ bot_id: botId.trim(), session_id: sessionId.trim() }) + +export async function enqueueSteerQueue(botId: string, sessionId: string, text: string, invocationId = crypto.randomUUID()): Promise { + const { data } = await postBotsByBotIdSessionsBySessionIdSteerQueue({ + path: queuePath(botId, sessionId), + body: { invocation_id: invocationId, text }, + throwOnError: true, + }) + return data +} + +export async function fetchSessionQueues(botId: string, sessionId: string): Promise { + const { data } = await getBotsByBotIdSessionsBySessionIdQueue({ path: queuePath(botId, sessionId), throwOnError: true }) + return data ?? {} +} + +export async function enqueueFollowUpQueue(botId: string, sessionId: string, text: string, invocationId = crypto.randomUUID()): Promise { + const { data } = await postBotsByBotIdSessionsBySessionIdFollowUpQueue({ + path: queuePath(botId, sessionId), + body: { invocation_id: invocationId, text }, + throwOnError: true, + }) + return data +} + +export async function promoteFollowUpQueueItemToSteer(botId: string, sessionId: string, itemId: string): Promise { + const { data } = await postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer({ + path: { ...queuePath(botId, sessionId), item_id: itemId.trim() }, + throwOnError: true, + }) + return data +} + +export async function updateSteerQueueItem(botId: string, sessionId: string, itemId: string, text: string): Promise { + const { data } = await patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId({ + path: { ...queuePath(botId, sessionId), item_id: itemId.trim() }, + body: { text }, + throwOnError: true, + }) + return data +} + +export async function updateFollowUpQueueItem(botId: string, sessionId: string, itemId: string, text: string): Promise { + const { data } = await patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId({ + path: { ...queuePath(botId, sessionId), item_id: itemId.trim() }, + body: { text }, + throwOnError: true, + }) + return data +} + +export async function deleteSteerQueueItem(botId: string, sessionId: string, itemId: string): Promise { + await deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId({ path: { ...queuePath(botId, sessionId), item_id: itemId.trim() }, throwOnError: true }) +} + +export async function deleteFollowUpQueueItem(botId: string, sessionId: string, itemId: string): Promise { + await deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId({ path: { ...queuePath(botId, sessionId), item_id: itemId.trim() }, throwOnError: true }) +} + +export async function reorderSteerQueue(botId: string, sessionId: string, itemId: string, beforeId: string): Promise { + const { data } = await putBotsByBotIdSessionsBySessionIdSteerQueueReorder({ + path: queuePath(botId, sessionId), + body: { item: { item_id: itemId }, before: { item_id: beforeId } }, + throwOnError: true, + }) + return data?.items ?? [] +} + +export async function reorderFollowUpQueue(botId: string, sessionId: string, itemId: string, beforeId: string): Promise { + const { data } = await putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder({ + path: queuePath(botId, sessionId), + body: { item: { item_id: itemId }, before: { item_id: beforeId } }, + throwOnError: true, + }) + return data?.items ?? [] +} + export async function fetchBots(): Promise { const { data } = await getBots({ throwOnError: true }) return data?.items ?? [] diff --git a/apps/web/src/composables/api/useChat.types.ts b/apps/web/src/composables/api/useChat.types.ts index 5ac3b78ff7..b74648c83e 100644 --- a/apps/web/src/composables/api/useChat.types.ts +++ b/apps/web/src/composables/api/useChat.types.ts @@ -438,6 +438,13 @@ export interface RuntimeCurrentRunView { updated_at: string messages: UIMessage[] request_user_turn?: UIUserTurn + // Ordered inputs already admitted into this run. The first entry is the + // request turn when present; later entries are applied steers. + user_turns?: UIUserTurn[] + // Live steer claims projected at their exact assistant-message + // boundary. Claimed entries are provisional; applied entries reference the + // settled history turn that replaces them. + steer_turns?: RuntimeSteerTurnView[] error_code?: string error?: string proposed_terminal_status?: RuntimeRunStatus @@ -446,6 +453,15 @@ export interface RuntimeCurrentRunView { operation?: RuntimeRunOperation } +export interface RuntimeSteerTurnView { + item_id: string + status: 'claimed' | 'applied' + text: string + turn_id?: string + after_message_id: number + timestamp: string +} + export interface RuntimeSnapshot { bot_id: string session_id: string @@ -480,6 +496,9 @@ export interface RuntimeProgressAppend { export interface RuntimeDelta { current_run_view?: RuntimeCurrentRunView run?: RuntimeCurrentRunPatch + user_turn_upserts?: UIUserTurn[] + steer_turn_upserts?: RuntimeSteerTurnView[] + steer_turn_removals?: string[] message_appends?: RuntimeMessageAppend[] progress_appends?: RuntimeProgressAppend[] message_upserts?: UIMessage[] diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 354a78e074..909d16c76b 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -141,6 +141,10 @@ "response_timeout": "The model did not respond in time. Please try again.", "response_interrupted": "The model response was interrupted. Please try again." }, + "queue_no_active_run": "The current response has ended. The message is still queued.", + "queue_admission_overloaded": "The queue is busy. Please retry shortly.", + "queue_admission_unavailable": "Queue admission is temporarily unavailable. Please retry shortly.", + "queue_item_not_pending": "This message is no longer pending in the queue.", "profile": { "request_invalid": "The profile update request is invalid.", "title_model_invalid": "The selected title model is unavailable or is not a chat model.", @@ -696,6 +700,23 @@ }, "currentBot": "Current Bot", "inputPlaceholder": "Ask anything", + "queue": { + "steer": "Current response", + "followUp": "Up next", + "mode": "Send as", + "steerDescription": "Add to the current response at the next safe step", + "followUpDescription": "Send automatically after the current response", + "steerPlaceholder": "Add something to the current response…", + "followUpPlaceholder": "Write the next message…", + "enqueueSteer": "Add to current response", + "enqueueFollowUp": "Add to up next", + "steerQueued": "Added to the current response queue", + "steerFailed": "Could not steer the current response. The message is still queued.", + "reorder": "Reorder", + "remove": "Remove", + "switchToFollowUp": "Queue after this run", + "switchToSteer": "Steer the current run" + }, "readonlyHint": "This chat is read-only", "readonlyPlaceholder": "This chat is read-only. Sending messages is disabled.", "composerActions": "Add files or switch Agent", diff --git a/apps/web/src/i18n/locales/ja.json b/apps/web/src/i18n/locales/ja.json index e472928b59..1239dc7885 100644 --- a/apps/web/src/i18n/locales/ja.json +++ b/apps/web/src/i18n/locales/ja.json @@ -138,6 +138,10 @@ "response_timeout": "モデルから時間内に応答がありませんでした。もう一度お試しください。", "response_interrupted": "モデルの応答が中断されました。もう一度お試しください。" }, + "queue_no_active_run": "現在の応答は終了しました。メッセージはキューに残っています。", + "queue_admission_overloaded": "キューが混雑しています。しばらくしてから再試行してください。", + "queue_admission_unavailable": "キューサービスは一時的に利用できません。しばらくしてから再試行してください。", + "queue_item_not_pending": "このメッセージは待機中のキューにありません。", "profile": { "request_invalid": "プロフィール更新リクエストが無効です。", "title_model_invalid": "選択したタイトルModelは利用できないか、チャットModelではありません。", @@ -680,6 +684,23 @@ }, "currentBot": "現在のBot", "inputPlaceholder": "質問を入力してください", + "queue": { + "steer": "現在の応答", + "followUp": "次のメッセージ", + "mode": "送信方法", + "steerDescription": "次の安全なステップで現在の応答に追加", + "followUpDescription": "現在の応答が終わった後に自動送信", + "steerPlaceholder": "現在の応答に追加する内容を入力…", + "followUpPlaceholder": "次のメッセージを入力…", + "enqueueSteer": "現在の応答に追加", + "enqueueFollowUp": "次のメッセージに追加", + "steerQueued": "現在の応答キューに追加済み", + "steerFailed": "現在の応答に追加できませんでした。メッセージはキューに残っています。", + "reorder": "並べ替え", + "remove": "削除", + "switchToFollowUp": "この実行の後に追加", + "switchToSteer": "現在の実行に追加" + }, "readonlyHint": "このチャットは読み取り専用です", "readonlyPlaceholder": "このチャットは読み取り専用です。メッセージの送信は無効になっています。", "composerActions": "ファイルの追加またはエージェントの切り替え", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index f1ceaad4fa..afcc18b6c9 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -141,6 +141,10 @@ "response_timeout": "模型未能及时响应,请重试。", "response_interrupted": "模型响应意外中断,请重试。" }, + "queue_no_active_run": "当前回复已经结束,消息仍保留在队列中。", + "queue_admission_overloaded": "队列当前繁忙,请稍后重试。", + "queue_admission_unavailable": "队列服务暂时不可用,请稍后重试。", + "queue_item_not_pending": "这条消息已不在待处理队列中。", "profile": { "request_invalid": "个人资料更新请求无效。", "title_model_invalid": "所选标题模型不可用或不是聊天模型。", @@ -696,6 +700,23 @@ }, "currentBot": "当前 Bot", "inputPlaceholder": "问点什么", + "queue": { + "steer": "当前回复", + "followUp": "接下来", + "mode": "发送方式", + "steerDescription": "在下一个安全步骤加入当前回复", + "followUpDescription": "当前回复结束后自动发送", + "steerPlaceholder": "输入要插入当前回复的内容…", + "followUpPlaceholder": "输入下一条消息…", + "enqueueSteer": "插入当前回复", + "enqueueFollowUp": "添加到接下来", + "steerQueued": "已加入当前回复队列", + "steerFailed": "无法插入当前回复,消息仍保留在队列中。", + "reorder": "调整顺序", + "remove": "删除", + "switchToFollowUp": "排到本次运行之后", + "switchToSteer": "插入当前运行" + }, "readonlyHint": "该聊天为只读", "readonlyPlaceholder": "该聊天为只读,无法发送消息", "composerActions": "添加文件或切换 Agent", diff --git a/apps/web/src/pages/home/components/chat-pane.vue b/apps/web/src/pages/home/components/chat-pane.vue index 1df15caa59..d883e9e41e 100644 --- a/apps/web/src/pages/home/components/chat-pane.vue +++ b/apps/web/src/pages/home/components/chat-pane.vue @@ -410,6 +410,13 @@ :class="isWelcome ? 'min-h-28 p-3' : 'p-2.5 chat-composer-docked'" @click="handleComposerClick" > + rejected (error_code set) + +----------------> canceled +``` -Steer items capture the active run ID at admission and are claimable only by -that run's owner, generation, and fencing token. Follow-up items capture the -run that was active when they were enqueued. At a terminal boundary the -application claims the next follow-up and starts a normal new turn; applying -the claim is idempotent, and a failed start releases the claim for retry. +- `accepted`: the item is stored and pending. Only accepted items are listed, + reordered, edited, or canceled. +- `claimed`: a consumer holds the item. A steer claim carries the run ID, + owner, generation, fencing token, and a claim token; a follow-up claim carries + the terminal run that triggered it and a claim token. +- `applied`: the item entered a model step whose history commit succeeded + (steer), or started its continuation run (follow-up). +- `rejected`: the runtime refused the item after acceptance. `ErrorCode` names + the stable reason; today the only reason is `queue_target_run_not_active`. +- `canceled`: the caller withdrew a pending item. Promoting a follow-up to a + steer cancels the follow-up and creates a new steer item. + +`expired` exists in the status vocabulary but no path writes it. + +Invocation IDs provide best-effort replay protection for the lifetime of the +retained items: the same invocation with the same payload returns the existing +item, and a different payload returns `ErrQueueInvocationConflict`. + +## Capacity and compaction + +Each queue document is bounded in two ways: + +- At most `MaxPendingQueueItems` (64) accepted items per queue and session. + Enqueue beyond that returns `ErrQueueCapacityExceeded`, surfaced as + `queue_capacity_exceeded` over HTTP and channel slash commands. +- After every mutation the document keeps all accepted and claimed items and + only the newest 64 terminal items. Map entries that reference dropped items + (promotion records, per-run follow-up claims) are removed with them. + +Replay protection therefore covers roughly the last 64 completed submissions. + +## Steer + +A steer is bound to the run that was active when it was admitted. + +History keeps its existing turn model: every user message opens a turn, and +the assistant and tool rows that answer it belong to that turn. An applied +steer is therefore persisted as its own turn, and the output that follows it +is filed under that turn, while the run ID does not change. A run can span +several turns; the live projection names the post-steer assistant segment +after the steer's turn as soon as that turn is known. + +- Admission records the active run as `TargetRunID`. Without an active run, + or after the run has been sealed, admission returns `ErrQueueNoActiveRun`. +- At each committed step the application applies the previously claimed steer + and claims the next accepted steer for the same run. During a tool loop the + claimed text is injected into the next model request; at a final step the + claim reopens the same run with the steer as the next model input. +- A step that parks the run for a tool approval or user input applies the + previous claim but does not claim a new one. The continuation's first + committed step claims instead, so no claim waits unapplied across the + decision. +- `ClaimNextSteer` returns the run's existing unapplied claim before selecting + a new item. When the run has been reclaimed by a new owner, the stored claim + is advanced to the new owner, generation, and fencing token; the previous + owner's reference no longer matches it. +- A final step that finds no steer seals the run (`ClosedRunID`), so a steer + that arrives between the final commit and the terminal record is refused. +- When the run reaches any terminal state the terminal observer calls + `CloseSteerRun`: every accepted or claimed steer targeting the run becomes + `rejected` with `queue_target_run_not_active`, and the run is sealed. + +A claim is valid only for the run's current owner, generation, and fencing +token; applying with a stale claim returns `ErrRunOwnershipLost`. + +## Follow-up + +A follow-up is bound to the session. It records the run that was active when +it was enqueued (`EnqueuedDuringRunID`) but is consumed by whichever run +finishes next. + +Two producers share the queue: + +- The queue panel and the `/queue` slash command store `{"text": ...}`. The + continuation starts as an ordinary chat turn on the same session. `/queue` + is accepted only on local channel types (web, cli); a platform channel gets + `queue_follow_up_unsupported_channel`, because it could not receive the + reply of a run the server starts from the queue. `/steer` stays available on + every channel: it joins the run whose reply the channel is already + streaming. +- A complete turn that met a busy session (`turn.ErrSessionBusy`) may be + stored through `EnqueueDeferredTurn` as `{"text": ..., "command": ...}` with + the full `StartTurnCommand`. The continuation keeps the route, reply target, + attachments, and metadata of the original message. The caller sees + `turn.ErrTurnDeferred`; if the run ended before the enqueue, the caller sees + `ErrQueueNoActiveRun` and retries ordinary admission. + Only the local channel types (web, cli) do this: their users observe the + resulting run through the session runtime subscription. Platform channels + deliver replies by streaming the caller's run handle, and a run started from + the queue has no such consumer, so they keep the bounded busy retry and + surface `ErrSessionBusy` when it expires. `StartTurn` itself never defers. + +Consumption: + +- The terminal observer claims the oldest accepted follow-up for the finished + run and starts it through normal turn admission with `NoDefer` set and the + retry identity `follow-up:`. Admission remains the only owner and + fencing authority; the queue only selects payload. +- One starter runs per session at a time. A successful start applies the + claim; a failed start releases it so the next terminal boundary claims it + again. Ordinary user turns may win the session slot first; the follow-up + then waits for that run to end. +- An enqueue that observed an active run re-checks the live snapshot after + writing. If the run has already ended, the enqueue path starts the follow-up + itself, so an item cannot wait for an unrelated later run. +- Follow-ups are not rejected when the run they were queued behind aborts, + fails, or is lost. The queued input still belongs to the session; only + steers, which are run-bound, are rejected at terminal. + +## Redis transactions + +Queue mutations run as `WATCH`/`MULTI` transactions whose watch set is the +queue document plus, where a decision depends on ownership, the run key that +the finishing owner deletes. The session state key is read inside the +transaction but never watched: it is rewritten on every streamed runtime delta +and watching it would fail queue transactions during normal output. A steer +admitted against a snapshot that turns terminal is still closed by +`CloseSteerRun`, which serializes on the queue document. + +Conflicting transactions retry with exponential backoff up to eight times and +then return `ErrQueueAdmissionOverloaded`. ## PostgreSQL boundary From 423e6a7183bff43bd716a49a872f174955e7a4a4 Mon Sep 17 00:00:00 2001 From: Fodesu Date: Mon, 7 Sep 2026 16:11:23 +0800 Subject: [PATCH 4/7] fix(agent): log the private cause of decision continuation stream errors publicAgentStreamEvent replaces a native error event's detail with a stable code before the event leaves the application, and continueRuntimeDecision derives its failure cause from that public event. A failed ask_user or tool approval continuation therefore logged only agent.response_interrupted. Record the original event text in the continuation loops before the conversion. Co-Authored-By: Claude Fable 5.1 --- internal/agent/application/runtime_decision.go | 16 ++++++++++++++++ .../agent/application/service_tool_approval.go | 4 ++++ internal/agent/application/service_user_input.go | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/internal/agent/application/runtime_decision.go b/internal/agent/application/runtime_decision.go index a21a2d7b15..104af0f790 100644 --- a/internal/agent/application/runtime_decision.go +++ b/internal/agent/application/runtime_decision.go @@ -503,6 +503,22 @@ func (s *Service) logRuntimeDecisionContinuationFailure(command sessionruntime.C ) } +// logContinuationStreamError records the private detail of a native error +// event observed while a decision continuation streams. publicAgentStreamEvent +// replaces that detail with a stable code before the event leaves the +// application, so this is the only place the original text is retained. +func (s *Service) logContinuationStreamError(runID string, event native.StreamEvent) { + if s == nil || s.logger == nil { + return + } + s.logger.Error("decision continuation stream error", + slog.String("run_id", strings.TrimSpace(runID)), + slog.String("event_type", string(event.Type)), + slog.String("code", strings.TrimSpace(event.Code)), + slog.String("error", strings.TrimSpace(event.Error)), + ) +} + func firstLifecycleCause(causes ...error) error { for _, cause := range causes { if cause != nil { diff --git a/internal/agent/application/service_tool_approval.go b/internal/agent/application/service_tool_approval.go index ab06dbde87..e22d6e70d4 100644 --- a/internal/agent/application/service_tool_approval.go +++ b/internal/agent/application/service_tool_approval.go @@ -516,6 +516,10 @@ func (s *Service) continueToolApprovalSession( } if eventErr := agentStreamLifecycleError(event); eventErr != nil && lifecycleCause == nil { lifecycleCause = eventErr + // The public event forwarded downstream carries only a stable code; + // keep the runtime's private detail in the server log so a failed + // continuation can be diagnosed. + s.logContinuationStreamError(req.RunID, event) } if event.IsTerminal() { terminalEventSeen = true diff --git a/internal/agent/application/service_user_input.go b/internal/agent/application/service_user_input.go index 7e3ae5f5db..5f5bc64075 100644 --- a/internal/agent/application/service_user_input.go +++ b/internal/agent/application/service_user_input.go @@ -470,6 +470,10 @@ func (s *Service) continueUserInputSession( } if eventErr := agentStreamLifecycleError(event); eventErr != nil && lifecycleCause == nil { lifecycleCause = eventErr + // The public event forwarded downstream carries only a stable code; + // keep the runtime's private detail in the server log so a failed + // continuation can be diagnosed. + s.logContinuationStreamError(chatReq.RunID, event) } if event.IsTerminal() { terminalEventSeen = true From 819a62882c0eacffbee41a37b0d159d4e7badf29 Mon Sep 17 00:00:00 2001 From: Fodesu Date: Mon, 7 Sep 2026 16:57:52 +0800 Subject: [PATCH 5/7] fix(web): merge runtime frames in projection order instead of re-sorting by timestamp applyRuntimeTranscript re-sorted the whole transcript whenever a runtime frame carried a user turn with turn_position. sortChatMessages falls back to timestamps when the other side has no position, and a live assistant turn never has one. Once the first step commit persisted the request user, the frame replaced it with the stored row whose timestamp is the commit time, later than the assistant turn that had already started streaming, so the reply rendered above its own request. The runtime projection already orders a run's turns (request users, then assistant segments split around each steer). Insert that block as delivered. Co-Authored-By: Claude Fable 5.1 --- .../web/src/store/chat-list.normalize.test.ts | 8 ++ apps/web/src/store/chat-list.normalize.ts | 9 ++ .../src/store/chat/runtime-projection.test.ts | 34 ++++++ apps/web/src/store/chat/runtime-projection.ts | 17 ++- .../store/chat/runtime-transcript-merge.ts | 11 +- apps/web/src/store/chat/transcript.test.ts | 110 +++++++++++++++++- apps/web/src/store/chat/transcript.ts | 20 ++-- 7 files changed, 186 insertions(+), 23 deletions(-) diff --git a/apps/web/src/store/chat-list.normalize.test.ts b/apps/web/src/store/chat-list.normalize.test.ts index 2b66b2f67e..3244e78abf 100644 --- a/apps/web/src/store/chat-list.normalize.test.ts +++ b/apps/web/src/store/chat-list.normalize.test.ts @@ -107,6 +107,14 @@ describe('sortChatMessages', () => { expect(sorted.map(m => m.id)).toEqual(['a', 'c', 'b']) expect(items.map(m => m.id)).toEqual(['b', 'a', 'c']) }) + + it('keeps the request before the reply inside one turn when timestamps tie', () => { + const items = [ + { id: 'runtime-assistant', role: 'assistant', turnId: 'turn-1', turnPosition: 3, messages: [], timestamp: '2026-07-09T00:00:01.000Z', streaming: false }, + { id: 'runtime-user', role: 'user', turnId: 'turn-1', turnPosition: 3, text: 'hi', timestamp: '2026-07-09T00:00:01.000Z', streaming: false }, + ] as ChatMessage[] + expect(sortChatMessages(items).map(m => m.role)).toEqual(['user', 'assistant']) + }) }) describe('isOptimisticTurn', () => { diff --git a/apps/web/src/store/chat-list.normalize.ts b/apps/web/src/store/chat-list.normalize.ts index 257f84897b..107bbede17 100644 --- a/apps/web/src/store/chat-list.normalize.ts +++ b/apps/web/src/store/chat-list.normalize.ts @@ -120,6 +120,8 @@ export function skillActivationTextFromRaw(text: string, activation: UISkillActi return matchesSkill ? rest.join(' ').trim() : '' } +const turnRoleRank: Record = { user: 0, assistant: 1, system: 2 } + export function sortChatMessages(items: ChatMessage[]): ChatMessage[] { return [...items].sort((a, b) => { // Turn positions are the authoritative order once both sides carry one; @@ -127,6 +129,13 @@ export function sortChatMessages(items: ChatMessage[]): ChatMessage[] { const ap = a.turnPosition const bp = b.turnPosition if (ap !== undefined && bp !== undefined && ap !== bp) return ap - bp + // Inside one turn the request precedes the reply. Rows of a turn are + // persisted together at step commit and share a timestamp, so neither + // timestamps nor ids can order them. + const aTurn = a.turnId?.trim() ?? '' + if (aTurn && aTurn === (b.turnId?.trim() ?? '') && a.role !== b.role) { + return turnRoleRank[a.role] - turnRoleRank[b.role] + } const at = Date.parse(a.timestamp) const bt = Date.parse(b.timestamp) if (!Number.isNaN(at) && !Number.isNaN(bt) && at !== bt) return at - bt diff --git a/apps/web/src/store/chat/runtime-projection.test.ts b/apps/web/src/store/chat/runtime-projection.test.ts index 232480399a..08afb30b1b 100644 --- a/apps/web/src/store/chat/runtime-projection.test.ts +++ b/apps/web/src/store/chat/runtime-projection.test.ts @@ -225,6 +225,40 @@ describe('runtime projection', () => { const users = applied.transcript.turns.filter(turn => turn.role === 'user') expect(users).toHaveLength(2) expect(users[1]).toMatchObject({ turn_id: 'turn-steer-1', turn_position: 2 }) + // History files the post-steer assistant output under the steer's turn; + // the live segment must carry that identity so the settled page replaces + // it instead of rendering a second copy beside it. + expect(applied.transcript.turns.map(turn => [turn.role, turn.turn_id])).toEqual([ + ['user', 'turn-1'], + ['assistant', 'turn-1'], + ['user', 'turn-steer-1'], + ['assistant', 'turn-steer-1'], + ]) + }) + + it('drops the empty trailing assistant segment once the run has settled', () => { + const state = reduceRuntimeProjection(createEmptyRuntimeProjection(), snapshot(runView({ + status: 'completed', + messages: [{ id: 0, type: 'text', content: 'before' }], + user_turns: [ + { turn_id: 'turn-1', role: 'user', text: 'hello', timestamp: '2026-07-27T08:00:00.000Z' }, + { turn_id: 'turn-steer-1', turn_position: 2, role: 'user', text: 'change direction', timestamp: '2026-07-27T08:00:02.000Z' }, + ], + steer_turns: [{ + item_id: 'steer-item-1', + status: 'applied', + text: 'change direction', + turn_id: 'turn-steer-1', + after_message_id: 0, + timestamp: '2026-07-27T08:00:02.000Z', + }], + }))) + + expect(state.transcript.turns.map(turn => [turn.role, turn.turn_id])).toEqual([ + ['user', 'turn-1'], + ['assistant', 'turn-1'], + ['user', 'turn-steer-1'], + ]) }) it('treats a null message list from an idle runtime snapshot as empty', () => { diff --git a/apps/web/src/store/chat/runtime-projection.ts b/apps/web/src/store/chat/runtime-projection.ts index 295b6ff4d6..2ae8b71a77 100644 --- a/apps/web/src/store/chat/runtime-projection.ts +++ b/apps/web/src/store/chat/runtime-projection.ts @@ -193,13 +193,24 @@ function transcriptForRun(run: RuntimeCurrentRunView | null): RuntimeTranscriptS id: `runtime:${RUNTIME_STEER_TURN_PREFIX}${steer.item_id}:user`, }) segmentStart = segmentEnd - segmentTurnId = `${RUNTIME_STEER_TURN_PREFIX}${steer.item_id}:assistant` + // History persists an applied steer as its own turn and files the + // assistant output that follows it under that turn. Name the live + // segment after the durable turn as soon as it is known, so the settled + // page replaces this segment instead of rendering beside it. Until then + // the segment carries the provisional steer identity. + segmentTurnId = durable + ? steerTurnId + : `${RUNTIME_STEER_TURN_PREFIX}${steer.item_id}:assistant` segmentTimestamp = steer.timestamp } // The final segment is the only live assistant after a steer boundary. It // intentionally exists while empty so the running indicator stays below - // the newly admitted user input until the next model delta arrives. - turns.push(runtimeAssistantTurn(segmentTurnId, segmentTimestamp, assistantMessages.slice(segmentStart))) + // the newly admitted user input until the next model delta arrives. Once + // the run has settled an empty segment would only render a blank turn. + const finalSegment = assistantMessages.slice(segmentStart) + if (active || finalSegment.length > 0 || turns.every(turn => turn.role !== 'assistant')) { + turns.push(runtimeAssistantTurn(segmentTurnId, segmentTimestamp, finalSegment)) + } } return { runId: run.run_id, diff --git a/apps/web/src/store/chat/runtime-transcript-merge.ts b/apps/web/src/store/chat/runtime-transcript-merge.ts index 236efae146..584a9d89d1 100644 --- a/apps/web/src/store/chat/runtime-transcript-merge.ts +++ b/apps/web/src/store/chat/runtime-transcript-merge.ts @@ -8,8 +8,15 @@ export function markRuntimeTurn( slice: RuntimeTranscriptSlice, originalUser: boolean, ): RuntimeChatTurn { - const isSteerAssistantSegment = turn.role === 'assistant' && isRuntimeSteerTurnId(turn.turnId) - if (originalUser || !turn.turnId || (turn.role === 'assistant' && !isSteerAssistantSegment)) { + // An assistant segment keeps its own turn identity when it is provisional + // (steer prefix) or nested under a user turn the same frame carries: that + // is the durable turn history files the post-steer output under. Any other + // assistant turn belongs to the run's request turn. + const nestedAssistantSegment = turn.role === 'assistant' && Boolean(turn.turnId) && ( + isRuntimeSteerTurnId(turn.turnId) + || slice.turns.some(other => other.role === 'user' && other.turn_id.trim() === turn.turnId) + ) + if (originalUser || !turn.turnId || (turn.role === 'assistant' && !nestedAssistantSegment)) { turn.turnId = slice.turnId } turn.runtimeRunId = slice.runId diff --git a/apps/web/src/store/chat/transcript.test.ts b/apps/web/src/store/chat/transcript.test.ts index bdd030706c..d52f9ffb18 100644 --- a/apps/web/src/store/chat/transcript.test.ts +++ b/apps/web/src/store/chat/transcript.test.ts @@ -601,6 +601,8 @@ describe('chat transcript controller', () => { status: 'running', operation: null, streaming: true, + // Frames arrive in projection order: request user, the assistant + // segment that preceded the steer, then the steer itself. turns: [ { id: 'runtime-user', @@ -609,6 +611,13 @@ describe('chat transcript controller', () => { text: 'original', timestamp: '2026-01-01T00:00:00.000Z', }, + { + id: 'runtime-assistant', + turn_id: 'turn-1', + role: 'assistant', + messages: [{ id: 0, type: 'text', content: 'tool output' }], + timestamp: '2026-01-01T00:01:00.000Z', + }, { id: 'runtime-steer', turn_id: 'turn-steer', @@ -617,21 +626,74 @@ describe('chat transcript controller', () => { text: 'continue from here', timestamp: '2026-01-01T00:02:00.000Z', }, + ], + }) + + expect(transcript.messages.map(turn => turn.role)).toEqual(['user', 'assistant', 'user']) + expect(transcript.messages[2]).toMatchObject({ + role: 'user', + text: 'continue from here', + }) + }) + + it('keeps a request user persisted at step commit ahead of the assistant that started before it', () => { + const { transcript } = makeTranscript() + transcript.applyRuntimeTranscript({ + runId: 'run-1', + turnId: 'turn-1', + invocationId: '', + status: 'running', + operation: null, + streaming: true, + turns: [ + { + id: 'runtime-user', + turn_id: 'turn-1', + role: 'user', + text: 'question', + timestamp: '2026-01-01T00:00:00.000Z', + }, { id: 'runtime-assistant', turn_id: 'turn-1', role: 'assistant', - messages: [{ id: 0, type: 'text', content: 'tool output' }], - timestamp: '2026-01-01T00:01:00.000Z', + messages: [{ id: 0, type: 'text', content: 'first step' }], + timestamp: '2026-01-01T00:00:00.000Z', }, ], }) - expect(transcript.messages.map(turn => turn.role)).toEqual(['user', 'assistant', 'user']) - expect(transcript.messages[2]).toMatchObject({ - role: 'user', - text: 'continue from here', + // The first step commit persists the request user; the runtime frame now + // carries that row with its turn_position and the commit timestamp, which + // is later than the assistant turn that was already streaming. + transcript.applyRuntimeTranscript({ + runId: 'run-1', + turnId: 'turn-1', + invocationId: '', + status: 'running', + operation: null, + streaming: true, + turns: [ + { + id: 'persisted-user', + turn_id: 'turn-1', + turn_position: 7, + role: 'user', + text: 'question', + timestamp: '2026-01-01T00:00:28.000Z', + }, + { + id: 'runtime-assistant', + turn_id: 'turn-1', + role: 'assistant', + messages: [{ id: 0, type: 'text', content: 'first step' }, { id: 1, type: 'text', content: 'second step' }], + timestamp: '2026-01-01T00:00:00.000Z', + }, + ], }) + + expect(transcript.messages.map(turn => turn.role)).toEqual(['user', 'assistant']) + expect(transcript.messages[0]).toMatchObject({ role: 'user', text: 'question', turnPosition: 7 }) }) it('renders a provisional steer between completed and next-step assistant output', () => { @@ -680,6 +742,42 @@ describe('chat transcript controller', () => { expect(transcript.messages[1]).toMatchObject({ streaming: false }) }) + it('lets settled history adopt the post-steer assistant segment instead of duplicating it', () => { + const { transcript } = makeTranscript() + transcript.replaceMessages([rawUser('history-old', 'earlier')], 'session-1') + transcript.hasLoadedOlder.value = true + transcript.applyRuntimeTranscript({ + runId: 'run-1', + turnId: 'turn-1', + invocationId: '', + status: 'running', + operation: null, + streaming: true, + turns: [ + { id: 'runtime-user', turn_id: 'turn-1', role: 'user', text: 'original', timestamp: '2026-01-01T00:00:00.000Z' }, + { id: 'runtime-assistant', turn_id: 'turn-1', role: 'assistant', messages: [{ id: 0, type: 'text', content: 'before steer' }], timestamp: '2026-01-01T00:00:00.000Z' }, + { id: 'runtime-steer', turn_id: 'turn-steer-1', turn_position: 2, role: 'user', text: 'change direction', timestamp: '2026-01-01T00:02:00.000Z' }, + { id: 'runtime-assistant-after', turn_id: 'turn-steer-1', role: 'assistant', messages: [{ id: 1, type: 'text', content: 'after steer' }], timestamp: '2026-01-01T00:02:00.000Z' }, + ], + }) + + transcript.mergeMessages([ + rawUser('history-old', 'earlier'), + { id: 'db-user', turn_id: 'turn-1', turn_position: 1, role: 'user', text: 'original', timestamp: '2026-01-01T00:00:30.000Z' }, + { id: 'db-assistant', turn_id: 'turn-1', turn_position: 1, role: 'assistant', messages: [{ id: 0, type: 'text', content: 'before steer' }], timestamp: '2026-01-01T00:00:30.000Z' }, + { id: 'db-steer', turn_id: 'turn-steer-1', turn_position: 2, role: 'user', text: 'change direction', timestamp: '2026-01-01T00:02:00.000Z' }, + { id: 'db-assistant-after', turn_id: 'turn-steer-1', turn_position: 2, role: 'assistant', messages: [{ id: 0, type: 'text', content: 'after steer' }], timestamp: '2026-01-01T00:02:00.000Z' }, + ], 'session-1') + + expect(transcript.messages.map(turn => [turn.role, turn.turnId])).toEqual([ + ['user', 'turn-history-old'], + ['user', 'turn-1'], + ['assistant', 'turn-1'], + ['user', 'turn-steer-1'], + ['assistant', 'turn-steer-1'], + ]) + }) + it('requires a history resync when a replacement anchor is missing', () => { const { transcript } = makeTranscript() transcript.replaceMessages([rawUser('user-old')], 'session-1') diff --git a/apps/web/src/store/chat/transcript.ts b/apps/web/src/store/chat/transcript.ts index 17e9956db8..964c4e7e5d 100644 --- a/apps/web/src/store/chat/transcript.ts +++ b/apps/web/src/store/chat/transcript.ts @@ -10,7 +10,6 @@ import { messageIdentityId, mergeApprovalState, nextId, - sortChatMessages, } from '../chat-list.normalize' import { upsertById } from '../chat-list.utils' import type { @@ -528,17 +527,14 @@ export function createTranscriptController({ for (let index = indices.length - 1; index >= 0; index -= 1) { messages.splice(indices[index]!, 1) } - // A persisted steer carries its authoritative turn_position. Runtime - // frames previously inserted the whole run at the first matching turn, - // which moved a later steer above the assistant/tool output that preceded - // it. Once a position is known, merge and use the same ordering rule as - // settled history; positionless live frames retain their arrival order. - if (resolved.some(turn => turn.role === 'user' && turn.turnPosition !== undefined)) { - messages.splice(0, 0, ...resolved) - messages.splice(0, messages.length, ...sortChatMessages(messages)) - } else { - messages.splice(insertAt, 0, ...resolved) - } + // The runtime frame already orders a run's turns: request users first, + // then assistant segments split around each steer by after_message_id. + // Insert that block as delivered. Re-sorting the whole transcript here + // would fall back to timestamps wherever a live assistant turn has no + // turn_position yet, and a request user persisted at step commit carries + // a later timestamp than the assistant turn that started streaming + // before it, which rendered the reply above its own request. + messages.splice(insertAt, 0, ...resolved) return true } From 030f5abeac1e185f11a776980be0ea9e1420bc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=A8=E8=8B=92?= <16112591+chen-ran@users.noreply.github.com> Date: Wed, 9 Sep 2026 02:45:04 +0800 Subject: [PATCH 6/7] refactor(runtime): consolidate queue state and compatibility paths Retire unused steer and decision entry points, share queue transitions and native continuation lifecycles, and keep one canonical runtime input source. Remove redundant command JSON conversion and queue reorder allocations. Preserve committed input/output across queue failures and owner loss, and reconcile prepared terminal outcomes after the live lease expires. Consolidate duplicate tests while retaining the failure and recovery cases. Validation: full Go tests, Redis/Valkey race contracts, 20 two-server fault scenarios, Web tests/build, SDK typecheck, and changed-code lint. Full-repository Go lint and Web typecheck retain documented baseline failures. Current steer remains step-boundary based; immediate preemption and the ordinary busy-input policy are still outstanding. No human QA recorded. --- .github/workflows/go-ci.yml | 2 + apps/web/src/composables/api/useChat.types.ts | 10 - apps/web/src/i18n/locales/en.json | 3 + apps/web/src/i18n/locales/ja.json | 3 + apps/web/src/i18n/locales/zh.json | 3 + .../session-follow-up-queue-item.test.ts | 4 +- .../session-follow-up-queue-item.vue | 3 +- .../components/session-follow-up-queue.vue | 3 +- .../components/use-session-follow-up-queue.ts | 4 +- .../home/composables/useChatScroll.test.ts | 5 +- apps/web/src/store/chat-list.test.ts | 60 +-- .../src/store/chat/external-agent-defaults.ts | 3 +- .../store/chat/external-agent-input.test.ts | 26 ++ .../src/store/chat/external-agent-sessions.ts | 30 +- .../src/store/chat/external-agent-staging.ts | 29 +- apps/web/src/store/chat/runtime-projection.ts | 24 +- docs/design/session-runtime-requirements.md | 12 +- .../agent/adapter/channelqueue/adapter.go | 14 +- internal/agent/application/contract.go | 7 +- .../agent/application/decision_output_test.go | 10 +- .../native_decision_continuation.go | 172 +++++++ .../agent/application/queue_continuation.go | 78 +++- .../application/queue_continuation_test.go | 187 +++++--- .../agent/application/queue_payload_test.go | 18 + .../agent/application/queue_step_binding.go | 10 +- ...ansaction.go => queue_step_coordinator.go} | 92 ++-- .../application/queue_step_deferred_test.go | 30 +- .../application/queue_step_failure_test.go | 86 ++++ .../runtime_decision_finish_test.go | 3 +- .../application/runtime_decision_test.go | 24 +- internal/agent/application/service.go | 3 +- .../application/service_run_lifecycle_test.go | 9 - internal/agent/application/service_stream.go | 6 +- .../application/service_tool_approval.go | 158 +------ .../agent/application/service_trigger_test.go | 2 +- .../agent/application/service_user_input.go | 159 +------ .../application/service_user_input_test.go | 24 +- internal/agent/application/session_queue.go | 77 ++- internal/agent/application/step_commit.go | 82 ++-- .../agent/application/step_ownership_test.go | 47 ++ .../agent/application/step_persister_test.go | 20 +- .../subagent_abort_control_alignment_test.go | 2 +- .../subagent_runtime_alignment_test.go | 172 +------ .../agent/application/subagent_step_commit.go | 17 +- internal/agent/application/turn_admission.go | 17 +- .../turn_admission_integration_test.go | 14 +- .../agent/application/turn_admission_test.go | 2 +- .../application/turn_inject_ownership_test.go | 2 +- .../agent/application/turn_service_test.go | 2 +- .../agent/runtime/acp/client/client_test.go | 10 +- internal/agent/runtime/native/agent.go | 75 ++- .../native/provider_stream_observer.go | 12 +- internal/agent/runtime/native/stream_test.go | 20 +- internal/agent/runtime/native/types.go | 4 +- .../runtime/session/acceptance/README.md | 26 +- .../session/acceptance/queue_contract_test.go | 204 ++++++++ .../session/acceptance/terminal_crash_test.go | 148 ++++++ internal/agent/runtime/session/admit_test.go | 388 +--------------- .../runtime/session/command_transport_test.go | 84 ++++ internal/agent/runtime/session/commands.go | 437 +----------------- .../runtime/session/decision_output_test.go | 2 +- .../runtime/session/decision_route_test.go | 226 +++++---- internal/agent/runtime/session/finalize.go | 4 +- .../runtime/session/inject_ownership_test.go | 2 +- internal/agent/runtime/session/live_queue.go | 127 ++--- .../runtime/session/live_queue_manager.go | 26 +- .../runtime/session/live_queue_redis_test.go | 100 +--- .../agent/runtime/session/live_queue_test.go | 298 ++++++------ .../runtime/session/live_queue_transitions.go | 235 ++++++++++ internal/agent/runtime/session/manager.go | 119 ++--- .../agent/runtime/session/manager_test.go | 382 ++------------- internal/agent/runtime/session/memory.go | 6 +- .../runtime/session/memory_live_queue.go | 242 +++------- internal/agent/runtime/session/queue/queue.go | 41 -- .../runtime/session/queue_capability_test.go | 67 +++ .../session/queue_reorder_regression_test.go | 31 ++ internal/agent/runtime/session/reaper_test.go | 88 ++-- internal/agent/runtime/session/recovery.go | 1 + .../agent/runtime/session/recovery_test.go | 22 +- internal/agent/runtime/session/redis.go | 28 +- .../runtime/session/redis_lease_index_test.go | 10 +- .../agent/runtime/session/redis_live_queue.go | 226 ++------- .../agent/runtime/session/redis_liveness.go | 5 +- internal/agent/runtime/session/redis_test.go | 41 +- .../session/reservation_fixture_test.go | 38 ++ .../agent/runtime/session/run_view_codec.go | 90 ++++ .../runtime/session/run_view_codec_test.go | 61 +++ internal/agent/runtime/session/state.go | 6 +- .../runtime/session/terminal_observer_test.go | 44 +- .../session/terminal_reconciler_test.go | 40 ++ internal/agent/runtime/session/types.go | 45 +- internal/agent/runtime/session/user_turns.go | 21 +- .../turn/grpctransport/transport_test.go | 23 + internal/agent/turn/grpctransport/wire.go | 97 ++-- internal/apperror/error.go | 5 + ...runtime_fence_postgres_integration_test.go | 13 +- internal/chat/message/service.go | 30 +- internal/chat/message/step_commit.go | 98 ++-- internal/chat/message/types.go | 37 +- .../contextview/steer_continuation_test.go | 63 +++ internal/handlers/session_queue.go | 80 ++-- internal/handlers/session_queue_test.go | 10 +- internal/testutil/sessionledger/ledger.go | 374 +++++++++++++++ internal/testutil/sessionruntime/runtime.go | 50 ++ mise.toml | 4 +- packages/sdk/src/@pinia/colada.gen.ts | 181 +++++++- packages/sdk/src/index.ts | 4 +- packages/sdk/src/sdk.gen.ts | 104 ++++- packages/sdk/src/types.gen.ts | 35 +- spec/docs.go | 85 ++-- spec/swagger.json | 85 ++-- spec/swagger.yaml | 66 +-- 112 files changed, 3788 insertions(+), 3508 deletions(-) create mode 100644 apps/web/src/store/chat/external-agent-input.test.ts create mode 100644 internal/agent/application/native_decision_continuation.go create mode 100644 internal/agent/application/queue_payload_test.go rename internal/agent/application/{queue_step_transaction.go => queue_step_coordinator.go} (65%) create mode 100644 internal/agent/application/queue_step_failure_test.go create mode 100644 internal/agent/application/step_ownership_test.go create mode 100644 internal/agent/runtime/session/acceptance/queue_contract_test.go create mode 100644 internal/agent/runtime/session/acceptance/terminal_crash_test.go create mode 100644 internal/agent/runtime/session/command_transport_test.go create mode 100644 internal/agent/runtime/session/live_queue_transitions.go delete mode 100644 internal/agent/runtime/session/queue/queue.go create mode 100644 internal/agent/runtime/session/queue_capability_test.go create mode 100644 internal/agent/runtime/session/queue_reorder_regression_test.go create mode 100644 internal/agent/runtime/session/reservation_fixture_test.go create mode 100644 internal/agent/runtime/session/run_view_codec.go create mode 100644 internal/agent/runtime/session/run_view_codec_test.go create mode 100644 internal/contextview/steer_continuation_test.go create mode 100644 internal/testutil/sessionledger/ledger.go create mode 100644 internal/testutil/sessionruntime/runtime.go diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index c1bb6ffde9..cb325fb044 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -174,3 +174,5 @@ jobs: -run '^TestPostgresRuntimeFence' ./internal/chat/message TEST_POSTGRES_BOOTSTRAP_SCHEMA=0 go test -v -race -count=1 -timeout 90s \ -run '^TestPostgresRuntimeFence' ./internal/agent/runtime/acp + TEST_POSTGRES_BOOTSTRAP_SCHEMA=0 go test -v -race -count=1 -timeout 90s \ + ./internal/agent/runtime/session/ledger diff --git a/apps/web/src/composables/api/useChat.types.ts b/apps/web/src/composables/api/useChat.types.ts index b74648c83e..8385374fc6 100644 --- a/apps/web/src/composables/api/useChat.types.ts +++ b/apps/web/src/composables/api/useChat.types.ts @@ -409,14 +409,6 @@ export interface RuntimeCursor { seq: number } -export interface RuntimeSteerState { - id: string - status: string - text?: string - error?: string - created_at: string - updated_at: string -} export interface RuntimeRunOperation { kind: 'retry' | 'edit' @@ -449,7 +441,6 @@ export interface RuntimeCurrentRunView { error?: string proposed_terminal_status?: RuntimeRunStatus finish_proposed_at?: string - steer?: RuntimeSteerState operation?: RuntimeRunOperation } @@ -476,7 +467,6 @@ export interface RuntimeCurrentRunPatch { status?: RuntimeRunStatus error_code?: string error?: string - steer?: RuntimeSteerState updated_at?: string owner_lease_expires_at?: string } diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 909d16c76b..3bbc6e1638 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -112,6 +112,9 @@ "finish": "Done" }, "errors": { + "queue": { + "steer_unsupported": "This run cannot accept steer input. Wait for it to finish and send a new message." + }, "bot": { "name_taken": "This name is already taken." }, diff --git a/apps/web/src/i18n/locales/ja.json b/apps/web/src/i18n/locales/ja.json index 1239dc7885..0ae405ee4a 100644 --- a/apps/web/src/i18n/locales/ja.json +++ b/apps/web/src/i18n/locales/ja.json @@ -109,6 +109,9 @@ "finish": "完了" }, "errors": { + "queue": { + "steer_unsupported": "現在の実行にはメッセージを追加できません。終了してから新しいメッセージを送信してください。" + }, "bot": { "name_taken": "この名前はすでに使用されています。" }, diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index afcc18b6c9..ce5ab52804 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -112,6 +112,9 @@ "finish": "完成" }, "errors": { + "queue": { + "steer_unsupported": "当前运行不支持插入消息,请等它结束后再发送。" + }, "bot": { "name_taken": "该名称已被占用。" }, diff --git a/apps/web/src/pages/home/components/session-follow-up-queue-item.test.ts b/apps/web/src/pages/home/components/session-follow-up-queue-item.test.ts index c94f13db01..f889503782 100644 --- a/apps/web/src/pages/home/components/session-follow-up-queue-item.test.ts +++ b/apps/web/src/pages/home/components/session-follow-up-queue-item.test.ts @@ -1,13 +1,13 @@ // @vitest-environment jsdom -import { createApp, nextTick, defineComponent, h } from 'vue' +import { createApp, nextTick, defineComponent, h, type VNode } from 'vue' import { afterEach, describe, expect, it, vi } from 'vitest' const uiStubs = vi.hoisted(() => ({ ButtonStub: { name: 'UiButtonStub', inheritAttrs: false, - setup(_: unknown, context: { attrs: Record; slots: { default?: () => unknown } }) { + setup(_: unknown, context: { attrs: Record; slots: { default?: () => VNode[] } }) { return () => h('button', context.attrs, context.slots.default?.()) }, }, diff --git a/apps/web/src/pages/home/components/session-follow-up-queue-item.vue b/apps/web/src/pages/home/components/session-follow-up-queue-item.vue index 6944c5d00a..ad48befb40 100644 --- a/apps/web/src/pages/home/components/session-follow-up-queue-item.vue +++ b/apps/web/src/pages/home/components/session-follow-up-queue-item.vue @@ -26,7 +26,7 @@ :title="$t('chat.queue.enqueueSteer')" :aria-label="$t('chat.queue.enqueueSteer')" class="size-7 shrink-0 text-muted-foreground" - :disabled="busy" + :disabled="busy || steerSupported === false" @pointerdown.prevent @click="emit('steer')" > @@ -67,6 +67,7 @@ import type { EditableFollowUpQueueItem } from './use-session-follow-up-queue' const props = defineProps<{ item: EditableFollowUpQueueItem busy: boolean + steerSupported?: boolean }>() const draft = ref(props.item.text) watch(() => props.item.text, value => { draft.value = value }) diff --git a/apps/web/src/pages/home/components/session-follow-up-queue.vue b/apps/web/src/pages/home/components/session-follow-up-queue.vue index 25b81c7ecb..cc4e5276b6 100644 --- a/apps/web/src/pages/home/components/session-follow-up-queue.vue +++ b/apps/web/src/pages/home/components/session-follow-up-queue.vue @@ -13,6 +13,7 @@ :key="item.item_id" :item="item" :busy="isBusy(item)" + :steer-supported="steerSupported" @save="save(item, $event)" @steer="steer(item)" @remove="remove(item)" @@ -43,7 +44,7 @@ const props = defineProps<{ }>() const { t } = useI18n() -const { items, hasItems, busy, refresh, update, remove: removeItem, steer: steerItem, reorder } = useSessionFollowUpQueue( +const { items, hasItems, steerSupported, busy, refresh, update, remove: removeItem, steer: steerItem, reorder } = useSessionFollowUpQueue( () => props.botId, () => props.sessionId, () => props.active ?? false, diff --git a/apps/web/src/pages/home/components/use-session-follow-up-queue.ts b/apps/web/src/pages/home/components/use-session-follow-up-queue.ts index f32df3c102..53cfaf41b7 100644 --- a/apps/web/src/pages/home/components/use-session-follow-up-queue.ts +++ b/apps/web/src/pages/home/components/use-session-follow-up-queue.ts @@ -41,6 +41,7 @@ export function useSessionFollowUpQueue( ) { const items = ref([]) const loading = ref(false) + const steerSupported = ref(false) const busy = ref(new Set()) const hasItems = computed(() => items.value.length > 0) let requestVersion = 0 @@ -86,6 +87,7 @@ export function useSessionFollowUpQueue( const response = await fetchSessionQueues(bot, session) if (version === requestVersion) { items.value = merge(response.steer ?? [], response.follow_up ?? []) + steerSupported.value = response.steer_supported === true } } finally { if (version === requestVersion) loading.value = false @@ -198,5 +200,5 @@ export function useSessionFollowUpQueue( watch([() => toValue(active), hasItems], syncAutoRefresh) if (getCurrentScope()) onScopeDispose(stopAutoRefresh) - return { items, loading, busy, hasItems, refresh, update, remove, steer, reorder } + return { items, loading, steerSupported, busy, hasItems, refresh, update, remove, steer, reorder } } diff --git a/apps/web/src/pages/home/composables/useChatScroll.test.ts b/apps/web/src/pages/home/composables/useChatScroll.test.ts index 135a4953fd..3af60af0be 100644 --- a/apps/web/src/pages/home/composables/useChatScroll.test.ts +++ b/apps/web/src/pages/home/composables/useChatScroll.test.ts @@ -40,7 +40,10 @@ class ResizeObserverMock { readonly unobserve = vi.fn() readonly disconnect = vi.fn() - constructor(private readonly callback: ResizeObserverCallback) { + private readonly callback: ResizeObserverCallback + + constructor(callback: ResizeObserverCallback) { + this.callback = callback ResizeObserverMock.instances.push(this) } diff --git a/apps/web/src/store/chat-list.test.ts b/apps/web/src/store/chat-list.test.ts index 6a97aa171c..35cd47d2c5 100644 --- a/apps/web/src/store/chat-list.test.ts +++ b/apps/web/src/store/chat-list.test.ts @@ -881,7 +881,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageDefaultExternalAgentSession({ agentId: 'codex', projectPath: '/data', projectMode: 'project' }) + store.stageDefaultExternalAgentSession({ runtime: 'acp', agentId: 'codex', projectPath: '/data', projectMode: 'project' }) const onBeforeMessageSend = vi.fn() const result = await store.sendMessage('/new', undefined, { onBeforeMessageSend }) expect(onBeforeMessageSend).not.toHaveBeenCalled() @@ -1045,7 +1045,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageDefaultExternalAgentSession({ agentId: 'codex', projectPath: '/data', projectMode: 'project' }) + store.stageDefaultExternalAgentSession({ runtime: 'acp', agentId: 'codex', projectPath: '/data', projectMode: 'project' }) store.resetToEmptyComposer({ explicitSelection: true }) api.fetchSessions.mockResolvedValueOnce({ @@ -1098,8 +1098,8 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageDefaultExternalAgentSession({ agentId: 'codex', projectPath: '/data', projectMode: 'project' }) - store.stageExternalAgentSession({ agentId: 'claude-code', projectPath: '/data/other', projectMode: 'project' }) + store.stageDefaultExternalAgentSession({ runtime: 'acp', agentId: 'codex', projectPath: '/data', projectMode: 'project' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude-code', projectPath: '/data/other', projectMode: 'project' }) api.fetchSessions.mockResolvedValueOnce({ items: [{ @@ -1139,7 +1139,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'custom-agent' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'custom-agent' }) await store.ensurePendingACPRuntime() // The runtime ID is server generated; the client never invents one. @@ -1187,7 +1187,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() api.fetchACPRuntimeByID.mockResolvedValueOnce({ @@ -1230,7 +1230,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() const recreated = await store.ensurePendingACPRuntime() @@ -1255,12 +1255,12 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) const first = store.ensurePendingACPRuntime() // Switching agents mid-create must NOT reuse the codex create promise: // the new staging starts its own runtime immediately. - store.stageExternalAgentSession({ agentId: 'claude-code' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude-code' }) const second = await store.ensurePendingACPRuntime() expect(api.createACPRuntime).toHaveBeenCalledTimes(2) @@ -1297,10 +1297,10 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) const first = store.ensurePendingACPRuntime() - store.stageExternalAgentSession({ agentId: 'codex', projectPath: '/data/other' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex', projectPath: '/data/other' }) await store.ensurePendingACPRuntime() expect(api.createACPRuntime).toHaveBeenCalledTimes(2) @@ -1336,10 +1336,10 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) const first = store.ensurePendingACPRuntime() - store.stageExternalAgentSession({ agentId: 'claude-code' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude-code' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_claude') @@ -1369,13 +1369,13 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_warm') // The model PATCH hangs; the user switches agents meanwhile. const pick = store.setPendingACPModel('gpt-5.1-codex-high') - store.stageExternalAgentSession({ agentId: 'claude-code' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude-code' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_claude') @@ -1411,7 +1411,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() // ABA: pick hangs → user leaves ACP → re-stages the SAME agent. The @@ -1419,7 +1419,7 @@ describe('chat-list store', () => { // late heal must not push the abandoned model onto the new runtime. const pick = store.setPendingACPModel('gpt-5.1-codex-high') store.clearPendingExternalAgentSession() - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_new') @@ -1435,7 +1435,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await expect(store.setPendingACPModel('gpt-5.1-codex-high')).rejects.toMatchObject({ message: 'runtime create failed', @@ -1468,7 +1468,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_warm') @@ -1489,7 +1489,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) const ensurePromise = store.ensurePendingACPRuntime() // The user clears the staged agent while the runtime is still starting. @@ -1851,7 +1851,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) await store.ensurePendingACPRuntime() expect(store.pendingACPRuntimeId).toBe('rt_warm') @@ -3274,7 +3274,7 @@ describe('chat-list store', () => { const store = useChatStore() await store.selectBot('bot-1') - store.stageExternalAgentSession({ agentId: 'codex' }) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }) const result = await store.sendMessage('/help', undefined, { composerScope: 'bot-1:draft-a', }) @@ -4776,10 +4776,10 @@ describe('chat-list store', () => { store.bindChatView(targetB.viewId, targetB, true) store.focusChatView(targetA.viewId) - store.stageExternalAgentSession({ agentId: 'codex' }, {}, targetA) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'codex' }, {}, targetA) await store.ensurePendingACPRuntime(targetA) store.focusChatView(targetB.viewId) - store.stageExternalAgentSession({ agentId: 'claude' }, {}, targetB) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude' }, {}, targetB) expect(store.pendingExternalAgentStateFor(targetA)).toMatchObject({ metadata: { acp_agent_id: 'codex' }, @@ -4872,13 +4872,13 @@ describe('chat-list store', () => { store.bindChatView(targetA.viewId, targetA, true) store.bindChatView(targetB.viewId, targetB, true) store.focusChatView(targetA.viewId) - store.stageExternalAgentSession({ agentId: 'custom-agent' }, {}, targetA) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'custom-agent' }, {}, targetA) const sending = store.sendMessage('from ACP A', undefined, { target: targetA }) await flushPromises() store.focusChatView(targetB.viewId) store.selectDraft({ explicitSelection: true }) - store.stageExternalAgentSession({ agentId: 'claude' }, {}, targetB) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude' }, {}, targetB) creation.reject(new Error('create failed')) await expect(sending).resolves.toMatchObject({ ok: false, stage: 'startup' }) @@ -4897,7 +4897,7 @@ describe('chat-list store', () => { store.bindChatView(targetA.viewId, targetA, true) store.bindChatView(targetB.viewId, targetB, true) store.focusChatView(targetA.viewId) - store.stageExternalAgentSession({ agentId: 'custom-agent' }, {}, targetA) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'custom-agent' }, {}, targetA) await store.ensurePendingACPRuntime(targetA) store.focusChatView(targetB.viewId) store.selectDraft({ explicitSelection: true }) @@ -4978,7 +4978,7 @@ describe('chat-list store', () => { const updating = store.updateCurrentSessionAgent({ agentId: 'custom-agent' }, targetA) store.focusChatView(targetB.viewId) store.selectDraft({ explicitSelection: true }) - store.stageExternalAgentSession({ agentId: 'claude' }, {}, targetB) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude' }, {}, targetB) await store.ensurePendingACPRuntime(targetB) update.resolve({ id: 'session-a', @@ -5018,7 +5018,7 @@ describe('chat-list store', () => { await flushPromises() store.focusChatView(targetB.viewId) store.selectDraft({ explicitSelection: true }) - store.stageExternalAgentSession({ agentId: 'claude' }, {}, targetB) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude' }, {}, targetB) settings.resolve({ data: { chat_runtime: 'codex', chat_acp_agent_id: '', @@ -5138,7 +5138,7 @@ describe('chat-list store', () => { const command = store.sendMessage('/new codex', undefined, { target }) await flushPromises() - store.stageExternalAgentSession({ agentId: 'claude' }, {}, target) + store.stageExternalAgentSession({ runtime: 'acp', agentId: 'claude' }, {}, target) await store.ensurePendingACPRuntime(target) settings.resolve({ data: { diff --git a/apps/web/src/store/chat/external-agent-defaults.ts b/apps/web/src/store/chat/external-agent-defaults.ts index 874ee0e67f..3fbf11df3a 100644 --- a/apps/web/src/store/chat/external-agent-defaults.ts +++ b/apps/web/src/store/chat/external-agent-defaults.ts @@ -79,7 +79,8 @@ export function createExternalAgentDefaults(deps: { deps.rememberDefault(bid, null) return null } - const input = { + const input: ExternalAgentSessionInput = { + runtime: runtime === 'codex' || runtime === 'claude-code' ? runtime : 'acp', botAgentId: settings.default_bot_agent_id?.trim() || undefined, agentId, projectPath: settings.chat_acp_project_path?.trim() diff --git a/apps/web/src/store/chat/external-agent-input.test.ts b/apps/web/src/store/chat/external-agent-input.test.ts new file mode 100644 index 0000000000..0fdb556b15 --- /dev/null +++ b/apps/web/src/store/chat/external-agent-input.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { normalizedExternalAgentInput, sameExternalAgentSessionInput } from './external-agent-staging' + +describe('External Agent input normalization', () => { + it('gives a default Direct Agent and an explicit selection the same identity', () => { + const fromDefault = normalizedExternalAgentInput({ agentId: 'codex', botAgentId: 'agent-1' }) + expect(fromDefault.runtime).toBe('codex') + expect(sameExternalAgentSessionInput(fromDefault, { + agentId: 'codex', botAgentId: 'agent-1', runtime: 'codex', + })).toBe(true) + }) + + it('keeps an explicit custom ACP runtime distinct from a Direct runtime', () => { + const custom = { agentId: 'codex', runtime: 'acp' as const } + expect(normalizedExternalAgentInput(custom).runtime).toBe('acp') + expect(sameExternalAgentSessionInput(custom, { agentId: 'codex', runtime: 'codex' })).toBe(false) + }) + + it('normalizes project defaults once without mutating the caller', () => { + const input = { agentId: ' custom ', projectPath: ' ', projectMode: ' ' } + expect(normalizedExternalAgentInput(input)).toMatchObject({ + agentId: 'custom', runtime: 'acp', projectPath: '/data', projectMode: 'project', + }) + expect(input.agentId).toBe(' custom ') + }) +}) diff --git a/apps/web/src/store/chat/external-agent-sessions.ts b/apps/web/src/store/chat/external-agent-sessions.ts index 39ba9c6e14..8bc37d11a8 100644 --- a/apps/web/src/store/chat/external-agent-sessions.ts +++ b/apps/web/src/store/chat/external-agent-sessions.ts @@ -5,9 +5,8 @@ import { updateSessionAgent, type SessionSummary, } from '@/composables/api/useChat' -import { BOT_AGENT_RUNTIME_ACP, BOT_AGENT_RUNTIME_CLAUDE_CODE, BOT_AGENT_RUNTIME_CODEX, botAgentRuntimeForProvider } from '@/utils/bot-agent' import { provisionalSessionTitle } from '../chat-list.utils' -import { externalAgentDraftMetadata } from './external-agent-staging' +import { externalAgentDraftMetadata, normalizedExternalAgentInput } from './external-agent-staging' import { StreamFailureError } from './send' import type { ExternalAgentSessionInput, ChatViewTarget } from './types' @@ -53,32 +52,9 @@ export interface ExternalAgentSessionDeps { draftWorkdirIdFor: (botId: string, opts: { externalAgent: boolean }) => string } -function normalizedExternalAgentInput(input: ExternalAgentSessionInput): ExternalAgentSessionInput { - const metadata = externalAgentDraftMetadata(input) - return { - ...input, - agentId: String(metadata.acp_agent_id ?? ''), - projectPath: String(metadata.project_path ?? ''), - projectMode: String(metadata.acp_project_mode ?? ''), - } -} - function agentSessionRuntimeType(input: ExternalAgentSessionInput): string { - switch (input.runtime) { - case BOT_AGENT_RUNTIME_CODEX: - case BOT_AGENT_RUNTIME_CLAUDE_CODE: - return input.runtime - case BOT_AGENT_RUNTIME_ACP: - return 'acp_agent' - default: - break - } - // Legacy callers (slash commands, cached defaults) carry only agentId; - // direct external agents are addressed by their runtime name, so derive - // it here instead of silently creating an acp_agent session the server - // will refuse. - const derived = botAgentRuntimeForProvider(input.agentId) - return derived === BOT_AGENT_RUNTIME_ACP ? 'acp_agent' : derived + const runtime = normalizedExternalAgentInput(input).runtime + return runtime === 'acp' ? 'acp_agent' : runtime } function externalAgentSessionMetadata(input: ExternalAgentSessionInput): Record { diff --git a/apps/web/src/store/chat/external-agent-staging.ts b/apps/web/src/store/chat/external-agent-staging.ts index 3f4f348970..110ef47022 100644 --- a/apps/web/src/store/chat/external-agent-staging.ts +++ b/apps/web/src/store/chat/external-agent-staging.ts @@ -9,6 +9,7 @@ import { setACPRuntimeReasoningByID as requestSetACPRuntimeReasoningByID, } from '@/composables/api/useChat' import { ACP_DEFAULT_PROJECT_MODE, ACP_DEFAULT_PROJECT_PATH } from '@/utils/acp' +import { botAgentRuntimeForProvider, normalizeBotAgentRuntime, type BotAgentRuntime } from '@/utils/bot-agent' import { isApiErrorCode } from '@/utils/api-error' import type { ACPRuntimeStatusRegistry } from './acp-runtime-registry' import type { ExternalAgentSessionInput } from './types' @@ -37,6 +38,19 @@ export interface DetachedExternalAgentSession { botId: string } +// Normalize legacy input objects once before staging or session creation. +// Internal draft identity must not infer a different runtime from the writer. +export function normalizedExternalAgentInput(input: ExternalAgentSessionInput): ExternalAgentSessionInput & { runtime: BotAgentRuntime } { + return { + ...input, + runtime: normalizeBotAgentRuntime(input.runtime) || botAgentRuntimeForProvider(input.agentId), + botAgentId: input.botAgentId?.trim() || undefined, + agentId: input.agentId.trim(), + projectPath: input.projectPath?.trim() || ACP_DEFAULT_PROJECT_PATH, + projectMode: input.projectMode?.trim() || ACP_DEFAULT_PROJECT_MODE, + } +} + export function externalAgentDraftMetadata(input: ExternalAgentSessionInput): Record { const agentId = input.agentId.trim() const projectMode = input.projectMode?.trim() || ACP_DEFAULT_PROJECT_MODE @@ -58,7 +72,7 @@ export function sameExternalAgentSessionInput(a: ExternalAgentSessionInput, b: E const right = externalAgentDraftMetadata(b) return left.acp_agent_id === right.acp_agent_id && (a.botAgentId?.trim() ?? '') === (b.botAgentId?.trim() ?? '') - && (a.runtime || 'acp') === (b.runtime || 'acp') + && normalizedExternalAgentInput(a).runtime === normalizedExternalAgentInput(b).runtime && (a.sessionMode || 'chat') === (b.sessionMode || 'chat') && left.project_path === right.project_path && left.acp_project_mode === right.acp_project_mode @@ -120,7 +134,7 @@ export function createExternalAgentStaging(deps: ExternalAgentStagingDeps) { const pendingACPRuntimeEnsuring = computed(() => pendingACPCreating.value) function cloneExternalAgentInput(input: ExternalAgentSessionInput): ExternalAgentSessionInput { - return { ...input } + return normalizedExternalAgentInput(input) } function rememberDefaultExternalAgentInput(botId: string, input: ExternalAgentSessionInput | null) { @@ -145,6 +159,7 @@ export function createExternalAgentStaging(deps: ExternalAgentStagingDeps) { } function pendingExternalAgentIdentityKey(botId: string, input: ExternalAgentSessionInput): string { + input = normalizedExternalAgentInput(input) return [botId, input.sessionMode ?? 'chat', input.botAgentId ?? '', input.runtime ?? 'acp', input.agentId, input.projectPath ?? '', input.projectMode ?? ''].join('\u0000') } @@ -193,7 +208,7 @@ export function createExternalAgentStaging(deps: ExternalAgentStagingDeps) { function stageExternalAgentSession(input: ExternalAgentSessionInput, options: { explicitSelection?: boolean } = {}) { const ownerBotId = (currentBotId.value ?? '').trim() - const metadata = externalAgentDraftMetadata(input) + input = normalizedExternalAgentInput(input) const existing = pendingExternalAgentSessionInput.value const samePendingAgent = Boolean(existing && pendingExternalAgentBotId.value === ownerBotId @@ -205,13 +220,7 @@ export function createExternalAgentStaging(deps: ExternalAgentStagingDeps) { } const previousOwnerBotId = pendingExternalAgentBotId.value pendingExternalAgentBotId.value = ownerBotId - pendingExternalAgentSessionInput.value = { - ...input, - botAgentId: input.botAgentId?.trim() || undefined, - agentId: String(metadata.acp_agent_id ?? ''), - projectPath: String(metadata.project_path ?? ''), - projectMode: String(metadata.acp_project_mode ?? ''), - } + pendingExternalAgentSessionInput.value = input if (!samePendingAgent && pendingACPRuntimeId.value) { const bid = previousOwnerBotId const runtimeId = pendingACPRuntimeId.value diff --git a/apps/web/src/store/chat/runtime-projection.ts b/apps/web/src/store/chat/runtime-projection.ts index 2ae8b71a77..6f546e4abf 100644 --- a/apps/web/src/store/chat/runtime-projection.ts +++ b/apps/web/src/store/chat/runtime-projection.ts @@ -95,7 +95,6 @@ function cloneRunView(run: RuntimeCurrentRunView): RuntimeCurrentRunView { forward: run.request_user_turn.forward ? { ...run.request_user_turn.forward } : undefined, } : undefined, - steer: run.steer ? { ...run.steer } : undefined, operation: run.operation ? { ...run.operation, @@ -120,16 +119,19 @@ function emptyTranscript(): RuntimeTranscriptSlice { } } +// Older snapshots carry a single request/replacement turn. Keep this wire +// adaptation shared by full projection and incremental delta application. +function userTurnsForRun(run: RuntimeCurrentRunView) { + if (run.user_turns?.length) return run.user_turns + const fallback = run.request_user_turn ?? run.operation?.replacement_user_turn + return fallback ? [{ ...fallback }] : [] +} + function transcriptForRun(run: RuntimeCurrentRunView | null): RuntimeTranscriptSlice { if (!run) return emptyTranscript() const turnId = run.turn_id.trim() const turns: UITurn[] = [] - const fallbackUserTurn = run.request_user_turn ?? run.operation?.replacement_user_turn - const userTurns = run.user_turns?.length - ? run.user_turns - : fallbackUserTurn - ? [fallbackUserTurn] - : [] + const userTurns = userTurnsForRun(run) const active = isRuntimeRunActive(run.status) const steerTurns = [...(run.steer_turns ?? [])] .filter(steer => steer.status === 'applied' || active) @@ -262,7 +264,6 @@ function applyRunPatch( ...(patch.status !== undefined ? { status: patch.status } : {}), ...(patch.error_code !== undefined ? { error_code: patch.error_code } : {}), ...(patch.error !== undefined ? { error: patch.error } : {}), - ...(patch.steer !== undefined ? { steer: { ...patch.steer } } : {}), ...(patch.updated_at !== undefined ? { updated_at: patch.updated_at } : {}), ...(patch.owner_lease_expires_at !== undefined ? { owner_lease_expires_at: patch.owner_lease_expires_at } @@ -271,12 +272,7 @@ function applyRunPatch( } const messages = delta.reset_messages ? [] : next.messages - const fallbackUserTurn = next.request_user_turn ?? next.operation?.replacement_user_turn - const userTurns = next.user_turns?.length - ? [...next.user_turns] - : fallbackUserTurn - ? [{ ...fallbackUserTurn }] - : [] + const userTurns = [...userTurnsForRun(next)] const steerTurns = [...(next.steer_turns ?? [])] for (const incoming of delta.user_turn_upserts ?? []) { const turnId = incoming.turn_id.trim() diff --git a/docs/design/session-runtime-requirements.md b/docs/design/session-runtime-requirements.md index 83d77a49cd..e301ac82e7 100644 --- a/docs/design/session-runtime-requirements.md +++ b/docs/design/session-runtime-requirements.md @@ -167,11 +167,15 @@ run 进入终态时,终态、最终输出和 turn 投影必须形成一个一 - owner 进程退出、租约到期、live backend 更换或优雅关机时,reaper/关闭流程必须把提案收敛为其原始 `completed`、`aborted` 或 `failed`,不能改写为 `lost`; - 只有从未跨过终态提案边界的 active run 才能因 owner 消失进入 `lost`。 -ledger 成为终态后,系统必须以该 durable outcome 修复可能滞后的 live 投影;修复必须校验 live run ref 中保存的同一个 fencing token,不能覆盖后继 run。 +ledger 成为终态后,系统必须以该 durable outcome 修复可能滞后的 live 投影;修复必须原子校验 live snapshot 中保存的同一个 fencing token(旧 snapshot 仅在 lease ref 尚存时可使用其中的 token),不能覆盖后继 run。 ### SR-TURN-001:turn 必须是显式身份 -每个已准入 run 必须显式关联一个服务端生成的 `turn_id`。用户消息、Agent 输出、工具事件和决策都通过该身份归属到同一个 turn。 +每个已准入 run 必须显式关联一个服务端生成的起始 `turn_id`。普通 run 的用户消息与回答归于该 turn。已应用的 steer 是同一 run 中新的用户输入,可以打开新的 canonical turn;该输入之后的 Agent 输出和工具消息归于新 turn,所有这些 turn 通过显式 `run_id` 关联,不能重新准入第二个 run。 + +run 控制记录及其 decision 的 `turn_id` 保持起始 turn 身份,用于 owner/fence 与决策恢复校验;聊天消息的 `turn_id` 表示该消息所属的 canonical turn。客户端提交决策以 `decision_id` 和 run 身份为准,不能把临时渲染 ID 或某个显示分段的 ID 当作 run 控制身份。steer 的用户消息必须进入实际 provider 请求,并与对应完整 step 一起持久化,不能只在实时投影中展示。 + +决策停等后,同一 owner 的续跑必须接续其已消费的 step 游标;owner 更换后游标可随 generation 重建。该游标仅服务进程内排序与投影屏障,不宣称跨进程模型采样重放。 实现不能根据以下字段是否相似来决定两条消息属于同一 turn: @@ -242,7 +246,9 @@ Redis 或 Valkey 不应成为单实例 OSS 部署的强制依赖。 | terminal proposal crash | 故障注入/Server 重启 | `finishing` 在 owner 消失后收敛到原提案,不能变成 `lost` | SR-DUR-002、SR-OWN-002 | | decision restart | Server 重启 | decision 可恢复,回答只消费一次 | SR-DEC-001 | -当前黑盒验收代码覆盖 `baseline`、`reconnect snapshot`、`reconnect abort`、`duplicate invocation`、`same-session concurrency` 和 `owner crash`。包内测试使用故障注入覆盖终态提案重试、owner 租约到期、live backend 更换和优雅关机;`concurrent subscribers`、完整进程级 `terminal proposal crash` 与 `decision restart` 仍需补充黑盒用例。 +当前黑盒代码包含基础执行、重连、多个订阅者、控制/决策重放、重复准入、busy、owner crash、decision restart、live backend loss,以及队列排序/连续消费和 steer 后决策续跑。`terminal proposal crash` 使用隔离数据库的单 invocation advisory-lock trigger,在已提交提案与最终状态更新之间阻塞,然后终止真实 owner 进程。 + +owner/decision/terminal-proposal crash 用例由 `MEMOH_SESSION_RUNTIME_ACCEPTANCE_CRASH` 显式启用;backend-loss 用例另有开关。用例存在、编译通过或因未启用而跳过,不代表该验收已执行通过。 ## 7. 通过标准 diff --git a/internal/agent/adapter/channelqueue/adapter.go b/internal/agent/adapter/channelqueue/adapter.go index bc2876f945..8e51b40135 100644 --- a/internal/agent/adapter/channelqueue/adapter.go +++ b/internal/agent/adapter/channelqueue/adapter.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/felinics/memoh/internal/agent/application" - "github.com/felinics/memoh/internal/agent/runtime/session/queue" + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/channel/inbound" ) @@ -50,15 +50,17 @@ func mapAdmissionError(err error) error { switch { case err == nil: return nil - case errors.Is(err, queue.ErrNoActiveRun): + case errors.Is(err, sessionruntime.ErrQueueSteerUnsupported): + return inbound.NewQueueCommandError(inbound.QueueCommandCodeUnsupported) + case errors.Is(err, sessionruntime.ErrQueueNoActiveRun): return inbound.NewQueueCommandError(inbound.QueueCommandCodeNoActiveRun) - case errors.Is(err, queue.ErrInvocationConflict): + case errors.Is(err, sessionruntime.ErrQueueInvocationConflict): return inbound.NewQueueCommandError(inbound.QueueCommandCodeConflict) - case errors.Is(err, queue.ErrAdmissionOverloaded): + case errors.Is(err, sessionruntime.ErrQueueAdmissionOverloaded): return inbound.NewQueueCommandError(inbound.QueueCommandCodeOverloaded) - case errors.Is(err, queue.ErrCapacityExceeded): + case errors.Is(err, sessionruntime.ErrQueueCapacityExceeded): return inbound.NewQueueCommandError(inbound.QueueCommandCodeCapacity) - case errors.Is(err, queue.ErrInvalidReference): + case errors.Is(err, sessionruntime.ErrQueueInvalidReference): return inbound.NewQueueCommandError(inbound.QueueCommandCodeInvalid) default: return err diff --git a/internal/agent/application/contract.go b/internal/agent/application/contract.go index 3e114448fe..56c2f0734d 100644 --- a/internal/agent/application/contract.go +++ b/internal/agent/application/contract.go @@ -4,7 +4,6 @@ import ( "encoding/json" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - sessionqueue "github.com/felinics/memoh/internal/agent/runtime/session/queue" "github.com/felinics/memoh/internal/agent/turn" messagepkg "github.com/felinics/memoh/internal/chat/message" ) @@ -93,10 +92,8 @@ type ChatRequest struct { InjectCh <-chan turn.InjectMessage `json:"-"` // QueueInjectCh is the execution-owned sender paired with InjectCh. Only the // durable step coordinator uses it after claiming a steer item. - QueueInjectCh chan<- turn.InjectMessage `json:"-"` - // QueueSteerClaim is a live-runtime claim to re-inject after owner recovery. - QueueSteerClaim *sessionqueue.SteerClaimRef `json:"-"` - StepIndexOffset int `json:"-"` + QueueInjectCh chan<- turn.InjectMessage `json:"-"` + StepIndexOffset int `json:"-"` // PublishRuntimeEvents is set for server-owned continuations, which do not // have a client runHandle pump to publish native events into the session // runtime projection. diff --git a/internal/agent/application/decision_output_test.go b/internal/agent/application/decision_output_test.go index 0dbfe231df..759284af4d 100644 --- a/internal/agent/application/decision_output_test.go +++ b/internal/agent/application/decision_output_test.go @@ -11,7 +11,7 @@ import ( ) type decisionOutputBackend struct { - sessionruntime.Backend + *sessionruntime.MemoryBackend output []struct { Type string Output json.RawMessage @@ -19,7 +19,7 @@ type decisionOutputBackend struct { } func (b *decisionOutputBackend) AppendDecisionOutput(ctx context.Context, ref sessionruntime.DecisionOutputRef, seq int64, payload json.RawMessage, limits sessionruntime.DecisionOutputLimits) (sessionruntime.DecisionOutputState, error) { - state, err := b.Backend.AppendDecisionOutput(ctx, ref, seq, payload, limits) + state, err := b.MemoryBackend.AppendDecisionOutput(ctx, ref, seq, payload, limits) if err == nil && state.Applied { entry := struct { Type string @@ -36,7 +36,7 @@ func (b *decisionOutputBackend) AppendDecisionOutput(ctx context.Context, ref se func TestContinuationPublishesTextAndNextQuestionToChannel(t *testing.T) { for _, nextQuestion := range []bool{false, true} { t.Run(map[bool]string{false: "ordinary reply", true: "second question"}[nextQuestion], func(t *testing.T) { - backend := &decisionOutputBackend{Backend: sessionruntime.NewMemoryBackend()} + backend := &decisionOutputBackend{MemoryBackend: sessionruntime.NewMemoryBackend()} manager, handle := newWaitingDecisionRuntime(t, backend) service := &Service{decisionRuntime: manager} events := []native.StreamEvent{{Type: native.EventAgentStart}, {Type: native.EventTextDelta, Delta: "收到你的答案"}} @@ -72,13 +72,13 @@ func TestContinuationPublishesTextAndNextQuestionToChannel(t *testing.T) { } } -type failedEndCheckpointBackend struct{ sessionruntime.Backend } +type failedEndCheckpointBackend struct{ *sessionruntime.MemoryBackend } func (b failedEndCheckpointBackend) AppendDecisionOutput(ctx context.Context, ref sessionruntime.DecisionOutputRef, seq int64, payload json.RawMessage, limits sessionruntime.DecisionOutputLimits) (sessionruntime.DecisionOutputState, error) { if payload == nil { return sessionruntime.DecisionOutputState{}, errors.New("checkpoint write unavailable") } - return b.Backend.AppendDecisionOutput(ctx, ref, seq, payload, limits) + return b.MemoryBackend.AppendDecisionOutput(ctx, ref, seq, payload, limits) } func TestContinuationClosesRunWhenEndCheckpointCannotPersist(t *testing.T) { diff --git a/internal/agent/application/native_decision_continuation.go b/internal/agent/application/native_decision_continuation.go new file mode 100644 index 0000000000..68e7f8cea5 --- /dev/null +++ b/internal/agent/application/native_decision_continuation.go @@ -0,0 +1,172 @@ +package application + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + + "github.com/felinics/memoh/internal/agent/runtime/native" + "github.com/felinics/memoh/internal/models" +) + +// runNativeDecisionContinuation owns the model stream after a committed answer. +// Authorization and answer/tool-result persistence remain with each decision +// kind; cancellation, step persistence and terminal publication share one path. +func (s *Service) runNativeDecisionContinuation(ctx context.Context, req ChatRequest, cfg native.RunConfig, modelID string, runtimeLifecycle *continuationLifecycleResult, eventCh chan<- WSStreamEvent) error { + terminal := s.contextLifecycleTerminal(ctx, cfg) + var lifecycleCause error + var lifecycleDeferred bool + var terminalEventSeen bool + defer func() { + if runtimeLifecycle != nil { + runtimeLifecycle.cause = lifecycleCause + runtimeLifecycle.deferred = lifecycleDeferred + if snapshot, ok := cfg.ContextLifecycle.Snapshot(); ok { + runtimeLifecycle.snapshot = &snapshot + } + return + } + if !lifecycleDeferred { + terminal(lifecycleCause) + } + }() + + continuationRC := resolvedContext{runConfig: cfg, model: models.GetResponse{ID: modelID}} + stepCommitter, stopQueueBinding, err := s.bindQueueContinuation(ctx, &req, &cfg, continuationRC) + if err != nil { + return err + } + defer stopQueueBinding() + reasoningTiming := newReasoningTimingTracker(nil) + configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) + idleCtx, idleCancel := s.withStreamIdleTimeout(ctx, reasoningEffortForIdle(cfg)) + defer idleCancel.Stop() + stream := s.agent.Stream(idleCtx, cfg) + stored := false + failureEventForwarded := false + var hasVisibleOutput bool + for event := range stream { + idleCancel.Reset() + if event.Type == native.EventToolCallStart { + idleCancel.RecordToolCall() + } + if eventErr := agentStreamLifecycleError(event); eventErr != nil && lifecycleCause == nil { + lifecycleCause = eventErr + // The public event forwarded downstream carries only a stable code; + // keep the runtime's private detail in the server log so a failed + // continuation can be diagnosed. + s.logContinuationStreamError(req.RunID, event) + } + if event.IsTerminal() { + terminalEventSeen = true + lifecycleDeferred = pendingContinuationDecision(event) + if !lifecycleDeferred { + switch event.Type { + case native.EventAgentEnd: + lifecycleCause = nil + case native.EventAgentAbort: + if idleCancel.DidFire() { + lifecycleCause = context.Cause(idleCtx) + } else if context.Cause(ctx) != nil || lifecycleCause == nil { + lifecycleCause = agentAbortCause(ctx) + } + } + } + } + if hasVisibleAgentStreamOutput(event) { + hasVisibleOutput = true + } + if event.Type == native.EventAgentAbort && idleCancel.DidFire() && eventCh != nil { + if failureData, marshalErr := json.Marshal(agentFailureStreamEvent(context.Cause(idleCtx))); marshalErr == nil { + select { + case eventCh <- json.RawMessage(failureData): + failureEventForwarded = true + case <-ctx.Done(): + lifecycleCause = context.Cause(ctx) + return lifecycleCause + } + } + } + data, err := json.Marshal(publicAgentStreamEvent(event)) + if err != nil { + continue + } + if !stored && event.IsTerminal() && len(event.Messages) > 0 { + if snap, ok := extractTerminalSnapshot(data); ok { + if stepCommitter == nil { + snap.reasoningTiming = takeTerminalReasoningTiming(reasoningTiming, event.Type) + } + snap.visibleOutput = hasVisibleOutput + snap.failureCode = snapshotFailureCode(idleCancel.DidFire(), lifecycleCause) + lifecycleDeferred = lifecycleDeferred || snap.deferredToolID != "" + if snap.aborted && !lifecycleDeferred && lifecycleCause == nil { + lifecycleCause = agentAbortCause(ctx) + } + var storeErr error + if stepCommitter != nil { + storeErr = stepCommitter.finish(ctx, extractInputTokensFromUsage(snap.usage)) + } else { + storeErr = s.persistTerminalSnapshot( + context.WithoutCancel(ctx), + req, + resolvedContext{runConfig: cfg, model: models.GetResponse{ID: modelID}}, + snap, + ) + } + if storeErr != nil { + lifecycleCause = storeErr + lifecycleDeferred = false + return storeErr + } + stored = true + } + } + if eventCh != nil && shouldForwardAfterIdleFailure(event, failureEventForwarded) { + select { + case eventCh <- json.RawMessage(data): + case <-ctx.Done(): + lifecycleCause = context.Cause(ctx) + return lifecycleCause + } + } + } + if !stored && stepCommitter != nil { + if storeErr := stepCommitter.finish(ctx, 0); storeErr != nil { + lifecycleCause = storeErr + return storeErr + } + stored = true + } + if stepCommitter != nil { + if commitErr := stepCommitter.err(); commitErr != nil && ctx.Err() == nil { + lifecycleCause = commitErr + return commitErr + } + } + if idleCancel.DidFire() { + lifecycleCause = context.Cause(idleCtx) + if !stored { + if _, storeErr := s.persistTurnFailure(context.WithoutCancel(ctx), req, resolvedContext{runConfig: cfg, model: models.GetResponse{ID: modelID}}, snapshotFailureCode(true, lifecycleCause)); storeErr != nil { + s.logger.Error("decision continuation timeout persist failed", slog.Any("error", storeErr)) + } + } + if eventCh != nil && !failureEventForwarded { + if data, marshalErr := json.Marshal(agentFailureStreamEvent(lifecycleCause)); marshalErr == nil { + select { + case eventCh <- json.RawMessage(data): + case <-ctx.Done(): + } + } + } + return lifecycleCause + } + if ctx.Err() != nil { + lifecycleCause = context.Cause(ctx) + return lifecycleCause + } + if lifecycleCause == nil && !lifecycleDeferred && !terminalEventSeen { + lifecycleCause = errors.New("agent continuation ended without a terminal event") + } + return nil +} diff --git a/internal/agent/application/queue_continuation.go b/internal/agent/application/queue_continuation.go index f275a6fc15..d87f7556f2 100644 --- a/internal/agent/application/queue_continuation.go +++ b/internal/agent/application/queue_continuation.go @@ -6,6 +6,7 @@ import ( "errors" "log/slog" "strings" + "sync" "time" "github.com/google/uuid" @@ -34,17 +35,16 @@ func encodeFollowUpCommand(cmd turn.StartTurnCommand) ([]byte, error) { func decodeFollowUpPayload(payload []byte) followUpPayload { var body followUpPayload - if err := json.Unmarshal(payload, &body); err == nil { - body.Text = strings.TrimSpace(body.Text) - return body + if err := json.Unmarshal(payload, &body); err != nil { + return followUpPayload{} } - // Legacy plain-text payloads carry no JSON envelope. - return followUpPayload{Text: strings.TrimSpace(string(payload))} + body.Text = strings.TrimSpace(body.Text) + return body } -// continuationPayloadText renders the user-visible text of a steer or -// follow-up payload. -func continuationPayloadText(payload []byte) string { +// QueuePayloadText renders only user-visible text. Invalid/empty payloads never +// fall back to the raw envelope, which can contain a deferred command credential. +func QueuePayloadText(payload []byte) string { body := decodeFollowUpPayload(payload) if text := strings.TrimSpace(body.Text); text != "" { return text @@ -134,25 +134,63 @@ func (s *Service) startFollowUpAfterTerminal(ctx context.Context, terminal sessi go s.startFollowUp(ctx, terminal) } +// followUpStart coalesces terminal/enqueue notifications while admission is in +// flight. Its lifetime ends before output is drained; output delivery must not +// hold the next run's admission gate. +type followUpStart struct { + mu sync.Mutex + closed bool + pending *sessionruntime.TerminalRun +} + func (s *Service) startFollowUp(parent context.Context, terminal sessionruntime.TerminalRun) { ctx := context.WithoutCancel(parent) key := sessionruntime.Key{BotID: terminal.BotID, SessionID: terminal.SessionID} - // One starter per session at a time. A second trigger (terminal observer - // plus an enqueue-time kick) would otherwise receive the same idempotent - // claim and could release it while the first is still admitting the turn. - if _, busy := s.followUpStarts.LoadOrStore(key.String(), struct{}{}); busy { + state := &followUpStart{} + for { + current, busy := s.followUpStarts.LoadOrStore(key.String(), state) + if !busy { + break + } + active := current.(*followUpStart) + active.mu.Lock() + if active.closed { + active.mu.Unlock() + continue + } + active.pending = &terminal + active.mu.Unlock() + return + } + for { + if handle := s.admitFollowUp(ctx, key, terminal); handle != nil { + // Server-owned handles need a consumer, independently of the short + // admission loop. Terminal observers can schedule the next item. + go drainDeferredTurn(handle) + } + state.mu.Lock() + if state.pending != nil { + terminal = *state.pending + state.pending = nil + state.mu.Unlock() + continue + } + state.closed = true + s.followUpStarts.CompareAndDelete(key.String(), state) + state.mu.Unlock() return } - defer s.followUpStarts.Delete(key.String()) +} +func (s *Service) admitFollowUp(ctx context.Context, key sessionruntime.Key, terminal sessionruntime.TerminalRun) turn.RunHandle { item, claim, ok, err := s.sessionManager.ClaimNextFollowUp(ctx, key, terminal.RunID) if err != nil || !ok { - return + return nil } cmd, ok := s.followUpCommand(item) if !ok { _ = s.sessionManager.ReleaseFollowUp(ctx, key, claim) - return + return nil } var handle turn.RunHandle for attempt := 0; ; attempt++ { @@ -163,25 +201,23 @@ func (s *Service) startFollowUp(parent context.Context, terminal sessionruntime. // ctx is detached from its parent, so only the backoff bounds the wait. time.Sleep(time.Duration(1< 0 { + backend = backends[0] + } key := sessionruntime.Key{BotID: "bot", SessionID: "session"} _, _, err := backend.Update(context.Background(), key, func(snapshot sessionruntime.Snapshot, _ bool) (sessionruntime.Snapshot, bool, error) { snapshot.BotID, snapshot.SessionID = key.BotID, key.SessionID snapshot.CurrentRunView = &sessionruntime.CurrentRunView{ - RunID: "run-1", TurnID: "turn-1", Generation: "gen-1", OwnerID: "owner-1", Status: sessionruntime.RunStatusRunning, + RunID: "run-1", TurnID: "turn-1", Generation: "gen-1", SteerSupported: true, Status: sessionruntime.RunStatusRunning, } return snapshot, true, nil }) @@ -72,14 +74,14 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { // Original run: the deferred step commits without touching the queue. parkedInject := make(chan turn.InjectMessage, 16) - original := newQueueStepTransaction(service, ChatRequest{ + original := newQueueStepCoordinator(service, ChatRequest{ BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, RunHandle: handle, QueueInjectCh: parkedInject, - }, "model") + }) if original == nil { t.Fatal("queue step transaction unavailable") } - outcome, err := original.commit(ctx, 0, "", queueStepDeferredDecision, messagepkg.AgentStep{RunID: handle.RunID}, nil) + outcome, err := original.commit(ctx, queueStepDeferredDecision, messagepkg.AgentStep{RunID: handle.RunID}, nil) if err != nil { t.Fatalf("deferred commit: %v", err) } @@ -89,24 +91,24 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { if got := drainInject(parkedInject); len(got) != 0 { t.Fatalf("parked inject channel received %v", got) } - if steers, _, err := service.sessionManager.PendingQueues(ctx, key, 0); err != nil || len(steers) != 1 || steers[0].Status != sessionqueue.Accepted { + if steers, _, err := service.sessionManager.PendingQueues(ctx, key, 0); err != nil || len(steers) != 1 || steers[0].Status != sessionruntime.QueueAccepted { t.Fatalf("steer should stay accepted across the park: %#v, %v", steers, err) } - // Continuation after the decision: a fresh request, no QueueSteerClaim, + // Continuation after the decision: a fresh request, // a fresh inject channel. This mirrors continueToolApprovalSession. continuationInject := make(chan turn.InjectMessage, 16) - continuation := newQueueStepTransaction(service, ChatRequest{ + continuation := newQueueStepCoordinator(service, ChatRequest{ BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, RunHandle: handle, QueueInjectCh: continuationInject, UserMessagePersisted: true, - }, "model") + }) if continuation == nil { t.Fatal("continuation queue step transaction unavailable") } // Step N+1: the model call that consumed the approved tool result. Its // commit claims the steer for step N+2. - outcome, err = continuation.commit(ctx, 1, "", queueStepToolLoop, messagepkg.AgentStep{RunID: handle.RunID}, nil) + outcome, err = continuation.commit(ctx, queueStepToolLoop, messagepkg.AgentStep{RunID: handle.RunID}, nil) if err != nil { t.Fatalf("continuation commit: %v", err) } @@ -125,7 +127,7 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { } // Step N+2 saw the steer; its commit applies the claim exactly once. - outcome, err = continuation.commit(ctx, 2, "", queueStepFinal, messagepkg.AgentStep{RunID: handle.RunID}, nil) + outcome, err = continuation.commit(ctx, queueStepFinal, messagepkg.AgentStep{RunID: handle.RunID}, nil) if err != nil { t.Fatalf("final commit: %v", err) } @@ -135,7 +137,7 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { if _, err := service.sessionManager.UpdateSteer(ctx, key, item.ID, []byte("x")); err == nil { t.Fatal("applied steer still mutable") } - if _, err := service.EnqueueSteer(ctx, handle.BotID, handle.SessionID, "invoke-2", []byte(`{"text":"late"}`)); !errors.Is(err, sessionqueue.ErrNoActiveRun) { + if _, err := service.EnqueueSteer(ctx, handle.BotID, handle.SessionID, "invoke-2", []byte(`{"text":"late"}`)); !errors.Is(err, sessionruntime.ErrQueueNoActiveRun) { t.Fatalf("late steer after sealed final = %v", err) } } diff --git a/internal/agent/application/queue_step_failure_test.go b/internal/agent/application/queue_step_failure_test.go new file mode 100644 index 0000000000..17b67eb7c1 --- /dev/null +++ b/internal/agent/application/queue_step_failure_test.go @@ -0,0 +1,86 @@ +package application + +import ( + "context" + "errors" + "testing" + + sdk "github.com/felinics/twilight/sdk" + + contextfrag "github.com/felinics/memoh/internal/agent/context/fragment" + "github.com/felinics/memoh/internal/agent/runtime/native" + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" + "github.com/felinics/memoh/internal/agent/turn" + messagepkg "github.com/felinics/memoh/internal/chat/message" + "github.com/felinics/memoh/internal/runtimefence" +) + +type failedSteerApplyBackend struct { + *sessionruntime.MemoryBackend + applyErr error + releases int +} + +func (b *failedSteerApplyBackend) ApplySteer(ctx context.Context, key sessionruntime.Key, ref sessionruntime.SteerClaimRef) error { + if b.applyErr != nil { + return b.applyErr + } + return b.MemoryBackend.ApplySteer(ctx, key, ref) +} + +func (b *failedSteerApplyBackend) ReleaseSteer(ctx context.Context, key sessionruntime.Key, ref sessionruntime.SteerClaimRef) error { + b.releases++ + return b.MemoryBackend.ReleaseSteer(ctx, key, ref) +} + +func TestStepCommitSeparatesHistoryFailureFromQueueFailure(t *testing.T) { + for _, historyFails := range []bool{false, true} { + t.Run(map[bool]string{false: "queue apply after committed history", true: "history before queue apply"}[historyFails], func(t *testing.T) { + failure := errors.New("injected boundary failure") + backend := &failedSteerApplyBackend{MemoryBackend: sessionruntime.NewMemoryBackend()} + service, handle := newDeferredSteerTestService(t, backend) + ctx := runtimefence.WithContext(context.Background(), runtimefence.Fence{BotID: handle.BotID, SessionID: handle.SessionID, Token: handle.FencingToken}) + item, err := service.EnqueueSteer(ctx, handle.BotID, handle.SessionID, "steer-failure", []byte(`{"text":"adjust"}`)) + if err != nil { + t.Fatal(err) + } + req := ChatRequest{BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, RunHandle: handle, UserMessagePersisted: true, PersistedUserMessageID: "user", QueueInjectCh: make(chan turn.InjectMessage, 1)} + committer := service.newAgentStepCommitter(ctx, req, resolvedContext{runConfig: native.RunConfig{ContextLifecycle: contextfrag.NewLifecycleHolder()}}) + if committer == nil { + t.Fatal("missing fenced step committer") + } + if _, err := committer.queueStep.commit(ctx, queueStepToolLoop, messagepkg.AgentStep{}, nil); err != nil { + t.Fatal(err) + } + store := service.messageService.(*recordingStepPersister) + if historyFails { + store.stepErr = failure + } else { + backend.applyErr = failure + } + step := &sdk.StepResult{FinishReason: sdk.FinishReasonStop, Messages: []sdk.Message{sdk.AssistantMessage("committed response")}} + if err := committer.commit(ctx, 0, step); !errors.Is(err, failure) { + t.Fatalf("commit error=%v", err) + } + pending, _, err := service.sessionManager.PendingQueues(ctx, sessionruntime.Key{BotID: handle.BotID, SessionID: handle.SessionID}, 0) + if err != nil { + t.Fatal(err) + } + if historyFails { + if len(committer.persistedMessages()) != 0 || committer.nextStep != 0 || backend.releases != 1 || len(pending) != 1 || pending[0].ID != item.ID { + t.Fatal("uncommitted step did not return its claim") + } + } else { + if len(committer.persistedMessages()) != 1 || committer.nextStep != 1 || backend.releases != 0 || len(pending) != 0 { + t.Fatal("committed prefix was lost or its claim was released") + } + if err := committer.commit(ctx, 0, step); err == nil { + t.Fatal("already-persisted step was accepted twice") + } + if len(store.steps) != 1 { + t.Fatalf("history writes=%d", len(store.steps)) + } + } + }) + } +} diff --git a/internal/agent/application/runtime_decision_finish_test.go b/internal/agent/application/runtime_decision_finish_test.go index 973d349538..ce17f7ee29 100644 --- a/internal/agent/application/runtime_decision_finish_test.go +++ b/internal/agent/application/runtime_decision_finish_test.go @@ -17,6 +17,7 @@ import ( "github.com/felinics/memoh/internal/db/postgres/sqlc" dbstore "github.com/felinics/memoh/internal/db/store" "github.com/felinics/memoh/internal/runtimefence" + sessiontest "github.com/felinics/memoh/internal/testutil/sessionruntime" ) type finishDecisionQueries struct { @@ -89,7 +90,7 @@ func TestWebAndChannelStopCloseParkedInputWithoutContinuing(t *testing.T) { t.Fatalf("cleanup replay: %v, calls=%d", err, input.cancelCalls) } // The runtime must release the slot as well as the decision row. - if _, err := manager.StartRunHandle(context.Background(), handle.BotID, handle.SessionID, + if _, err := sessiontest.Start(context.Background(), manager, handle.BotID, handle.SessionID, "55555555-5555-4555-8555-555555555555", make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)); err != nil { t.Fatalf("next run blocked: %v", err) } diff --git a/internal/agent/application/runtime_decision_test.go b/internal/agent/application/runtime_decision_test.go index 5f05b0f26e..5ecb41a7d7 100644 --- a/internal/agent/application/runtime_decision_test.go +++ b/internal/agent/application/runtime_decision_test.go @@ -15,6 +15,7 @@ import ( sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/agent/turn" "github.com/felinics/memoh/internal/apperror" + sessiontest "github.com/felinics/memoh/internal/testutil/sessionruntime" ) func TestRuntimeDecisionContinuationLogsPrivateCause(t *testing.T) { @@ -45,7 +46,7 @@ func newWaitingDecisionRuntime(t *testing.T, backends ...sessionruntime.Backend) if len(backends) > 0 { backend = backends[0] } - manager := sessionruntime.NewManager(backend, sessionruntime.Options{ + manager := sessiontest.New(backend, sessionruntime.Options{ OwnerID: "runtime-lifecycle-owner", StateTTL: time.Minute, OwnerLeaseTTL: time.Second, @@ -55,8 +56,7 @@ func newWaitingDecisionRuntime(t *testing.T, backends ...sessionruntime.Backend) if err := manager.Start(context.Background()); err != nil { t.Fatalf("start runtime manager: %v", err) } - handle, err := manager.StartRunHandle( - context.Background(), + handle, err := sessiontest.Start(context.Background(), manager, lifecycleTestBotID, lifecycleTestSessionID, lifecycleTestRunID, @@ -90,7 +90,7 @@ func runtimeDecisionEvent(t *testing.T, event native.StreamEvent) WSStreamEvent } type failNextRuntimeDecisionBackend struct { - sessionruntime.Backend + *sessionruntime.MemoryBackend failNext atomic.Bool err error } @@ -103,7 +103,7 @@ func (b *failNextRuntimeDecisionBackend) Update( if b.failNext.CompareAndSwap(true, false) { return sessionruntime.Snapshot{}, false, b.err } - return b.Backend.Update(ctx, key, update) + return b.MemoryBackend.Update(ctx, key, update) } func TestRuntimeDecisionTerminalDoesNotExposePrivateErrors(t *testing.T) { @@ -165,7 +165,7 @@ func TestContinueRuntimeDecisionDoesNotParkProviderCancellation(t *testing.T) { sessionID = "session-provider-cancel" runID = "run-provider-cancel" ) - manager := sessionruntime.NewManager(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ + manager := sessiontest.New(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ OwnerID: "owner-provider-cancel", StateTTL: time.Minute, OwnerLeaseTTL: time.Second, @@ -175,8 +175,7 @@ func TestContinueRuntimeDecisionDoesNotParkProviderCancellation(t *testing.T) { if err := manager.Start(context.Background()); err != nil { t.Fatalf("start runtime manager: %v", err) } - handle, err := manager.StartRunHandle( - context.Background(), + handle, err := sessiontest.Start(context.Background(), manager, botID, sessionID, runID, @@ -225,10 +224,10 @@ func TestContinueRuntimeDecisionCancelsContinuationAfterPublicationFailure(t *te ) publishErr := errors.New("private runtime publication failure") backend := &failNextRuntimeDecisionBackend{ - Backend: sessionruntime.NewMemoryBackend(), - err: publishErr, + MemoryBackend: sessionruntime.NewMemoryBackend(), + err: publishErr, } - manager := sessionruntime.NewManager(backend, sessionruntime.Options{ + manager := sessiontest.New(backend, sessionruntime.Options{ OwnerID: "owner-publish-failure", StateTTL: time.Minute, OwnerLeaseTTL: time.Second, @@ -238,8 +237,7 @@ func TestContinueRuntimeDecisionCancelsContinuationAfterPublicationFailure(t *te if err := manager.Start(context.Background()); err != nil { t.Fatalf("start runtime manager: %v", err) } - handle, err := manager.StartRunHandle( - context.Background(), + handle, err := sessiontest.Start(context.Background(), manager, botID, sessionID, runID, diff --git a/internal/agent/application/service.go b/internal/agent/application/service.go index fbb5cd2368..a4d656d0fd 100644 --- a/internal/agent/application/service.go +++ b/internal/agent/application/service.go @@ -830,8 +830,7 @@ func (s *Service) Chat(ctx context.Context, req ChatRequest) (ChatResponse, erro stepCommitter := s.newAgentStepCommitter(ctx, req, rc) if stepCommitter != nil { cfg.OnStepCommitted = stepCommitter.commit - cfg.ContinueAfterFinal = &stepCommitter.continueAfterFinal - cfg.NextModelInputs = &stepCommitter.nextModelInputs + stepCommitter.bindContinuation(&cfg) } cfg = s.prepareRunConfig(ctx, cfg) terminal := s.contextLifecycleTerminal(ctx, cfg) diff --git a/internal/agent/application/service_run_lifecycle_test.go b/internal/agent/application/service_run_lifecycle_test.go index db35a04e43..a4ea40e01d 100644 --- a/internal/agent/application/service_run_lifecycle_test.go +++ b/internal/agent/application/service_run_lifecycle_test.go @@ -84,15 +84,6 @@ func (a *lifecycleTurnAdmitter) Admit( return a.admission, nil } -func (a *lifecycleTurnAdmitter) FinishRun( - _ context.Context, - handle sessionruntime.RunHandle, - status, message string, -) error { - a.finishes = append(a.finishes, recordedFinish{handle: handle, status: status, message: message}) - return a.finishErr -} - func (a *lifecycleTurnAdmitter) FinishRunWithErrorCode( _ context.Context, handle sessionruntime.RunHandle, diff --git a/internal/agent/application/service_stream.go b/internal/agent/application/service_stream.go index 36b2581014..e944337bcb 100644 --- a/internal/agent/application/service_stream.go +++ b/internal/agent/application/service_stream.go @@ -237,8 +237,7 @@ func (s *Service) StreamChat(ctx context.Context, req ChatRequest) (<-chan Strea stepCommitter := s.newAgentStepCommitter(streamCtx, streamReq, rc) configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) if stepCommitter != nil { - cfg.ContinueAfterFinal = &stepCommitter.continueAfterFinal - cfg.NextModelInputs = &stepCommitter.nextModelInputs + stepCommitter.bindContinuation(&cfg) } cfg = s.prepareRunConfig(streamCtx, cfg) terminal := s.contextLifecycleTerminal(streamCtx, cfg) @@ -609,8 +608,7 @@ func (s *Service) streamChatWSResultWithHooks( stepCommitter := s.newAgentStepCommitter(streamCtx, req, rc) configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) if stepCommitter != nil { - cfg.ContinueAfterFinal = &stepCommitter.continueAfterFinal - cfg.NextModelInputs = &stepCommitter.nextModelInputs + stepCommitter.bindContinuation(&cfg) } cfg = s.prepareRunConfig(streamCtx, cfg) terminal := s.contextLifecycleTerminal(streamCtx, cfg) diff --git a/internal/agent/application/service_tool_approval.go b/internal/agent/application/service_tool_approval.go index e22d6e70d4..b3cfae7fa4 100644 --- a/internal/agent/application/service_tool_approval.go +++ b/internal/agent/application/service_tool_approval.go @@ -2,10 +2,8 @@ package application import ( "context" - "encoding/json" "errors" "fmt" - "log/slog" "strings" sdk "github.com/felinics/twilight/sdk" @@ -16,7 +14,6 @@ import ( sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/bots" sessionpkg "github.com/felinics/memoh/internal/chat/thread" - "github.com/felinics/memoh/internal/models" "github.com/felinics/memoh/internal/workspace" ) @@ -462,23 +459,6 @@ func (s *Service) continueToolApprovalSession( if err != nil { return err } - terminal := s.contextLifecycleTerminal(ctx, cfg) - var lifecycleCause error - var lifecycleDeferred bool - var terminalEventSeen bool - defer func() { - if runtimeLifecycle != nil { - runtimeLifecycle.cause = lifecycleCause - runtimeLifecycle.deferred = lifecycleDeferred - if snapshot, ok := cfg.ContextLifecycle.Snapshot(); ok { - runtimeLifecycle.snapshot = &snapshot - } - return - } - if !lifecycleDeferred { - terminal(lifecycleCause) - } - }() req := ChatRequest{ RunID: cfg.RunID, @@ -495,143 +475,7 @@ func (s *Service) continueToolApprovalSession( WorkspaceTarget: workspaceTargetFromRunConfig(resolved.RunConfig), } - continuationRC := resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}} - stepCommitter, stopQueueBinding, err := s.bindQueueContinuation(ctx, &req, &cfg, continuationRC) - if err != nil { - return err - } - defer stopQueueBinding() - reasoningTiming := newReasoningTimingTracker(nil) - configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) - idleCtx, idleCancel := s.withStreamIdleTimeout(ctx, reasoningEffortForIdle(cfg)) - defer idleCancel.Stop() - stream := s.agent.Stream(idleCtx, cfg) - stored := false - failureEventForwarded := false - var hasVisibleOutput bool - for event := range stream { - idleCancel.Reset() - if event.Type == native.EventToolCallStart { - idleCancel.RecordToolCall() - } - if eventErr := agentStreamLifecycleError(event); eventErr != nil && lifecycleCause == nil { - lifecycleCause = eventErr - // The public event forwarded downstream carries only a stable code; - // keep the runtime's private detail in the server log so a failed - // continuation can be diagnosed. - s.logContinuationStreamError(req.RunID, event) - } - if event.IsTerminal() { - terminalEventSeen = true - lifecycleDeferred = pendingContinuationDecision(event) - if !lifecycleDeferred { - switch event.Type { - case native.EventAgentEnd: - lifecycleCause = nil - case native.EventAgentAbort: - if idleCancel.DidFire() { - lifecycleCause = context.Cause(idleCtx) - } else if context.Cause(ctx) != nil || lifecycleCause == nil { - lifecycleCause = agentAbortCause(ctx) - } - } - } - } - if hasVisibleAgentStreamOutput(event) { - hasVisibleOutput = true - } - if event.Type == native.EventAgentAbort && idleCancel.DidFire() && eventCh != nil { - if failureData, marshalErr := json.Marshal(agentFailureStreamEvent(context.Cause(idleCtx))); marshalErr == nil { - select { - case eventCh <- json.RawMessage(failureData): - failureEventForwarded = true - case <-ctx.Done(): - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - } - } - data, err := json.Marshal(publicAgentStreamEvent(event)) - if err != nil { - continue - } - if !stored && event.IsTerminal() && len(event.Messages) > 0 { - if snap, ok := extractTerminalSnapshot(data); ok { - if stepCommitter == nil { - snap.reasoningTiming = takeTerminalReasoningTiming(reasoningTiming, event.Type) - } - snap.visibleOutput = hasVisibleOutput - snap.failureCode = snapshotFailureCode(idleCancel.DidFire(), lifecycleCause) - lifecycleDeferred = lifecycleDeferred || snap.deferredToolID != "" - if snap.aborted && !lifecycleDeferred && lifecycleCause == nil { - lifecycleCause = agentAbortCause(ctx) - } - var storeErr error - if stepCommitter != nil { - storeErr = stepCommitter.finish(ctx, extractInputTokensFromUsage(snap.usage)) - } else { - storeErr = s.persistTerminalSnapshot( - context.WithoutCancel(ctx), - req, - resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}}, - snap, - ) - } - if storeErr != nil { - lifecycleCause = storeErr - lifecycleDeferred = false - return storeErr - } - stored = true - } - } - if eventCh != nil && shouldForwardAfterIdleFailure(event, failureEventForwarded) { - select { - case eventCh <- json.RawMessage(data): - case <-ctx.Done(): - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - } - } - if !stored && stepCommitter != nil { - if storeErr := stepCommitter.finish(ctx, 0); storeErr != nil { - lifecycleCause = storeErr - return storeErr - } - stored = true - } - if stepCommitter != nil { - if commitErr := stepCommitter.err(); commitErr != nil && ctx.Err() == nil { - lifecycleCause = commitErr - return commitErr - } - } - if idleCancel.DidFire() { - lifecycleCause = context.Cause(idleCtx) - if !stored { - if _, storeErr := s.persistTurnFailure(context.WithoutCancel(ctx), req, resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}}, snapshotFailureCode(true, lifecycleCause)); storeErr != nil { - s.logger.Error("tool approval timeout persist failed", slog.Any("error", storeErr)) - } - } - if eventCh != nil && !failureEventForwarded { - if data, marshalErr := json.Marshal(agentFailureStreamEvent(lifecycleCause)); marshalErr == nil { - select { - case eventCh <- json.RawMessage(data): - case <-ctx.Done(): - } - } - } - return lifecycleCause - } - if ctx.Err() != nil { - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - if lifecycleCause == nil && !lifecycleDeferred && !terminalEventSeen { - lifecycleCause = errors.New("agent continuation ended without a terminal event") - } - return nil + return s.runNativeDecisionContinuation(ctx, req, cfg, resolved.ModelID, runtimeLifecycle, eventCh) } func withLocalWebReplyTarget(req toolapproval.Request) toolapproval.Request { diff --git a/internal/agent/application/service_trigger_test.go b/internal/agent/application/service_trigger_test.go index 3a15b57739..ca79bd696f 100644 --- a/internal/agent/application/service_trigger_test.go +++ b/internal/agent/application/service_trigger_test.go @@ -541,7 +541,7 @@ func (f *fakeTriggeredAdmitter) Admit(_ context.Context, input sessionruntime.Ad }, nil } -func (*fakeTriggeredAdmitter) FinishRun(context.Context, sessionruntime.RunHandle, string, string) error { +func (*fakeTriggeredAdmitter) FinishRunWithErrorCode(context.Context, sessionruntime.RunHandle, string, string) error { return nil } diff --git a/internal/agent/application/service_user_input.go b/internal/agent/application/service_user_input.go index 5f5bc64075..75ae833ce8 100644 --- a/internal/agent/application/service_user_input.go +++ b/internal/agent/application/service_user_input.go @@ -2,20 +2,16 @@ package application import ( "context" - "encoding/json" "errors" "fmt" - "log/slog" "strings" sdk "github.com/felinics/twilight/sdk" userinput "github.com/felinics/memoh/internal/agent/decision/input" - "github.com/felinics/memoh/internal/agent/runtime/native" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/bots" sessionpkg "github.com/felinics/memoh/internal/chat/thread" - "github.com/felinics/memoh/internal/models" "github.com/felinics/memoh/internal/workspace" ) @@ -412,23 +408,6 @@ func (s *Service) continueUserInputSession( if err != nil { return err } - terminal := s.contextLifecycleTerminal(ctx, cfg) - var lifecycleCause error - var lifecycleDeferred bool - var terminalEventSeen bool - defer func() { - if runtimeLifecycle != nil { - runtimeLifecycle.cause = lifecycleCause - runtimeLifecycle.deferred = lifecycleDeferred - if snapshot, ok := cfg.ContextLifecycle.Snapshot(); ok { - runtimeLifecycle.snapshot = &snapshot - } - return - } - if !lifecycleDeferred { - terminal(lifecycleCause) - } - }() chatReq := ChatRequest{ RunID: cfg.RunID, @@ -449,143 +428,7 @@ func (s *Service) continueUserInputSession( WorkspaceTarget: workspaceTargetFromRunConfig(resolved.RunConfig), } - continuationRC := resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}} - stepCommitter, stopQueueBinding, err := s.bindQueueContinuation(ctx, &chatReq, &cfg, continuationRC) - if err != nil { - return err - } - defer stopQueueBinding() - reasoningTiming := newReasoningTimingTracker(nil) - configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) - idleCtx, idleCancel := s.withStreamIdleTimeout(ctx, reasoningEffortForIdle(cfg)) - defer idleCancel.Stop() - stream := s.agent.Stream(idleCtx, cfg) - stored := false - failureEventForwarded := false - var hasVisibleOutput bool - for event := range stream { - idleCancel.Reset() - if event.Type == native.EventToolCallStart { - idleCancel.RecordToolCall() - } - if eventErr := agentStreamLifecycleError(event); eventErr != nil && lifecycleCause == nil { - lifecycleCause = eventErr - // The public event forwarded downstream carries only a stable code; - // keep the runtime's private detail in the server log so a failed - // continuation can be diagnosed. - s.logContinuationStreamError(chatReq.RunID, event) - } - if event.IsTerminal() { - terminalEventSeen = true - lifecycleDeferred = pendingContinuationDecision(event) - if !lifecycleDeferred { - switch event.Type { - case native.EventAgentEnd: - lifecycleCause = nil - case native.EventAgentAbort: - if idleCancel.DidFire() { - lifecycleCause = context.Cause(idleCtx) - } else if context.Cause(ctx) != nil || lifecycleCause == nil { - lifecycleCause = agentAbortCause(ctx) - } - } - } - } - if hasVisibleAgentStreamOutput(event) { - hasVisibleOutput = true - } - if event.Type == native.EventAgentAbort && idleCancel.DidFire() && eventCh != nil { - if failureData, marshalErr := json.Marshal(agentFailureStreamEvent(context.Cause(idleCtx))); marshalErr == nil { - select { - case eventCh <- json.RawMessage(failureData): - failureEventForwarded = true - case <-ctx.Done(): - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - } - } - data, err := json.Marshal(publicAgentStreamEvent(event)) - if err != nil { - continue - } - if !stored && event.IsTerminal() && len(event.Messages) > 0 { - if snap, ok := extractTerminalSnapshot(data); ok { - if stepCommitter == nil { - snap.reasoningTiming = takeTerminalReasoningTiming(reasoningTiming, event.Type) - } - snap.visibleOutput = hasVisibleOutput - snap.failureCode = snapshotFailureCode(idleCancel.DidFire(), lifecycleCause) - lifecycleDeferred = lifecycleDeferred || snap.deferredToolID != "" - if snap.aborted && !lifecycleDeferred && lifecycleCause == nil { - lifecycleCause = agentAbortCause(ctx) - } - var storeErr error - if stepCommitter != nil { - storeErr = stepCommitter.finish(ctx, extractInputTokensFromUsage(snap.usage)) - } else { - storeErr = s.persistTerminalSnapshot( - context.WithoutCancel(ctx), - chatReq, - resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}}, - snap, - ) - } - if storeErr != nil { - lifecycleCause = storeErr - lifecycleDeferred = false - return storeErr - } - stored = true - } - } - if eventCh != nil && shouldForwardAfterIdleFailure(event, failureEventForwarded) { - select { - case eventCh <- json.RawMessage(data): - case <-ctx.Done(): - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - } - } - if !stored && stepCommitter != nil { - if storeErr := stepCommitter.finish(ctx, 0); storeErr != nil { - lifecycleCause = storeErr - return storeErr - } - stored = true - } - if stepCommitter != nil { - if commitErr := stepCommitter.err(); commitErr != nil && ctx.Err() == nil { - lifecycleCause = commitErr - return commitErr - } - } - if idleCancel.DidFire() { - lifecycleCause = context.Cause(idleCtx) - if !stored { - if _, storeErr := s.persistTurnFailure(context.WithoutCancel(ctx), chatReq, resolvedContext{runConfig: cfg, model: models.GetResponse{ID: resolved.ModelID}}, snapshotFailureCode(true, lifecycleCause)); storeErr != nil { - s.logger.Error("user input timeout persist failed", slog.Any("error", storeErr)) - } - } - if eventCh != nil && !failureEventForwarded { - if data, marshalErr := json.Marshal(agentFailureStreamEvent(lifecycleCause)); marshalErr == nil { - select { - case eventCh <- json.RawMessage(data): - case <-ctx.Done(): - } - } - } - return lifecycleCause - } - if ctx.Err() != nil { - lifecycleCause = context.Cause(ctx) - return lifecycleCause - } - if lifecycleCause == nil && !lifecycleDeferred && !terminalEventSeen { - lifecycleCause = errors.New("agent continuation ended without a terminal event") - } - return nil + return s.runNativeDecisionContinuation(ctx, chatReq, cfg, resolved.ModelID, runtimeLifecycle, eventCh) } func withLocalWebUserInputReplyTarget(req userinput.Request) userinput.Request { diff --git a/internal/agent/application/service_user_input_test.go b/internal/agent/application/service_user_input_test.go index ecc9067660..d5cd595d83 100644 --- a/internal/agent/application/service_user_input_test.go +++ b/internal/agent/application/service_user_input_test.go @@ -17,6 +17,7 @@ import ( "github.com/felinics/memoh/internal/agent/turn" "github.com/felinics/memoh/internal/bots" session "github.com/felinics/memoh/internal/chat/thread" + sessiontest "github.com/felinics/memoh/internal/testutil/sessionruntime" ) const testACPUserInputOwnerID = "owner-user" @@ -363,7 +364,7 @@ func TestRuntimeUserInputCommandCommitsAndResumesSameRun(t *testing.T) { return sendAgentStreamEvent(ctx, eventCh, native.StreamEvent{Type: native.EventAgentEnd}) }, } - manager := sessionruntime.NewManager(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ + manager := sessiontest.New(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ OwnerID: "owner-1", StateTTL: time.Minute, OwnerLeaseTTL: time.Second, @@ -374,8 +375,7 @@ func TestRuntimeUserInputCommandCommitsAndResumesSameRun(t *testing.T) { t.Fatalf("start runtime manager: %v", err) } resolver.SetSessionRuntime(manager) - if err := manager.StartRun( - context.Background(), + if _, err := sessiontest.Start(context.Background(), manager, botID, sessionID, runID, @@ -415,17 +415,13 @@ func TestRuntimeUserInputCommandCommitsAndResumesSameRun(t *testing.T) { if err != nil { t.Fatalf("encode response: %v", err) } - handled, err := manager.DispatchRunCommand( - context.Background(), - botID, - sessionID, - runID, - sessionruntime.CommandUserInputResponse, - inputID, - payload, - ) - if err != nil || !handled { - t.Fatalf("dispatch response = handled:%v err:%v", handled, err) + err = resolver.handleRuntimeDecisionCommand(context.Background(), sessionruntime.Command{ + ID: "control-early-ack", Type: sessionruntime.CommandUserInputResponse, + BotID: botID, SessionID: sessionID, RunID: runID, + Generation: handle.Generation, TargetID: inputID, Payload: payload, + }) + if err != nil { + t.Fatalf("handle decision command: %v", err) } if fake.submitCalls != 1 { t.Fatalf("submit calls = %d, want 1 before acknowledgement", fake.submitCalls) diff --git a/internal/agent/application/session_queue.go b/internal/agent/application/session_queue.go index bf628bb03e..b2987c730d 100644 --- a/internal/agent/application/session_queue.go +++ b/internal/agent/application/session_queue.go @@ -2,18 +2,19 @@ package application import ( "context" + "encoding/json" "github.com/google/uuid" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - sessionqueue "github.com/felinics/memoh/internal/agent/runtime/session/queue" ) // SessionQueues is the application surface for user-facing queue operations. // Items are transient and live in the configured memory or Redis runtime. type SessionQueues struct { - Steer []sessionqueue.SteerItem - FollowUp []sessionqueue.FollowUpItem + SteerSupported bool + Steer []sessionruntime.SteerItem + FollowUp []sessionruntime.FollowUpItem } func (s *Service) liveQueueRuntime() (*sessionruntime.Manager, error) { @@ -23,24 +24,24 @@ func (s *Service) liveQueueRuntime() (*sessionruntime.Manager, error) { return s.sessionManager, nil } -func (s *Service) EnqueueSteer(ctx context.Context, botID, sessionID, invocationID string, payload []byte) (sessionqueue.SteerItem, error) { +func (s *Service) EnqueueSteer(ctx context.Context, botID, sessionID, invocationID string, payload []byte) (sessionruntime.SteerItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { - return sessionqueue.SteerItem{}, err + return sessionruntime.SteerItem{}, err } return runtime.EnqueueSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, uuid.NewString(), invocationID, payload) } -func (s *Service) EnqueueFollowUp(ctx context.Context, botID, sessionID, invocationID string, payload []byte) (sessionqueue.FollowUpItem, error) { +func (s *Service) EnqueueFollowUp(ctx context.Context, botID, sessionID, invocationID string, payload []byte) (sessionruntime.FollowUpItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { - return sessionqueue.FollowUpItem{}, err + return sessionruntime.FollowUpItem{}, err } item, err := runtime.EnqueueFollowUp(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, uuid.NewString(), invocationID, payload) if err != nil { - return sessionqueue.FollowUpItem{}, err + return sessionruntime.FollowUpItem{}, err } - if item.Status == sessionqueue.Accepted { + if item.Status == sessionruntime.QueueAccepted { s.kickFollowUpIfIdle(ctx, botID, sessionID, item.EnqueuedDuringRunID) } return item, nil @@ -51,14 +52,18 @@ func (s *Service) ListSessionQueues(ctx context.Context, botID, sessionID string if err != nil { return SessionQueues{}, err } - steers, followUps, err := runtime.PendingQueues(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionqueue.DefaultPendingListLimit) + steers, followUps, err := runtime.PendingQueues(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, 0) if err != nil { return SessionQueues{}, err } - return SessionQueues{Steer: steers, FollowUp: followUps}, nil + snapshot, err := runtime.Snapshot(ctx, botID, sessionID) + if err != nil { + return SessionQueues{}, err + } + return SessionQueues{Steer: steers, FollowUp: followUps, SteerSupported: sessionruntime.SteerRunAvailable(snapshot.CurrentRunView)}, nil } -func (s *Service) ReorderSteer(ctx context.Context, botID, sessionID string, item, before sessionqueue.SteerPendingRef) ([]sessionqueue.SteerItem, error) { +func (s *Service) ReorderSteer(ctx context.Context, botID, sessionID string, item, before sessionruntime.SteerPendingRef) ([]sessionruntime.SteerItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { return nil, err @@ -66,7 +71,7 @@ func (s *Service) ReorderSteer(ctx context.Context, botID, sessionID string, ite return runtime.ReorderSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, item, before) } -func (s *Service) ReorderFollowUp(ctx context.Context, botID, sessionID string, item, before sessionqueue.FollowUpPendingRef) ([]sessionqueue.FollowUpItem, error) { +func (s *Service) ReorderFollowUp(ctx context.Context, botID, sessionID string, item, before sessionruntime.FollowUpPendingRef) ([]sessionruntime.FollowUpItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { return nil, err @@ -74,20 +79,46 @@ func (s *Service) ReorderFollowUp(ctx context.Context, botID, sessionID string, return runtime.ReorderFollowUp(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, item, before) } -func (s *Service) UpdateSteer(ctx context.Context, botID, sessionID, itemID string, payload []byte) (sessionqueue.SteerItem, error) { +func (s *Service) UpdateSteer(ctx context.Context, botID, sessionID, itemID string, payload []byte) (sessionruntime.SteerItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { - return sessionqueue.SteerItem{}, err + return sessionruntime.SteerItem{}, err } - return runtime.UpdateSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionqueue.SteerItemID(itemID), payload) + return runtime.UpdateSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionruntime.SteerItemID(itemID), payload) } -func (s *Service) UpdateFollowUp(ctx context.Context, botID, sessionID, itemID string, payload []byte) (sessionqueue.FollowUpItem, error) { +func (s *Service) UpdateFollowUp(ctx context.Context, botID, sessionID, itemID string, payload []byte) (sessionruntime.FollowUpItem, error) { runtime, err := s.liveQueueRuntime() if err != nil { - return sessionqueue.FollowUpItem{}, err + return sessionruntime.FollowUpItem{}, err + } + key := sessionruntime.Key{BotID: botID, SessionID: sessionID} + _, items, err := runtime.PendingQueues(ctx, key, 0) + if err != nil { + return sessionruntime.FollowUpItem{}, err + } + for _, item := range items { + if string(item.ID) != itemID { + continue + } + body := decodeFollowUpPayload(item.Payload) + if body.Command != nil { + text := QueuePayloadText(payload) + body.Text = text + body.Command.Query = text + body.Command.ModelQuery = "" + body.Command.UserVisibleText = text + payload, err = json.Marshal(body) + if err != nil { + return sessionruntime.FollowUpItem{}, err + } + } + // Routing and attachment metadata are immutable for a queued command. + // The backend rechecks accepted status atomically with the edit, so a + // concurrent claim/cancel/promotion still rejects this write. + return runtime.UpdateFollowUp(ctx, key, item.ID, payload) } - return runtime.UpdateFollowUp(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionqueue.FollowUpItemID(itemID), payload) + return sessionruntime.FollowUpItem{}, sessionruntime.ErrQueueNotPending } func (s *Service) CancelSteer(ctx context.Context, botID, sessionID, itemID string) error { @@ -95,7 +126,7 @@ func (s *Service) CancelSteer(ctx context.Context, botID, sessionID, itemID stri if err != nil { return err } - return runtime.CancelSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionqueue.SteerItemID(itemID)) + return runtime.CancelSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionruntime.SteerItemID(itemID)) } func (s *Service) CancelFollowUp(ctx context.Context, botID, sessionID, itemID string) error { @@ -103,13 +134,13 @@ func (s *Service) CancelFollowUp(ctx context.Context, botID, sessionID, itemID s if err != nil { return err } - return runtime.CancelFollowUp(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionqueue.FollowUpItemID(itemID)) + return runtime.CancelFollowUp(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, sessionruntime.FollowUpItemID(itemID)) } -func (s *Service) PromoteFollowUpToSteer(ctx context.Context, botID, sessionID string, followUp sessionqueue.FollowUpPendingRef) (sessionqueue.PromoteFollowUpResult, error) { +func (s *Service) PromoteFollowUpToSteer(ctx context.Context, botID, sessionID string, followUp sessionruntime.FollowUpPendingRef) (sessionruntime.PromoteFollowUpResult, error) { runtime, err := s.liveQueueRuntime() if err != nil { - return sessionqueue.PromoteFollowUpResult{}, err + return sessionruntime.PromoteFollowUpResult{}, err } return runtime.PromoteFollowUpToSteer(ctx, sessionruntime.Key{BotID: botID, SessionID: sessionID}, followUp) } diff --git a/internal/agent/application/step_commit.go b/internal/agent/application/step_commit.go index fd433ee1a2..211336f1fb 100644 --- a/internal/agent/application/step_commit.go +++ b/internal/agent/application/step_commit.go @@ -2,9 +2,6 @@ package application import ( "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" "errors" "fmt" "log/slog" @@ -14,6 +11,7 @@ import ( sdk "github.com/felinics/twilight/sdk" + "github.com/felinics/memoh/internal/agent/runtime/native" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" chatview "github.com/felinics/memoh/internal/agent/view" messagepkg "github.com/felinics/memoh/internal/chat/message" @@ -24,12 +22,13 @@ import ( // persistence. It is intentionally enabled only for admitted, fenced turns; // legacy calls without a runtime owner keep their terminal-snapshot behavior. type agentStepCommitter struct { + ownerContext context.Context service *Service req ChatRequest rc resolvedContext persister messagepkg.AgentStepPersister reasoningTiming *reasoningTimingTracker - queueStep *queueStepTransaction + queueStep *queueStepCoordinator continueAfterFinal atomic.Bool nextModelInputs []sdk.Message @@ -57,7 +56,15 @@ func (s *Service) newAgentStepCommitter(ctx context.Context, req ChatRequest, rc if !ok { return nil } - queueStep := newQueueStepTransaction(s, req, rc.model.ID) + queueStep := newQueueStepCoordinator(s, req) + if queueStep != nil && queueStep.steerEnabled { + if err := s.sessionManager.EnableSteer(ctx, req.RunHandle); err != nil { + queueStep.steerEnabled = false + if s.logger != nil { + s.logger.Warn("steer consumer could not be published", slog.String("run_id", req.RunID), slog.Any("error", err)) + } + } + } if req.TurnReplacement != nil && queueStep == nil { return nil } @@ -74,13 +81,21 @@ func (s *Service) newAgentStepCommitter(ctx context.Context, req ChatRequest, rc return nil } return &agentStepCommitter{ - service: s, req: req, rc: rc, persister: persister, + service: s, req: req, rc: rc, persister: persister, ownerContext: ctx, queueStep: queueStep, turnRequestMessageID: requestMessageID, nextStep: req.StepIndexOffset, } } +func (c *agentStepCommitter) bindContinuation(cfg *native.RunConfig) { + if c == nil || cfg == nil { + return + } + cfg.ContinueAfterFinal = &c.continueAfterFinal + cfg.NextModelInputs = &c.nextModelInputs +} + func (c *agentStepCommitter) commit(ctx context.Context, stepIndex int, step *sdk.StepResult) error { return c.persist(ctx, stepIndex, step, false) } @@ -93,6 +108,11 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s if c == nil || step == nil { return errors.New("agent step is missing") } + persistCtx, ownershipErr := stepPersistenceContext(ctx, c.ownerContext) + if ownershipErr != nil { + return ownershipErr + } + ctx = persistCtx messages := sdkMessagesToModelMessages(step.Messages) timingState := "completed" if interrupted { @@ -158,30 +178,31 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s inputs[i].TurnRequestMessageID = c.turnRequestMessageID } agentStep := messagepkg.AgentStep{RunID: c.req.RunID, Messages: inputs, Interrupted: interrupted} - commitHash := agentStepCommitHash(agentStep, step) var persisted []messagepkg.Message + var queueErr error if c.queueStep != nil && !interrupted { stepCtx := context.WithoutCancel(ctx) outcome, commitErr := c.queueStep.commit( - stepCtx, stepIndex, commitHash, classifyQueueStep(step), agentStep, c.persisted, + stepCtx, classifyQueueStep(step), agentStep, c.persisted, ) - if commitErr != nil { + if !outcome.historyCommitted { return fail(commitErr) } + // History is already durable even if later queue coordination failed. + // Record that prefix below before returning the error; a cleanup must + // not lose it or write the same step again. + queueErr = commitErr persisted = outcome.persisted - if c.req.TurnReplacement == nil { - if publisher, ok := c.service.messageService.(messagepkg.AgentStepPublisher); ok { - publisher.PublishAgentStep(persisted) - } - } c.replacementFinalized = outcome.replacementFinalized - if outcome.continueAfterFinal && classifyQueueStep(step) == queueStepFinal { + if queueErr == nil && outcome.continueAfterFinal && classifyQueueStep(step) == queueStepFinal { if outcome.claimedSteer != nil { - c.nextModelInputs = append(c.nextModelInputs, sdk.UserMessage(continuationPayloadText(outcome.claimedSteer.Payload))) + c.nextModelInputs = append(c.nextModelInputs, sdk.UserMessage(QueuePayloadText(outcome.claimedSteer.Payload))) } c.continueAfterFinal.Store(true) } - c.publishQueueUserTurns(context.WithoutCancel(ctx), stepIndex, outcome) + if queueErr == nil { + c.publishQueueUserTurns(context.WithoutCancel(ctx), stepIndex, outcome) + } } else { persisted, err = c.persister.PersistAgentStep(context.WithoutCancel(ctx), agentStep) } @@ -207,6 +228,9 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s c.memoryPersisted = append(c.memoryPersisted, persisted...) c.messages = append(c.messages, messages...) } + if queueErr != nil { + return fail(queueErr) + } return nil } @@ -234,7 +258,7 @@ func (c *agentStepCommitter) publishQueueUserTurns(ctx context.Context, stepInde } if outcome.claimedSteer != nil { update.ClaimedSteerItemID = string(outcome.claimedSteer.ID) - update.ClaimedSteerText = continuationPayloadText(outcome.claimedSteer.Payload) + update.ClaimedSteerText = QueuePayloadText(outcome.claimedSteer.Payload) update.ClaimedSteerTimestamp = outcome.claimedSteer.CreatedAt // Anchor after the step that just committed. Its step_end marker was // emitted by the native loop before the commit barrier ran, so the wait @@ -251,18 +275,6 @@ func (c *agentStepCommitter) publishQueueUserTurns(ctx context.Context, stepInde } } -func agentStepCommitHash(step messagepkg.AgentStep, result *sdk.StepResult) string { - payload, err := json.Marshal(struct { - Step messagepkg.AgentStep `json:"step"` - Result *sdk.StepResult `json:"result"` - }{step, result}) - if err != nil { - return "" - } - sum := sha256.Sum256(payload) - return hex.EncodeToString(sum[:]) -} - func (c *agentStepCommitter) err() error { if c == nil { return nil @@ -318,3 +330,13 @@ func (c *agentStepCommitter) persistedMessages() []messagepkg.Message { defer c.mu.Unlock() return append([]messagepkg.Message(nil), c.persisted...) } + +// The run's owner context remains authoritative even when the SDK supplies a +// detached cleanup context. User abort checkpoints may outlive cancellation; +// a revoked owner must never write in the reaper's grace window. +func stepPersistenceContext(ctx, owner context.Context) (context.Context, error) { + if runOwnershipLost(ctx) || runOwnershipLost(owner) { + return nil, sessionruntime.ErrRunOwnershipLost + } + return context.WithoutCancel(ctx), nil +} diff --git a/internal/agent/application/step_ownership_test.go b/internal/agent/application/step_ownership_test.go new file mode 100644 index 0000000000..994b31fb00 --- /dev/null +++ b/internal/agent/application/step_ownership_test.go @@ -0,0 +1,47 @@ +package application + +import ( + "context" + "errors" + "testing" + + sdk "github.com/felinics/twilight/sdk" + + contextfrag "github.com/felinics/memoh/internal/agent/context/fragment" + "github.com/felinics/memoh/internal/agent/runtime/native" + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" +) + +func TestCheckpointDistinguishesUserAbortFromRevokedOwner(t *testing.T) { + for _, cause := range []error{context.Canceled, sessionruntime.ErrRunOwnershipLost} { + t.Run(cause.Error(), func(t *testing.T) { + store := &recordingStepPersister{recordingMessageService: &recordingMessageService{}} + owner, cancel := context.WithCancelCause(context.Background()) + committer := &agentStepCommitter{service: &Service{}, persister: store, ownerContext: owner, req: ChatRequest{BotID: "bot", ThreadID: "session", RunID: "run", UserMessagePersisted: true}, rc: resolvedContext{runConfig: native.RunConfig{ContextLifecycle: contextfrag.NewLifecycleHolder()}}} + cancel(cause) + err := committer.interrupt(context.WithoutCancel(owner), 0, &sdk.StepResult{Messages: []sdk.Message{sdk.AssistantMessage("partial")}}) + if errors.Is(cause, sessionruntime.ErrRunOwnershipLost) { + if !errors.Is(err, cause) || len(store.steps) != 0 { + t.Fatalf("revoked checkpoint: writes=%d err=%v", len(store.steps), err) + } + } else if err != nil || len(store.steps) != 1 { + t.Fatalf("user abort checkpoint: writes=%d err=%v", len(store.steps), err) + } + }) + } +} + +func TestSubagentRejectsRevokedCheckpointWithDetachedCallback(t *testing.T) { + store := &recordingStepPersister{recordingMessageService: &recordingMessageService{}} + service := &Service{messageService: store} + owner, cancel := context.WithCancelCause(subagentRunContext("bot", "session", 9)) + _, checkpoint := service.SubagentStepCommit(owner, "bot", "session", "model", "request", nil, nil) + if checkpoint == nil { + t.Fatal("missing checkpoint callback") + } + cancel(sessionruntime.ErrRunOwnershipLost) + err := checkpoint(context.WithoutCancel(owner), 0, &sdk.StepResult{Messages: []sdk.Message{sdk.AssistantMessage("late")}}) + if !errors.Is(err, sessionruntime.ErrRunOwnershipLost) || len(store.steps) != 0 { + t.Fatalf("revoked subagent checkpoint: writes=%d err=%v", len(store.steps), err) + } +} diff --git a/internal/agent/application/step_persister_test.go b/internal/agent/application/step_persister_test.go index b9ef58ec84..1640b48492 100644 --- a/internal/agent/application/step_persister_test.go +++ b/internal/agent/application/step_persister_test.go @@ -4,18 +4,20 @@ import ( "context" messagepkg "github.com/felinics/memoh/internal/chat/message" - dbstore "github.com/felinics/memoh/internal/db/store" ) -// recordingStepPersister is shared by subagent step tests. Queue-specific -// coordinator tests were removed; this helper only exercises ordinary history -// step persistence. +// recordingStepPersister records the history boundary shared by native and +// subagent step tests, with an optional persistence failure. type recordingStepPersister struct { *recordingMessageService - steps []messagepkg.AgentStep + steps []messagepkg.AgentStep + stepErr error } func (s *recordingStepPersister) PersistAgentStep(_ context.Context, step messagepkg.AgentStep) ([]messagepkg.Message, error) { + if s.stepErr != nil { + return nil, s.stepErr + } s.steps = append(s.steps, step) result := make([]messagepkg.Message, len(step.Messages)) for i, input := range step.Messages { @@ -24,14 +26,10 @@ func (s *recordingStepPersister) PersistAgentStep(_ context.Context, step messag return result, nil } -func (s *recordingStepPersister) PersistAgentStepTx(ctx context.Context, _ dbstore.Queries, step messagepkg.AgentStep) ([]messagepkg.Message, error) { - return s.PersistAgentStep(ctx, step) -} - -func (s *recordingStepPersister) PersistAgentReplacementStepTx(ctx context.Context, _ dbstore.Queries, step messagepkg.AgentStep) ([]messagepkg.Message, error) { +func (s *recordingStepPersister) PersistAgentReplacementStep(ctx context.Context, step messagepkg.AgentStep) ([]messagepkg.Message, error) { return s.PersistAgentStep(ctx, step) } -func (*recordingStepPersister) FinalizeAgentReplacementTx(context.Context, dbstore.Queries, string, messagepkg.TurnReplacement, string, string) error { +func (*recordingStepPersister) FinalizeAgentReplacement(context.Context, string, messagepkg.TurnReplacement, string, string) error { return nil } diff --git a/internal/agent/application/subagent_abort_control_alignment_test.go b/internal/agent/application/subagent_abort_control_alignment_test.go index 73bd32c659..28b2285d19 100644 --- a/internal/agent/application/subagent_abort_control_alignment_test.go +++ b/internal/agent/application/subagent_abort_control_alignment_test.go @@ -30,7 +30,7 @@ func (b *terminalLoadFaultBackend) Load(ctx context.Context, key sessionruntime. } func TestSpawnCleanEndRacingAbortControlAlignsAllTerminals(t *testing.T) { - runs := &abortAlignmentLedger{} + runs := newAbortAlignmentLedger() backend := &terminalLoadFaultBackend{MemoryBackend: sessionruntime.NewMemoryBackend()} manager := sessionruntime.NewManager(backend, sessionruntime.Options{ OwnerID: "clean-end-abort-owner", diff --git a/internal/agent/application/subagent_runtime_alignment_test.go b/internal/agent/application/subagent_runtime_alignment_test.go index 5cc607e60f..853f08ea20 100644 --- a/internal/agent/application/subagent_runtime_alignment_test.go +++ b/internal/agent/application/subagent_runtime_alignment_test.go @@ -3,7 +3,6 @@ package application import ( "context" "errors" - "sync" "testing" "time" @@ -13,6 +12,7 @@ import ( sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/agent/runtime/session/ledger" tools "github.com/felinics/memoh/internal/agent/tool" + "github.com/felinics/memoh/internal/testutil/sessionledger" ) type abortAlignmentProvider struct { @@ -57,174 +57,10 @@ type abortAlignmentFence struct{} func (abortAlignmentFence) Activate(context.Context, string, string, int64) error { return nil } -type abortAlignmentLedger struct { - mu sync.Mutex - run ledger.Run - token int64 -} - -var ( - _ sessionruntime.FenceActivator = abortAlignmentFence{} - _ ledger.Store = (*abortAlignmentLedger)(nil) -) - -func (s *abortAlignmentLedger) Admit(_ context.Context, params ledger.AdmitParams) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != "" { - if s.run.SessionID == params.SessionID && s.run.InvocationID == params.InvocationID { - return s.run, false, nil - } - if s.run.SessionID == params.SessionID && s.run.State.Active() { - return ledger.Run{}, false, ledger.ErrSessionBusy - } - } - s.run = ledger.Run{ - RunID: params.RunID, - BotID: params.BotID, - SessionID: params.SessionID, - InvocationID: params.InvocationID, - TurnID: params.TurnID, - TurnPosition: 1, - State: ledger.StateAccepted, - Input: append([]byte(nil), params.Input...), - InputFingerprint: params.InputFingerprint, - CreatedAt: time.Now(), - } - return s.run, true, nil -} - -func (s *abortAlignmentLedger) Get(_ context.Context, runID string) (ledger.Run, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != runID { - return ledger.Run{}, ledger.ErrRunNotFound - } - return s.run, nil -} - -func (s *abortAlignmentLedger) GetByInvocation(_ context.Context, sessionID, invocationID string) (ledger.Run, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.SessionID != sessionID || s.run.InvocationID != invocationID { - return ledger.Run{}, ledger.ErrRunNotFound - } - return s.run, nil -} - -func (s *abortAlignmentLedger) ActiveRun(_ context.Context, sessionID string) (ledger.Run, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.SessionID != sessionID || !s.run.State.Active() { - return ledger.Run{}, ledger.ErrRunNotFound - } - return s.run, nil -} - -func (s *abortAlignmentLedger) LatestRun(_ context.Context, sessionID string) (ledger.Run, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.SessionID != sessionID { - return ledger.Run{}, ledger.ErrRunNotFound - } - return s.run, nil -} - -func (s *abortAlignmentLedger) NextFencingToken(context.Context) (int64, error) { - s.mu.Lock() - defer s.mu.Unlock() - s.token++ - return s.token, nil -} - -func (s *abortAlignmentLedger) Claim(_ context.Context, params ledger.ClaimParams) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != params.RunID || s.run.State != ledger.StateAccepted { - return ledger.Run{}, false, nil - } - s.run.State = ledger.StateRunning - s.run.OwnerID = params.OwnerID - s.run.FencingToken = params.FencingToken - s.run.LiveGeneration = params.LiveGeneration - s.run.OwnerSince = time.Now() - return s.run, true, nil -} - -func (s *abortAlignmentLedger) SetWaitingDecision(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { - return s.transition(runID, token, ledger.StateWaitingDecision) -} - -func (s *abortAlignmentLedger) Resume(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { - return s.transition(runID, token, ledger.StateRunning) -} - -func (s *abortAlignmentLedger) transition(runID string, token int64, state ledger.State) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != runID || s.run.FencingToken != token || s.run.State.Terminal() || s.run.State == ledger.StateFinishing { - return ledger.Run{}, false, nil - } - s.run.State = state - return s.run, true, nil -} - -func (s *abortAlignmentLedger) PrepareFinish(_ context.Context, params ledger.PrepareFinishParams) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != params.RunID || s.run.FencingToken != params.FencingToken || s.run.State.Terminal() || - (s.run.State == ledger.StateWaitingDecision && !params.AllowWaitingDecision) { - return s.run, false, nil - } - if s.run.State != ledger.StateFinishing { - s.run.State = ledger.StateFinishing - s.run.ProposedState = params.State - s.run.ProposedErrorCode = params.ErrorCode - s.run.ProposedErrorMessage = params.ErrorMessage - s.run.FinishProposedAt = time.Now() - } - return s.run, true, nil -} - -func (s *abortAlignmentLedger) Finalize(_ context.Context, params ledger.FinalizeParams) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != params.RunID || s.run.FencingToken != params.FencingToken || s.run.State.Terminal() { - return s.run, false, nil - } - if s.run.State == ledger.StateFinishing { - s.run.State = s.run.ProposedState - s.run.ErrorCode = s.run.ProposedErrorCode - s.run.ErrorMessage = s.run.ProposedErrorMessage - } else { - s.run.State = params.State - s.run.ErrorCode = params.ErrorCode - s.run.ErrorMessage = params.ErrorMessage - } - s.run.UpdatedAt = time.Now() - return s.run, true, nil -} - -func (s *abortAlignmentLedger) RequestAbort(_ context.Context, runID string) (ledger.Run, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.run.RunID != runID || s.run.State.Terminal() || s.run.State == ledger.StateFinishing { - return s.run, false, nil - } - s.run.AbortRequestedAt = time.Now() - return s.run, true, nil -} - -func (*abortAlignmentLedger) StaleGenerationRuns(context.Context, ledger.StaleGenerationQuery) ([]ledger.Run, error) { - return nil, nil -} - -func (*abortAlignmentLedger) OrphanedRuns(context.Context, ledger.OrphanQuery) ([]ledger.Run, error) { - return nil, nil -} +func newAbortAlignmentLedger() *sessionledger.Store { return sessionledger.New() } func TestSpawnAbortAlignsManagerLedgerAndLifecycle(t *testing.T) { - runs := &abortAlignmentLedger{} + runs := newAbortAlignmentLedger() manager := sessionruntime.NewManager(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ OwnerID: "abort-alignment-owner", OwnerLeaseTTL: time.Minute, @@ -301,7 +137,7 @@ func TestSpawnAbortAlignsManagerLedgerAndLifecycle(t *testing.T) { } func TestSpawnWatchdogRetryKeepsManagerRunActive(t *testing.T) { - runs := &abortAlignmentLedger{} + runs := newAbortAlignmentLedger() manager := sessionruntime.NewManager(sessionruntime.NewMemoryBackend(), sessionruntime.Options{ OwnerID: "watchdog-retry-owner", OwnerLeaseTTL: time.Minute, diff --git a/internal/agent/application/subagent_step_commit.go b/internal/agent/application/subagent_step_commit.go index 08092c3f98..9b2a897e4d 100644 --- a/internal/agent/application/subagent_step_commit.go +++ b/internal/agent/application/subagent_step_commit.go @@ -70,6 +70,7 @@ func (s *Service) SubagentStepCommit( } committer := &subagentStepCommitter{ persister: persister, + ownerContext: ctx, runID: handle.RunID, botID: botID, sessionID: sessionID, @@ -86,11 +87,12 @@ func (s *Service) SubagentStepCommit( // carries no ChatRequest: the subagent path has no resolved chat context, and // its user message is persisted by the spawn provider before execution starts. type subagentStepCommitter struct { - persister messagepkg.AgentStepPersister - runID string - botID string - sessionID string - modelID string + ownerContext context.Context + persister messagepkg.AgentStepPersister + runID string + botID string + sessionID string + modelID string // turnRequestMessageID binds every step row to the task's persisted user // message, so the whole run files under the turn admission allocated // instead of splitting into history-minted turns. @@ -114,6 +116,11 @@ func (c *subagentStepCommitter) persist(ctx context.Context, stepIndex int, step if c == nil || step == nil { return errors.New("agent step is missing") } + persistCtx, ownershipErr := stepPersistenceContext(ctx, c.ownerContext) + if ownershipErr != nil { + return ownershipErr + } + ctx = persistCtx c.mu.Lock() defer c.mu.Unlock() if stepIndex != c.nextStep { diff --git a/internal/agent/application/turn_admission.go b/internal/agent/application/turn_admission.go index 4076ab5632..3d8fb7f4d7 100644 --- a/internal/agent/application/turn_admission.go +++ b/internal/agent/application/turn_admission.go @@ -31,7 +31,7 @@ import ( // stub here would only assert that the stub agrees with itself. type turnAdmitter interface { Admit(context.Context, sessionruntime.AdmitInput) (sessionruntime.Admission, error) - FinishRun(ctx context.Context, handle sessionruntime.RunHandle, status, message string) error + FinishRunWithErrorCode(ctx context.Context, handle sessionruntime.RunHandle, status, errorCode string) error // MarkInlineDecisionRun declares an admitted run's decision semantics: // its runtime blocks inline on decisions, so terminal decision statuses // resume the run (external drivers). Native runs skip the declaration @@ -39,10 +39,6 @@ type turnAdmitter interface { MarkInlineDecisionRun(botID, sessionID, runID string) } -type codedTurnFinisher interface { - FinishRunWithErrorCode(ctx context.Context, handle sessionruntime.RunHandle, status, errorCode string) error -} - // SetSessionRuntime injects the durable admission gate. Setter injection rather // than a constructor argument because the manager and this service are wired // into the same fx graph and each is reachable from the other's dependencies. @@ -158,7 +154,7 @@ func (s *Service) admitTurnRun( // The same retry identity naming different content. Whichever side is // wrong, running it would double-answer a message that already has a // run, so it is dropped exactly like a redelivery. - return sessionruntime.Admission{}, fmt.Errorf("%w: %s conflicts with an earlier submission", turn.ErrDuplicateTurn, invocationID) + return sessionruntime.Admission{}, fmt.Errorf("%w: %s: %w", turn.ErrDuplicateTurn, invocationID, sessionruntime.ErrInvocationConflict) case err != nil: return sessionruntime.Admission{}, fmt.Errorf("admit turn: %w", err) } @@ -212,14 +208,7 @@ func (s *Service) turnRunFinisher(ctx context.Context, admission sessionruntime. errorCode := strings.TrimSpace(string(apperror.CodeOf(cause))) ctx, cancel := context.WithTimeout(writeCtx, terminalWriteTimeout) defer cancel() - var err error - if coded, ok := s.sessionRuntime.(codedTurnFinisher); ok && errorCode != "" { - err = coded.FinishRunWithErrorCode(ctx, handle, status, errorCode) - } else { - // Compatibility implementations still receive only stable codes; raw - // provider diagnostics never cross this terminal boundary. - err = s.sessionRuntime.FinishRun(ctx, handle, status, errorCode) - } + err := s.sessionRuntime.FinishRunWithErrorCode(ctx, handle, status, errorCode) switch { case err == nil: if !staged && (status != "" || cause != nil) { diff --git a/internal/agent/application/turn_admission_integration_test.go b/internal/agent/application/turn_admission_integration_test.go index 4785f1a077..eb07ae6416 100644 --- a/internal/agent/application/turn_admission_integration_test.go +++ b/internal/agent/application/turn_admission_integration_test.go @@ -2,6 +2,7 @@ package application import ( "context" + "encoding/json" "log/slog" "os" "strings" @@ -15,6 +16,7 @@ import ( sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/agent/runtime/session/ledger" "github.com/felinics/memoh/internal/agent/turn" + chatview "github.com/felinics/memoh/internal/agent/view" dbpkg "github.com/felinics/memoh/internal/db" "github.com/felinics/memoh/internal/db/dbtest" dbsqlc "github.com/felinics/memoh/internal/db/postgres/sqlc" @@ -88,7 +90,17 @@ func TestAdmitTurnRunRequestUserTurnReachesSubscriber(t *testing.T) { if event.Snapshot == nil || event.Snapshot.CurrentRunView == nil { t.Fatal("snapshot has no current run, want the admitted run") } - request := event.Snapshot.CurrentRunView.RequestUserTurn + data, err := json.Marshal(event.Snapshot.CurrentRunView) + if err != nil { + t.Fatal(err) + } + var wire struct { + RequestUserTurn *chatview.UITurn `json:"request_user_turn"` + } + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatal(err) + } + request := wire.RequestUserTurn if request == nil { t.Fatal("snapshot request_user_turn is nil — the #1044 regression shape") } diff --git a/internal/agent/application/turn_admission_test.go b/internal/agent/application/turn_admission_test.go index d9a6a851a9..5270c05ddb 100644 --- a/internal/agent/application/turn_admission_test.go +++ b/internal/agent/application/turn_admission_test.go @@ -33,7 +33,7 @@ func (f *fakeTurnAdmitter) Admit(_ context.Context, input sessionruntime.AdmitIn }, nil } -func (*fakeTurnAdmitter) FinishRun(context.Context, sessionruntime.RunHandle, string, string) error { +func (*fakeTurnAdmitter) FinishRunWithErrorCode(context.Context, sessionruntime.RunHandle, string, string) error { return nil } diff --git a/internal/agent/application/turn_inject_ownership_test.go b/internal/agent/application/turn_inject_ownership_test.go index 92c4d8bd8c..3f2d9314ba 100644 --- a/internal/agent/application/turn_inject_ownership_test.go +++ b/internal/agent/application/turn_inject_ownership_test.go @@ -37,7 +37,7 @@ func (a *injectOwnershipAdmitter) Admit(_ context.Context, input sessionruntime. }, nil } -func (a *injectOwnershipAdmitter) FinishRun(context.Context, sessionruntime.RunHandle, string, string) error { +func (a *injectOwnershipAdmitter) FinishRunWithErrorCode(context.Context, sessionruntime.RunHandle, string, string) error { close(a.finishStarted) <-a.finishRelease a.input.Execution.InjectCh <- turn.InjectMessage{Text: "finishing steer"} diff --git a/internal/agent/application/turn_service_test.go b/internal/agent/application/turn_service_test.go index d1358927a0..37ee5f3942 100644 --- a/internal/agent/application/turn_service_test.go +++ b/internal/agent/application/turn_service_test.go @@ -88,7 +88,7 @@ func (a *scriptedAdmitter) Admit(_ context.Context, in sessionruntime.AdmitInput }, nil } -func (a *scriptedAdmitter) FinishRun(_ context.Context, handle sessionruntime.RunHandle, status, message string) error { +func (a *scriptedAdmitter) FinishRunWithErrorCode(_ context.Context, handle sessionruntime.RunHandle, status, message string) error { a.mu.Lock() defer a.mu.Unlock() a.finishes = append(a.finishes, recordedFinish{handle: handle, status: status, message: message}) diff --git a/internal/agent/runtime/acp/client/client_test.go b/internal/agent/runtime/acp/client/client_test.go index 4b11c432ed..94c83a6fea 100644 --- a/internal/agent/runtime/acp/client/client_test.go +++ b/internal/agent/runtime/acp/client/client_test.go @@ -28,6 +28,7 @@ import ( "github.com/felinics/memoh/internal/config" "github.com/felinics/memoh/internal/mcp" "github.com/felinics/memoh/internal/runtimefence" + sessiontest "github.com/felinics/memoh/internal/testutil/sessionruntime" "github.com/felinics/memoh/internal/workspace/bridge" pb "github.com/felinics/memoh/internal/workspace/bridgepb" "github.com/felinics/memoh/internal/workspace/bridgesvc" @@ -2500,7 +2501,7 @@ func TestACPWorkspaceEffectsRejectStaleRedisOwner(t *testing.T) { t.Fatalf("create stale owner backend: %v", err) } t.Cleanup(func() { _ = rawOwnerBackend.Close() }) - owner := sessionruntime.NewManager(nonClosingDistributedBackend{DistributedBackend: rawOwnerBackend}, sessionruntime.Options{ + owner := sessiontest.New(nonClosingDistributedBackend{DistributedBackend: rawOwnerBackend, LivenessBackend: rawOwnerBackend}, sessionruntime.Options{ OwnerID: "acp-workspace-owner-a", StateTTL: time.Minute, OwnerLeaseTTL: 100 * time.Millisecond, }) if err := owner.Start(ctx); err != nil { @@ -2510,7 +2511,7 @@ func TestACPWorkspaceEffectsRejectStaleRedisOwner(t *testing.T) { if err != nil { t.Fatalf("create takeover backend: %v", err) } - takeover := sessionruntime.NewManager(backendB, sessionruntime.Options{ + takeover := sessiontest.New(backendB, sessionruntime.Options{ OwnerID: "acp-workspace-owner-b", StateTTL: time.Minute, OwnerLeaseTTL: 100 * time.Millisecond, }) if err := takeover.Start(ctx); err != nil { @@ -2524,7 +2525,7 @@ func TestACPWorkspaceEffectsRejectStaleRedisOwner(t *testing.T) { streamA = "stream-acp-workspace-owner-a" streamB = "stream-acp-workspace-owner-b" ) - ownerHandle, err := owner.StartRunHandle(ctx, botID, sessionID, streamA, make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)) + ownerHandle, err := sessiontest.Start(ctx, owner, botID, sessionID, streamA, make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)) if err != nil { t.Fatalf("start stale owner run: %v", err) } @@ -2549,7 +2550,7 @@ func TestACPWorkspaceEffectsRejectStaleRedisOwner(t *testing.T) { if _, err := takeover.Snapshot(ctx, botID, sessionID); err != nil { t.Fatalf("reconcile expired owner: %v", err) } - if err := takeover.StartRun(ctx, botID, sessionID, streamB, make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)); err != nil { + if _, err := sessiontest.Start(ctx, takeover, botID, sessionID, streamB, make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)); err != nil { t.Fatalf("start takeover run: %v", err) } @@ -2590,6 +2591,7 @@ func TestACPWorkspaceEffectsRejectStaleRedisOwner(t *testing.T) { } type nonClosingDistributedBackend struct { + sessionruntime.LivenessBackend sessionruntime.DistributedBackend } diff --git a/internal/agent/runtime/native/agent.go b/internal/agent/runtime/native/agent.go index 698e9064ff..47a747b2c8 100644 --- a/internal/agent/runtime/native/agent.go +++ b/internal/agent/runtime/native/agent.go @@ -443,6 +443,7 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv } prepareStep, committedStepMessages := capturePreparedStepMessages(prepareStep) + committedStepMessages.byStep[0] = cloneProviderMessages(cfg.initialStepInputs) if readMediaState != nil { committedStepMessages.addAdmissionObserver(readMediaState.reconcilePreparedMessages) } @@ -473,9 +474,8 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv // marker can name the durable step index the following commit will use. emittedStep := 0 onStepCommitted := func(ctx context.Context, stepIndex int, step *sdk.StepResult) error { - stepIndex += cfg.StepIndexOffset if cfg.OnStepCommitted != nil { - if err := cfg.OnStepCommitted(ctx, stepIndex, committedStepMessages.decorate(stepIndex, step, toolExecutionMetadata)); err != nil { + if err := cfg.OnStepCommitted(ctx, cfg.StepIndexOffset+stepIndex, committedStepMessages.decorate(stepIndex, step, toolExecutionMetadata)); err != nil { return err } } @@ -814,7 +814,7 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv stepIndex := nextDurableStep if step := interruptedStep.snapshot(stepIndex); step != nil { step = committedStepMessages.decorate(stepIndex, step, toolExecutionMetadata) - if err := cfg.OnStepInterrupted(context.WithoutCancel(streamCtx), stepIndex, step); err != nil { + if err := cfg.OnStepInterrupted(ctx, cfg.StepIndexOffset+stepIndex, step); err != nil { // An owner that lost its lease, or a run another writer already // finalized, is an expected outcome of racing an abort. a.logger.Warn("persist interrupted model step failed", slog.Any("error", err)) @@ -895,13 +895,7 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv // steer becomes the next model input instead of being stranded after the // terminal event. if streamClosed && !aborted && streamResult != nil && cfg.ContinueAfterFinal != nil && cfg.ContinueAfterFinal.Swap(false) { - cfg.Messages = append(append([]sdk.Message(nil), cfg.Messages...), streamResult.Messages...) - if cfg.NextModelInputs != nil { - cfg.Messages = append(cfg.Messages, (*cfg.NextModelInputs)...) - *cfg.NextModelInputs = nil - } - cfg.StepIndexOffset += len(streamResult.Steps) - cfg.SuppressAgentStart = true + cfg = appendSteerContinuation(cfg, streamResult.Messages, len(streamResult.Steps)) // The completed invocation must stop its emitters before the continuation // starts; the continuation gets a fresh child context from the original // run context so closing the old stream does not cancel it. @@ -1062,6 +1056,7 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat } prepareStep, committedStepMessages := capturePreparedStepMessages(prepareStep) + committedStepMessages.byStep[0] = cloneProviderMessages(cfg.initialStepInputs) if readMediaState != nil { committedStepMessages.addAdmissionObserver(readMediaState.reconcilePreparedMessages) } @@ -1098,8 +1093,7 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat ) if cfg.OnStepCommitted != nil { opts = append(opts, sdk.WithOnStepCommitted(func(ctx context.Context, stepIndex int, step *sdk.StepResult) error { - stepIndex += cfg.StepIndexOffset - return cfg.OnStepCommitted(ctx, stepIndex, committedStepMessages.decorate(stepIndex, step, toolExecutionMetadata)) + return cfg.OnStepCommitted(ctx, cfg.StepIndexOffset+stepIndex, committedStepMessages.decorate(stepIndex, step, toolExecutionMetadata)) })) } @@ -1148,13 +1142,7 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat } finalMessages = toolExecutionMetadata.annotate(finalMessages) if cfg.ContinueAfterFinal != nil && cfg.ContinueAfterFinal.Swap(false) && len(genResult.Steps) > 0 { - cfg.Messages = append(append([]sdk.Message(nil), cfg.Messages...), finalMessages...) - if cfg.NextModelInputs != nil { - cfg.Messages = append(cfg.Messages, (*cfg.NextModelInputs)...) - *cfg.NextModelInputs = nil - } - cfg.StepIndexOffset += len(genResult.Steps) - cfg.SuppressAgentStart = true + cfg = appendSteerContinuation(cfg, finalMessages, len(genResult.Steps)) next, nextErr := a.runGenerate(genCtx, cfg) if nextErr != nil { return nil, nextErr @@ -1173,6 +1161,55 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat }, nil } +// appendSteerContinuation advances a completed invocation without changing its +// run identity or replaying its start event. Both execution modes share this +// input/step transition; their output delivery and finalizers remain separate. +func appendSteerContinuation(cfg RunConfig, messages []sdk.Message, steps int) RunConfig { + appended := append([]sdk.Message(nil), messages...) + cfg.initialStepInputs = nil + if cfg.NextModelInputs != nil { + cfg.initialStepInputs = cloneProviderMessages(*cfg.NextModelInputs) + appended = append(appended, cfg.initialStepInputs...) + *cfg.NextModelInputs = nil + } + cfg.Messages = append(append([]sdk.Message(nil), cfg.Messages...), appended...) + cfg.StepIndexOffset += steps + cfg.SuppressAgentStart = true + if len(cfg.initialStepInputs) > 0 { + current := len(cfg.Messages) - 1 + cfg.ContextCurrentUserMessageIndex = ¤t + } + if len(cfg.ContextSourceFrags) > 0 { + // The production applier renders typed sources, not cfg.Messages. + // Continue from the already-selected context and append this invocation's + // new messages, keeping system/workspace provenance intact. + frags := append([]contextfrag.ContextFrag(nil), cfg.ContextFrags...) + if len(frags) == 0 { + frags = append(frags, cfg.ContextSourceFrags...) + } + lastIndex := -1 + for i := range frags { + if frags[i].Provenance.Index > lastIndex { + lastIndex = frags[i].Provenance.Index + } + if frags[i].Kind == contextfrag.KindCurrentUserMessage { + frags[i].Kind = contextfrag.KindConversationEvent + frags[i].Slot = contextfrag.SlotHistory + } + } + current := len(appended) - 1 + added := contextfrag.CompileFrags(contextfrag.CompileInput{Scope: cfg.ContextScope, Messages: appended, CurrentUserMessageIndex: ¤t}) + for i := range added { + added[i].ID = fmt.Sprintf("continuation.%d.%03d", cfg.StepIndexOffset, i) + added[i].Provenance.Index = lastIndex + 1 + i + added[i].Budget.Overflow = contextfrag.OverflowKeep + } + frags = append(frags, added...) + cfg.ContextSourceFrags = frags + } + return cfg +} + func (a *Agent) buildGenerateOptions(ctx context.Context, cfg RunConfig, tools []sdk.Tool, approvalTools []sdk.Tool, prepareStep func(*sdk.GenerateParams) *sdk.GenerateParams) []sdk.GenerateOption { handoff := newProviderAttemptHandoff(cfg) tools = canonicalizeProviderToolSchemas(tools) diff --git a/internal/agent/runtime/native/provider_stream_observer.go b/internal/agent/runtime/native/provider_stream_observer.go index 940e2be296..02d6d500a9 100644 --- a/internal/agent/runtime/native/provider_stream_observer.go +++ b/internal/agent/runtime/native/provider_stream_observer.go @@ -16,7 +16,17 @@ func modelWithProviderStreamEventObserver(model *sdk.Model, observe func(StreamE return model } observed := *model - observed.Provider = providerStreamEventObserver{Provider: model.Provider, observe: observe} + provider := model.Provider + // A final-steer continuation reuses the model from the preceding call. + // Replace our observer instead of nesting another stream/notification loop. + for { + previous, ok := provider.(providerStreamEventObserver) + if !ok { + break + } + provider = previous.Provider + } + observed.Provider = providerStreamEventObserver{Provider: provider, observe: observe} return &observed } diff --git a/internal/agent/runtime/native/stream_test.go b/internal/agent/runtime/native/stream_test.go index ae69d5d721..80e407f7e2 100644 --- a/internal/agent/runtime/native/stream_test.go +++ b/internal/agent/runtime/native/stream_test.go @@ -109,6 +109,7 @@ func TestAgentStreamReopensAfterFinalSteer(t *testing.T) { t.Parallel() var calls atomic.Int32 var secondInput atomic.Bool + var observedText atomic.Int32 var continueAfter atomic.Bool nextInputs := []sdk.Message{} provider := agentStreamTestProvider(func(_ context.Context, params sdk.GenerateParams) (*sdk.StreamResult, error) { @@ -128,6 +129,11 @@ func TestAgentStreamReopensAfterFinalSteer(t *testing.T) { Model: &sdk.Model{ID: "mock-model", Provider: provider}, Messages: []sdk.Message{sdk.UserMessage("hello")}, Identity: SessionContext{BotID: "bot-1"}, ContinueAfterFinal: &continueAfter, NextModelInputs: &nextInputs, + OnProviderStreamEventObserved: func(event StreamEvent) { + if event.Type == EventTextDelta { + observedText.Add(1) + } + }, OnStepCommitted: func(_ context.Context, _ int, _ *sdk.StepResult) error { commits++ if commits == 1 { @@ -137,12 +143,22 @@ func TestAgentStreamReopensAfterFinalSteer(t *testing.T) { return nil }, }) - var terminal int + var terminal, starts int + var steps []int for event := range events { + if event.Type == EventAgentStart { + starts++ + } + if event.Type == EventStepEnd { + steps = append(steps, event.StepNumber) + } if event.IsTerminal() { terminal++ } } + if observedText.Load() != 2 || starts != 1 || len(steps) != 2 || steps[0] != 0 || steps[1] != 1 { + t.Fatalf("duplicate observer/start or wrong step offset: observed=%d starts=%d steps=%v", observedText.Load(), starts, steps) + } if calls.Load() != 2 || !secondInput.Load() || terminal != 1 { t.Fatalf("calls=%d second_input=%v terminal=%d", calls.Load(), secondInput.Load(), terminal) } @@ -357,7 +373,7 @@ func TestAgentStreamPersistsInterruptedInferenceStep(t *testing.T) { Messages: []sdk.Message{sdk.UserMessage("keep streaming")}, Identity: SessionContext{BotID: "bot-1"}, OnStepInterrupted: func(callbackCtx context.Context, stepIndex int, step *sdk.StepResult) error { - if callbackCtx.Err() != nil || stepIndex != 0 { + if !errors.Is(context.Cause(callbackCtx), context.Canceled) || stepIndex != 0 { t.Errorf("callback context/index = %v/%d", callbackCtx.Err(), stepIndex) } interrupted = step diff --git a/internal/agent/runtime/native/types.go b/internal/agent/runtime/native/types.go index 6caff58d00..aeaa18f0f1 100644 --- a/internal/agent/runtime/native/types.go +++ b/internal/agent/runtime/native/types.go @@ -178,6 +178,7 @@ type RunConfig struct { providerAttemptState *providerAttemptState providerMessageProvenance preparedMessageProvenance preparedStepMessages *stepMessageCapture + initialStepInputs []sdk.Message contextStepFailure func(error) SessionType string LiveToolStream bool @@ -238,7 +239,8 @@ type RunConfig struct { // OnStepInterrupted persists text/reasoning emitted by the current model // call when cancellation arrives before finish-step. Tool-call steps never - // use this path. + // use this path. It retains the original run cancellation cause so the + // persistence adapter can reject ownership loss before detaching for IO. OnStepInterrupted func(ctx context.Context, stepIndex int, step *sdk.StepResult) error // BackgroundManager provides access to the background task system. diff --git a/internal/agent/runtime/session/acceptance/README.md b/internal/agent/runtime/session/acceptance/README.md index 703a5b95cc..d420b0356c 100644 --- a/internal/agent/runtime/session/acceptance/README.md +++ b/internal/agent/runtime/session/acceptance/README.md @@ -114,11 +114,21 @@ container or erase the configured Redis database. The dedicated sets the crash guard explicitly. Do not enable either fault against a shared development or production environment. -## Current implementation boundary - -These are target-contract tests, not compatibility tests for the old -per-WebSocket stream registry. Until the production cutover is complete, cases -that require `run_accepted`, `runtime_subscribe`, the durable ledger, or -cross-instance control are expected to fail. A compiling/skipped suite only -proves that the acceptance harness is valid; it does not prove the runtime -contract is implemented. +## Execution and fault boundaries + +These tests exercise the current public runtime, not the retired per-WebSocket +registry. A skipped or merely compiled suite is not acceptance evidence. + +`TestQueueFollowUpsPreserveRepeatedReorderAndDrain` checks serial follow-up +admission after two reorder operations. `TestQueueSteerDecisionKeepsInputAndHistory` +checks a final-step steer, an ask_user pause, another steer admitted while parked, +and persisted user inputs after the decision continuation. + +`TestSRDUR002PreparedFinishSurvivesProcessCrash` is opt-in with +`MEMOH_SESSION_RUNTIME_ACCEPTANCE_CRASH=1` and requires two Servers. Its temporary +PostgreSQL trigger gates only the generated invocation's terminal UPDATE after +the finishing proposal has committed. The real owner is killed, the gate is +released, and the peer must converge to the original outcome with unchanged +history and an available active slot. The trigger is removed and the owner +restarted in cleanup. This fault intentionally writes test-only database objects; +normal ledger probes remain read-only. Never run it against a shared database. diff --git a/internal/agent/runtime/session/acceptance/queue_contract_test.go b/internal/agent/runtime/session/acceptance/queue_contract_test.go new file mode 100644 index 0000000000..4494365c1a --- /dev/null +++ b/internal/agent/runtime/session/acceptance/queue_contract_test.go @@ -0,0 +1,204 @@ +//go:build integration + +package acceptance + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" +) + +func enqueueTestInput(t *testing.T, fixture acceptanceFixture, sessionID, kind, text string) string { + t.Helper() + var item struct { + ID string `json:"item_id"` + } + if err := fixture.api.request(http.MethodPost, "/bots/"+fixture.botID+"/sessions/"+sessionID+"/"+kind+"-queue", map[string]string{"invocation_id": uniqueMarker("queue"), "text": text}, &item, http.StatusAccepted); err != nil { + t.Fatal(err) + } + if item.ID == "" { + t.Fatal("queue response has no item identity") + } + return item.ID +} + +func TestQueueFollowUpsPreserveRepeatedReorderAndDrain(t *testing.T) { + fixture := requireFixture(t, false) + prepareFakeModel(t) + sessionID := mustCreateSession(t, fixture, "queue-drain") + marker := uniqueMarker("queue-origin") + invocation := "invocation-" + marker + conn := mustDial(t, loadEnvironment().primaryURL, fixture) + defer closeWebSocket(conn) + mustSubscribeAndReadSnapshot(t, conn, sessionID) + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "block")) + if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { + t.Fatal("origin never reached model") + } + defer globalFakeModel.Release(marker) + ids := make([]string, 3) + markers := make([]string, 3) + for i := range ids { + markers[i] = uniqueMarker(fmt.Sprintf("follow-%d", i)) + ids[i] = enqueueTestInput(t, fixture, sessionID, "follow-up", directive(markers[i], 2, 5)) + } + endpoint := "/bots/" + fixture.botID + "/sessions/" + sessionID + "/follow-up-queue/reorder" + for _, move := range [][2]int{{2, 0}, {1, 0}} { + var response any + if err := fixture.api.request(http.MethodPut, endpoint, map[string]any{"item": map[string]string{"item_id": ids[move[0]]}, "before": map[string]string{"item_id": ids[move[1]]}}, &response, http.StatusOK); err != nil { + t.Fatal(err) + } + } + globalFakeModel.Release(marker) + mustReadRunTerminal(t, conn, admitted.RunID) + var priorPosition int64 = -1 + for _, i := range []int{2, 1, 0} { + completed := mustWaitRunState(t, sessionID, "follow-up:"+ids[i], func(run sessionRunRecord) bool { return run.State == "completed" }) + if completed.TurnPosition <= priorPosition { + t.Fatalf("queue order regressed: position %d after %d", completed.TurnPosition, priorPosition) + } + priorPosition = completed.TurnPosition + assertTerminalHistory(t, completed) + if count := globalFakeModel.RequestCount(markers[i]); count != 1 { + t.Fatalf("follow-up %d executed %d times", i, count) + } + } + var queues struct { + FollowUp []any `json:"follow_up"` + } + if err := fixture.api.request(http.MethodGet, "/bots/"+fixture.botID+"/sessions/"+sessionID+"/queue", nil, &queues, http.StatusOK); err != nil { + t.Fatal(err) + } + if len(queues.FollowUp) != 0 { + t.Fatalf("queue did not drain: %v", queues.FollowUp) + } +} + +func TestQueueSteerDecisionKeepsInputAndHistory(t *testing.T) { + testQueueSteerDecisionKeepsInputAndHistory(t, false) +} + +func TestQueueSteerDecisionSurvivesOwnerRestart(t *testing.T) { + if !envBool(crashEnv) { + t.Skipf("set %s=1 only against the isolated acceptance topology", crashEnv) + } + testQueueSteerDecisionKeepsInputAndHistory(t, true) +} + +func testQueueSteerDecisionKeepsInputAndHistory(t *testing.T, restartOwner bool) { + t.Helper() + fixture := requireFixture(t, restartOwner) + prepareFakeModel(t) + sessionID := mustCreateSession(t, fixture, "steer-decision") + marker := uniqueMarker("steer-origin") + invocation := "invocation-" + marker + conn := mustDial(t, loadEnvironment().primaryURL, fixture) + defer closeWebSocket(conn) + mustSubscribeAndReadSnapshot(t, conn, sessionID) + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "block")) + if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { + t.Fatal("origin never reached model") + } + defer globalFakeModel.Release(marker) + steerMarker := uniqueMarker("steer-decision") + steerText := directiveMode(steerMarker, 2, 5, "ask_user") + " ask after steering" + enqueueTestInput(t, fixture, sessionID, "steer", steerText) + globalFakeModel.Release(marker) + waiting := mustWaitRunState(t, sessionID, invocation, func(run sessionRunRecord) bool { return run.State == "waiting_decision" }) + decision := mustPendingUserInput(t, waiting) + answers, err := firstDecisionAnswer(decision.UIPayload) + if err != nil { + t.Fatal(err) + } + afterDecision := directive(uniqueMarker("after-decision"), 2, 5) + " continue after the answer" + enqueueTestInput(t, fixture, sessionID, "steer", afterDecision) + if restartOwner { + env := loadEnvironment() + if err := killAndRestartPrimary(env); err != nil { + t.Fatalf("restart owner after steered decision: %v", err) + } + closeWebSocket(conn) + recovered := mustWaitRunState(t, sessionID, invocation, func(run sessionRunRecord) bool { + return run.State == "waiting_decision" && run.FencingToken > waiting.FencingToken + }) + pending := mustPendingUserInput(t, recovered) + if pending.ID != decision.ID || recovered.RunID != admitted.RunID || recovered.TurnID != waiting.TurnID { + t.Fatalf("recovery changed durable control identity: decision=%+v run=%+v", pending, recovered) + } + conn = mustDial(t, peerURL(env), fixture) + defer closeWebSocket(conn) + mustSubscribeAndReadSnapshot(t, conn, sessionID) + } + control := "control-" + uniqueMarker("steer-answer") + if err := sendUserInputResponse(conn, sessionID, admitted.RunID, decision.ID, control, answers); err != nil { + t.Fatal(err) + } + ack := mustReadControlAck(t, conn, control) + if !ack.Applied { + t.Fatalf("steer decision was not accepted: %+v", ack) + } + mustReadRunCompleted(t, conn, admitted.RunID) + completed := mustWaitRunState(t, sessionID, invocation, func(run sessionRunRecord) bool { return run.State == "completed" }) + history, err := fixture.api.history(fixture.botID, sessionID) + if err != nil { + t.Fatal(err) + } + if !historyContainsRoleText(history, "user", steerText) { + t.Fatalf("applied steer input missing from durable history: %#v", history) + } + if !historyContainsRoleText(history, "user", afterDecision) { + t.Fatalf("post-decision steer missing from history: %#v", history) + } + if completed.RunID != admitted.RunID { + t.Fatal("steer created a second run") + } + ctx, cancel := context.WithTimeout(context.Background(), databaseTimeout) + defer cancel() + if status, err := requireLedger(t).userInputStatus(ctx, decision.ID); err != nil || status != "submitted" { + t.Fatalf("decision status=%s err=%v", status, err) + } + // Presence alone would miss duplicate input or continuation output written + // under the root turn after owner recovery. Inspect explicit DB membership. + rows, err := requireLedger(t).pool.Query(ctx, ` +SELECT turn_id::text, min(turn_position), + count(*) FILTER (WHERE role = 'user'), + count(*) FILTER (WHERE role = 'assistant'), + count(*) FILTER (WHERE role = 'tool'), + count(*) FILTER (WHERE run_id IS DISTINCT FROM $2::uuid) +FROM bot_history_messages +WHERE session_id = $1::uuid +GROUP BY turn_id +ORDER BY min(turn_position)`, sessionID, admitted.RunID) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + segments := 0 + for rows.Next() { + var turnID string + var position int64 + var users, assistants, tools, wrongRun int + if err := rows.Scan(&turnID, &position, &users, &assistants, &tools, &wrongRun); err != nil { + t.Fatal(err) + } + segments++ + wantTools := 0 + if segments == 2 { + wantTools = 1 + } + if users != 1 || assistants == 0 || tools != wantTools || wrongRun != 0 || position != int64(segments) { + t.Errorf("segment %d: turn=%s position=%d users=%d assistants=%d tools=%d wrong_run=%d", segments, turnID, position, users, assistants, tools, wrongRun) + } + if (segments == 1) != (turnID == completed.TurnID) { + t.Errorf("segment %d incorrectly uses root control turn %s", segments, completed.TurnID) + } + } + if err := rows.Err(); err != nil { + t.Fatal(err) + } + if segments != 3 { + t.Fatalf("got %d durable turn segments, want origin + steer + post-decision steer", segments) + } +} diff --git a/internal/agent/runtime/session/acceptance/terminal_crash_test.go b/internal/agent/runtime/session/acceptance/terminal_crash_test.go new file mode 100644 index 0000000000..d6284a0104 --- /dev/null +++ b/internal/agent/runtime/session/acceptance/terminal_crash_test.go @@ -0,0 +1,148 @@ +//go:build integration + +package acceptance + +import ( + "context" + "fmt" + "net/http" + "os/exec" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5" +) + +// This fault lives in the isolated test database, not in production code. It +// blocks only this invocation's terminal UPDATE, after its immutable proposal +// committed. Killing the real owner in that window must preserve the proposal. +func TestSRDUR002PreparedFinishSurvivesProcessCrash(t *testing.T) { + if !envBool(crashEnv) { + t.Skipf("set %s=1 for the isolated process-crash scenario", crashEnv) + } + fixture := requireFixture(t, true) + prepareFakeModel(t) + env := loadEnvironment() + sessionID := mustCreateSession(t, fixture, "finish-proposal-crash") + marker := uniqueMarker("finish-proposal") + invocation := "invocation-" + marker + conn := mustDial(t, env.primaryURL, fixture) + defer closeWebSocket(conn) + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 2, 0, "block")) + if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { + t.Fatal("run never reached the model") + } + defer globalFakeModel.Release(marker) + probe := requireLedger(t) + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + dsn := envOr(postgresURLEnv, "") + if dsn == "" { + t.Fatal("the fault test requires an explicit isolated PostgreSQL URL") + } + gate, err := pgx.Connect(ctx, dsn) + if err != nil { + t.Fatal(err) + } + key := time.Now().UnixNano() + name := fmt.Sprintf("acceptance_finish_%d", key) + fn := pgx.Identifier{"public", name}.Sanitize() + trigger := pgx.Identifier{name}.Sanitize() + if _, err := gate.Exec(ctx, "SELECT pg_advisory_lock($1)", key); err != nil { + _ = gate.Close(context.Background()) + t.Fatal(err) + } + unlocked := false + unlock := func() { + if unlocked { + return + } + cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if _, err := gate.Exec(cleanup, "SELECT pg_advisory_unlock($1)", key); err != nil { + t.Errorf("release terminal gate: %v", err) + } + unlocked = true + } + t.Cleanup(func() { + unlock() + cleanup, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if _, err := gate.Exec(cleanup, "DROP TRIGGER IF EXISTS "+trigger+" ON session_runs; DROP FUNCTION IF EXISTS "+fn+"()"); err != nil { + t.Errorf("remove terminal fault: %v", err) + } + _ = gate.Close(cleanup) + }) + // Identifiers are quoted and the invocation is test-generated, never user SQL. + body := fmt.Sprintf(`CREATE FUNCTION %s() RETURNS trigger LANGUAGE plpgsql AS $gate$ + BEGIN + IF NEW.invocation_id = '%s' THEN PERFORM pg_advisory_xact_lock(%d); END IF; + RETURN NEW; + END $gate$; + CREATE TRIGGER %s BEFORE UPDATE ON session_runs FOR EACH ROW + WHEN (OLD.state = 'finishing' AND NEW.state IN ('completed','aborted','failed','lost')) + EXECUTE FUNCTION %s()`, fn, strings.ReplaceAll(invocation, "'", "''"), key, trigger, fn) + if _, err := gate.Exec(ctx, body); err != nil { + t.Fatal(err) + } + globalFakeModel.Release(marker) + proposed := mustWaitRunState(t, sessionID, invocation, func(run sessionRunRecord) bool { return run.State == "finishing" }) + var proposal string + if err := probe.pool.QueryRow(ctx, "SELECT proposed_terminal_state FROM session_runs WHERE run_id=$1::uuid", proposed.RunID).Scan(&proposal); err != nil || proposal != "completed" { + t.Fatalf("proposal=%q err=%v", proposal, err) + } + before, err := probe.historySummary(ctx, proposed) + if err != nil || before.UserMessages != 1 || before.AssistantMessages == 0 || before.WrongTurnIDs != 0 { + t.Fatalf("proposal precedes durable output: %+v err=%v", before, err) + } + kill := exec.CommandContext(ctx, "docker", "kill", "--signal=KILL", env.primaryContainer) //nolint:gosec // explicit isolated acceptance container + if output, err := kill.CombinedOutput(); err != nil { + t.Fatalf("kill owner: %v: %s", err, output) + } + t.Cleanup(func() { + restart, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + start := exec.CommandContext(restart, "docker", "start", env.primaryContainer) //nolint:gosec // explicit isolated acceptance container + if output, err := start.CombinedOutput(); err != nil { + t.Errorf("restart owner: %v: %s", err, output) + return + } + client := &http.Client{Timeout: time.Second} + for restart.Err() == nil { + request, _ := http.NewRequestWithContext(restart, http.MethodHead, env.primaryURL+"/health", nil) + response, err := client.Do(request) //nolint:gosec // explicit isolated acceptance URL + if err == nil { + _ = response.Body.Close() + if response.StatusCode == http.StatusOK { + return + } + } + time.Sleep(200 * time.Millisecond) + } + t.Error("owner did not restart after fault test") + }) + unlock() + outcome, err := probe.waitRun(ctx, sessionID, invocation, func(run sessionRunRecord) bool { return run.terminal() }) + if err != nil || outcome.State != "completed" { + t.Fatalf("prepared outcome lost after crash: %+v err=%v", outcome, err) + } + if outcome.RunID != admitted.RunID { + t.Fatal("recovery changed run identity") + } + if after := assertTerminalHistory(t, outcome); after != before { + t.Fatalf("recovery duplicated output: before=%+v after=%+v", before, after) + } + peer := mustDial(t, env.secondaryURL, fixture) + defer closeWebSocket(peer) + if err := subscribeRuntime(peer, sessionID); err != nil { + t.Fatal(err) + } + if events, err := readUntil(peer, 10*time.Second, func(event wsEvent) bool { + return eventRunID(event) == outcome.RunID && eventState(event) == "completed" + }); err != nil { + t.Fatalf("durable terminal did not repair live projection: %v events=%+v", err, events) + } + _, next := mustSendAndAccept(t, fixture, peer, sessionID, "next-"+marker, directive(uniqueMarker("after-finish-crash"), 1, 0)) + mustReadRunCompleted(t, peer, next.RunID) +} diff --git a/internal/agent/runtime/session/admit_test.go b/internal/agent/runtime/session/admit_test.go index 2752677b72..073f2a283d 100644 --- a/internal/agent/runtime/session/admit_test.go +++ b/internal/agent/runtime/session/admit_test.go @@ -3,7 +3,6 @@ package sessionruntime import ( "context" "errors" - "sort" "strings" "sync" "testing" @@ -11,369 +10,12 @@ import ( "github.com/felinics/memoh/internal/agent/runtime/native" "github.com/felinics/memoh/internal/agent/runtime/session/ledger" + "github.com/felinics/memoh/internal/testutil/sessionledger" ) -// fakeLedger is an in-memory ledger with the same guarantees the PostgreSQL -// adapter provides: one active run per session, fenced idempotent transitions, -// and a monotonic token sequence. It exists so admission ordering can be tested -// without a database; the adapter's own SQL is covered by its integration test. -type fakeLedger struct { - mu sync.Mutex - runs map[string]*ledger.Run - // bySession preserves insertion order so ActiveRun is deterministic. - order []string - token int64 - - admitErr error - claimErr error - tokenErr error - prepareErr error - finalizeErr error - claimHook func(runID string) - - admits int - claims int - finalized []ledger.FinalizeParams -} - -func newFakeLedger() *fakeLedger { - return &fakeLedger{runs: map[string]*ledger.Run{}} -} - -func (f *fakeLedger) Admit(_ context.Context, params ledger.AdmitParams) (ledger.Run, bool, error) { - f.mu.Lock() - defer f.mu.Unlock() - f.admits++ - if f.admitErr != nil { - return ledger.Run{}, false, f.admitErr - } - for _, id := range f.order { - run := f.runs[id] - if run.SessionID != params.SessionID { - continue - } - if run.InvocationID == params.InvocationID { - return *run, false, nil - } - if run.State.Active() { - return ledger.Run{}, false, ledger.ErrSessionBusy - } - } - position := int64(1) - for _, id := range f.order { - if f.runs[id].SessionID == params.SessionID { - position++ - } - } - run := &ledger.Run{ - RunID: params.RunID, - BotID: params.BotID, - SessionID: params.SessionID, - InvocationID: params.InvocationID, - TurnID: params.TurnID, - TurnPosition: position, - State: ledger.StateAccepted, - Input: params.Input, - InputFingerprint: params.InputFingerprint, - CreatedAt: time.Now(), - } - f.runs[run.RunID] = run - f.order = append(f.order, run.RunID) - return *run, true, nil -} - -func (f *fakeLedger) Get(_ context.Context, runID string) (ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - run, ok := f.runs[runID] - if !ok { - return ledger.Run{}, ledger.ErrRunNotFound - } - return *run, nil -} - -func (f *fakeLedger) GetByInvocation(_ context.Context, sessionID, invocationID string) (ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - for _, id := range f.order { - if run := f.runs[id]; run.SessionID == sessionID && run.InvocationID == invocationID { - return *run, nil - } - } - return ledger.Run{}, ledger.ErrRunNotFound -} - -func (f *fakeLedger) ActiveRun(_ context.Context, sessionID string) (ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - for _, id := range f.order { - if run := f.runs[id]; run.SessionID == sessionID && run.State.Active() { - return *run, nil - } - } - return ledger.Run{}, ledger.ErrRunNotFound -} - -func (f *fakeLedger) LatestRun(_ context.Context, sessionID string) (ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - for i := len(f.order) - 1; i >= 0; i-- { - if run := f.runs[f.order[i]]; run.SessionID == sessionID { - return *run, nil - } - } - return ledger.Run{}, ledger.ErrRunNotFound -} - -func (f *fakeLedger) NextFencingToken(context.Context) (int64, error) { - f.mu.Lock() - defer f.mu.Unlock() - if f.tokenErr != nil { - return 0, f.tokenErr - } - f.token++ - return f.token, nil -} - -func (f *fakeLedger) Claim(_ context.Context, params ledger.ClaimParams) (ledger.Run, bool, error) { - if f.claimHook != nil { - f.claimHook(params.RunID) - } - f.mu.Lock() - defer f.mu.Unlock() - f.claims++ - if f.claimErr != nil { - return ledger.Run{}, false, f.claimErr - } - run, ok := f.runs[params.RunID] - if !ok { - return ledger.Run{}, false, ledger.ErrRunNotFound - } - if run.State != ledger.StateAccepted || run.FencingToken >= params.FencingToken { - return ledger.Run{}, false, nil - } - run.State = ledger.StateRunning - run.OwnerID = params.OwnerID - run.FencingToken = params.FencingToken - run.LiveGeneration = params.LiveGeneration - run.OwnerSince = time.Now() - return *run, true, nil -} - -func (f *fakeLedger) SetWaitingDecision(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { - return f.transition(runID, token, ledger.StateWaitingDecision) -} - -func (f *fakeLedger) Resume(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { - return f.transition(runID, token, ledger.StateRunning) -} - -func (f *fakeLedger) transition(runID string, token int64, state ledger.State) (ledger.Run, bool, error) { - f.mu.Lock() - defer f.mu.Unlock() - run, ok := f.runs[runID] - if !ok || run.FencingToken != token || run.State.Terminal() || run.State == ledger.StateFinishing { - return ledger.Run{}, false, nil - } - run.State = state - return *run, true, nil -} - -func (f *fakeLedger) PrepareFinish(_ context.Context, params ledger.PrepareFinishParams) (ledger.Run, bool, error) { - f.mu.Lock() - defer f.mu.Unlock() - if f.prepareErr != nil { - return ledger.Run{}, false, f.prepareErr - } - run, ok := f.runs[params.RunID] - if !ok || run.FencingToken != params.FencingToken || run.State.Terminal() || - (run.State == ledger.StateWaitingDecision && !params.AllowWaitingDecision) { - return ledger.Run{}, false, nil - } - if run.State != ledger.StateFinishing { - run.State = ledger.StateFinishing - run.ProposedState = params.State - run.ProposedErrorCode = params.ErrorCode - run.ProposedErrorMessage = params.ErrorMessage - run.FinishProposedAt = time.Now() - } - return *run, true, nil -} +type fakeLedger = sessionledger.Store -func (f *fakeLedger) Finalize(_ context.Context, params ledger.FinalizeParams) (ledger.Run, bool, error) { - f.mu.Lock() - defer f.mu.Unlock() - if f.finalizeErr != nil { - return ledger.Run{}, false, f.finalizeErr - } - run, ok := f.runs[params.RunID] - if !ok || run.FencingToken != params.FencingToken || run.State.Terminal() { - return ledger.Run{}, false, nil - } - state := params.State - errorCode := params.ErrorCode - errorMessage := params.ErrorMessage - if run.State == ledger.StateFinishing { - state = run.ProposedState - errorCode = run.ProposedErrorCode - errorMessage = run.ProposedErrorMessage - } else if state == ledger.StateLost && !run.AbortRequestedAt.IsZero() { - state = ledger.StateAborted - errorCode = "" - errorMessage = "" - } - run.State = state - run.ErrorCode = errorCode - run.ErrorMessage = errorMessage - f.finalized = append(f.finalized, params) - return *run, true, nil -} - -func (f *fakeLedger) RequestAbort(_ context.Context, runID string) (ledger.Run, bool, error) { - f.mu.Lock() - defer f.mu.Unlock() - run, ok := f.runs[runID] - if !ok || run.State.Terminal() || run.State == ledger.StateFinishing { - return ledger.Run{}, false, nil - } - run.AbortRequestedAt = time.Now() - return *run, true, nil -} - -// StaleGenerationRuns mirrors the adapter's keyset sweep: active rows that were -// claimed by an incarnation other than the current one, ordered so a cursor can -// page through them. -func (f *fakeLedger) StaleGenerationRuns(_ context.Context, query ledger.StaleGenerationQuery) ([]ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - var matched []ledger.Run - for _, id := range f.order { - run := *f.runs[id] - if !run.State.Active() || run.LiveGeneration == "" || run.LiveGeneration == query.CurrentGeneration { - continue - } - if run.LiveGeneration < query.After.LiveGeneration || - (run.LiveGeneration == query.After.LiveGeneration && run.RunID <= query.After.RunID) { - continue - } - matched = append(matched, run) - } - sort.Slice(matched, func(i, j int) bool { - if matched[i].LiveGeneration != matched[j].LiveGeneration { - return matched[i].LiveGeneration < matched[j].LiveGeneration - } - return matched[i].RunID < matched[j].RunID - }) - if query.Limit > 0 && len(matched) > int(query.Limit) { - matched = matched[:query.Limit] - } - return matched, nil -} - -func (f *fakeLedger) OrphanedRuns(_ context.Context, query ledger.OrphanQuery) ([]ledger.Run, error) { - f.mu.Lock() - defer f.mu.Unlock() - cutoff := time.Now().Add(-query.MinAge) - var matched []ledger.Run - for _, id := range f.order { - run := *f.runs[id] - if run.State != ledger.StateAccepted || run.OwnerID != "" || !run.CreatedAt.Before(cutoff) { - continue - } - matched = append(matched, run) - } - if query.Limit > 0 && len(matched) > int(query.Limit) { - matched = matched[:query.Limit] - } - return matched, nil -} - -// insertOrphan records an admission that committed under a process that died -// before it could claim anything. -func (f *fakeLedger) insertOrphan(runID, sessionID, invocationID, fingerprint string) { - f.mu.Lock() - defer f.mu.Unlock() - run := &ledger.Run{ - RunID: runID, - BotID: testBotID, - SessionID: sessionID, - InvocationID: invocationID, - TurnID: runID + "-turn", - TurnPosition: 1, - State: ledger.StateAccepted, - InputFingerprint: fingerprint, - CreatedAt: time.Now().Add(-time.Hour), - } - f.runs[run.RunID] = run - f.order = append(f.order, run.RunID) -} - -// insertClaimed records a run that some owner took and never finished, which is -// what the reaper finds after that owner disappears. -func (f *fakeLedger) insertClaimed(runID, sessionID string, token int64, generation string) { - f.mu.Lock() - defer f.mu.Unlock() - run := &ledger.Run{ - RunID: runID, - BotID: testBotID, - SessionID: sessionID, - InvocationID: runID + "-inv", - TurnID: runID + "-turn", - TurnPosition: 1, - State: ledger.StateRunning, - OwnerID: "owner-gone", - FencingToken: token, - LiveGeneration: generation, - OwnerSince: time.Now().Add(-time.Minute), - CreatedAt: time.Now().Add(-time.Minute), - } - f.runs[run.RunID] = run - f.order = append(f.order, run.RunID) -} - -func (f *fakeLedger) state(runID string) ledger.State { - f.mu.Lock() - defer f.mu.Unlock() - run, ok := f.runs[runID] - if !ok { - return "" - } - return run.State -} - -func (f *fakeLedger) errorCode(runID string) string { - f.mu.Lock() - defer f.mu.Unlock() - run, ok := f.runs[runID] - if !ok { - return "" - } - return run.ErrorCode -} - -func (f *fakeLedger) setFinalizeErr(err error) { - f.mu.Lock() - defer f.mu.Unlock() - f.finalizeErr = err -} - -func (f *fakeLedger) setPrepareErr(err error) { - f.mu.Lock() - defer f.mu.Unlock() - f.prepareErr = err -} - -func (f *fakeLedger) counts() (admits, claims int) { - f.mu.Lock() - defer f.mu.Unlock() - return f.admits, f.claims -} - -func (f *fakeLedger) terminalWrites() []ledger.FinalizeParams { - f.mu.Lock() - defer f.mu.Unlock() - return append([]ledger.FinalizeParams(nil), f.finalized...) -} +func newFakeLedger() *fakeLedger { return sessionledger.New() } type recordedActivation struct { botID string @@ -602,7 +244,7 @@ func TestAdmitDuplicateInvocationDoesNotExecuteTwice(t *testing.T) { if *f.executions != 1 { t.Fatalf("executions = %d, want 1", *f.executions) } - if _, claims := f.runs.counts(); claims != 1 { + if _, claims := f.runs.Counts(); claims != 1 { t.Fatalf("claims = %d, want 1", claims) } } @@ -667,10 +309,10 @@ func TestAdmitLostClaimReportsStateWithoutStarting(t *testing.T) { t.Parallel() f := newAdmitFixture(t) // Simulate the peer winning between this call's admission and its claim. - f.runs.claimHook = func(runID string) { - f.runs.mu.Lock() - defer f.runs.mu.Unlock() - if run, ok := f.runs.runs[runID]; ok && run.State == ledger.StateAccepted { + f.runs.ClaimHook = func(runID string) { + f.runs.Mu.Lock() + defer f.runs.Mu.Unlock() + if run, ok := f.runs.Runs[runID]; ok && run.State == ledger.StateAccepted { run.State = ledger.StateRunning run.OwnerID = "peer" run.FencingToken = 99 @@ -710,7 +352,7 @@ func TestAdmitReleasesClaimWhenFenceActivationFails(t *testing.T) { if *f.executions != 0 { t.Fatalf("executions = %d, want 0", *f.executions) } - writes := f.runs.terminalWrites() + writes := f.runs.TerminalWrites() if len(writes) != 1 { t.Fatalf("terminal writes = %d, want 1", len(writes)) } @@ -737,7 +379,7 @@ func TestAdmitReleasesClaimWhenExecutionCannotStart(t *testing.T) { if _, err := f.manager.Admit(context.Background(), in); err == nil { t.Fatal("admit should fail when the run cannot start") } - writes := f.runs.terminalWrites() + writes := f.runs.TerminalWrites() if len(writes) != 1 || writes[0].State != ledger.StateFailed { t.Fatalf("terminal writes = %+v, want one failed write", writes) } @@ -753,7 +395,7 @@ func TestAdmitAdoptsOwnerlessAdmission(t *testing.T) { t.Parallel() f := newAdmitFixture(t) in := f.input("inv-orphan", `{"text":"a"}`) - f.runs.insertOrphan("run-orphan", testSessionID, "inv-orphan", fingerprintPayload(in.Payload)) + f.runs.InsertOrphan("run-orphan", testSessionID, "inv-orphan", fingerprintPayload(in.Payload)) admission, err := f.manager.Admit(context.Background(), in) if err != nil { @@ -805,7 +447,7 @@ func TestAdmitRequiresExecutionBuilder(t *testing.T) { if err == nil { t.Fatal("admit without an execution builder should fail") } - if admits, _ := f.runs.counts(); admits != 0 { + if admits, _ := f.runs.Counts(); admits != 0 { t.Fatalf("admits = %d, want 0; validation must precede persistence", admits) } } @@ -832,7 +474,7 @@ func TestAdmittedRunParksAndResumesOnUserInputDecision(t *testing.T) { }); err != nil { t.Fatalf("publish user input request: %v", err) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateWaitingDecision { + if got := fixture.runs.State(admission.RunID); got != ledger.StateWaitingDecision { t.Fatalf("ledger state = %q, want waiting_decision", got) } @@ -859,7 +501,7 @@ func TestAdmittedRunParksAndResumesOnUserInputDecision(t *testing.T) { }); err != nil { t.Fatalf("resume decision run: %v", err) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateRunning { + if got := fixture.runs.State(admission.RunID); got != ledger.StateRunning { t.Fatalf("ledger state after resume = %q, want running", got) } if _, err := fixture.manager.HandleAgentEvent(context.Background(), handle, native.StreamEvent{ @@ -870,7 +512,7 @@ func TestAdmittedRunParksAndResumesOnUserInputDecision(t *testing.T) { if err := fixture.manager.FinishRun(context.Background(), handle, "", ""); err != nil { t.Fatalf("finish resumed run: %v", err) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateCompleted { + if got := fixture.runs.State(admission.RunID); got != ledger.StateCompleted { t.Fatalf("ledger state after completion = %q, want completed", got) } } diff --git a/internal/agent/runtime/session/command_transport_test.go b/internal/agent/runtime/session/command_transport_test.go new file mode 100644 index 0000000000..bd80cfedca --- /dev/null +++ b/internal/agent/runtime/session/command_transport_test.go @@ -0,0 +1,84 @@ +package sessionruntime + +import ( + "context" + "strconv" + "strings" + "time" +) + +// dispatchTestCommand supplies explicit command envelopes to the transport +// contract tests. Those tests use live reservations, not durable admission. +// Public decision identity/ledger validation is exercised by RouteDecisionResponse +// tests and the process-level acceptance suite, not a legacy production router. +func (m *Manager) dispatchTestCommand(ctx context.Context, botID, sessionID, commandType, targetID string, payload []byte) (bool, error) { + snapshot, err := m.Snapshot(ctx, botID, sessionID) + if err != nil { + return false, err + } + run := snapshot.CurrentRunView + targetID, present := runtimeCommandTargetID(run, commandType, targetID) + if !present { + return false, nil + } + cmd := Command{ + Type: commandType, ID: testCommandID(botID, sessionID, run, commandType, targetID), + BotID: botID, SessionID: sessionID, RunID: run.RunID, Generation: run.Generation, + TargetID: targetID, DecisionResolved: true, + Payload: payload, PayloadHash: activeCommandPayloadHash(commandType, payload), + } + loadCtx, cancel := context.WithTimeout(ctx, min(m.commandTimeout(), 100*time.Millisecond)) + result, found, err := m.loadCommandResult(loadCtx, cmd.ID) + cancel() + if err != nil { + return true, err + } + if found { + return true, commandResultErrorFor(cmd, result) + } + if !isActiveRunStatus(run.Status) { + return false, nil + } + now, err := m.backend.Now(ctx) + if err != nil { + return true, err + } + cmd.CreatedAt, cmd.ExpiresAt = now, now.Add(m.commandTimeout()) + if m.distributed == nil || run.OwnerID == m.ownerID { + return true, commandResultErrorFor(cmd, m.executeRoutedCommand(ctx, cmd)) + } + return true, m.dispatchRemoteCommand(ctx, run.OwnerID, cmd) +} + +func testCommandID(botID, sessionID string, run *CurrentRunView, commandType, targetID string) string { + return decisionControlCommandID(commandType, botID, targetID, + strings.Join([]string{sessionID, run.RunID, run.Generation}, ":")) +} + +func runtimeCommandTargetID(run *CurrentRunView, commandType, targetID string) (string, bool) { + targetID = strings.TrimSpace(targetID) + if run == nil || targetID == "" { + return "", false + } + for _, message := range run.Messages { + switch commandType { + case CommandToolApprovalResponse: + if message.Approval != nil && (strings.TrimSpace(message.Approval.ApprovalID) == targetID || strconv.Itoa(message.Approval.ShortID) == targetID) { + canonical := strings.TrimSpace(message.Approval.ApprovalID) + if canonical == "" { + canonical = strconv.Itoa(message.Approval.ShortID) + } + return canonical, true + } + case CommandUserInputResponse: + if message.UserInput != nil && (strings.TrimSpace(message.UserInput.UserInputID) == targetID || strconv.Itoa(message.UserInput.ShortID) == targetID) { + canonical := strings.TrimSpace(message.UserInput.UserInputID) + if canonical == "" { + canonical = strconv.Itoa(message.UserInput.ShortID) + } + return canonical, true + } + } + } + return "", false +} diff --git a/internal/agent/runtime/session/commands.go b/internal/agent/runtime/session/commands.go index 2492c96c92..4a6fb71d89 100644 --- a/internal/agent/runtime/session/commands.go +++ b/internal/agent/runtime/session/commands.go @@ -8,14 +8,12 @@ import ( "fmt" "log/slog" "sort" - "strconv" "strings" "time" "github.com/google/uuid" "github.com/felinics/memoh/internal/agent/runtime/session/ledger" - "github.com/felinics/memoh/internal/agent/turn" ) func (m *Manager) RunRef(ctx context.Context, botID, sessionID, runID string) (RunRef, bool, error) { @@ -635,114 +633,6 @@ func (m *Manager) decisionRunRef(ctx context.Context, target DecisionTarget) (Ru }, true, nil } -// DispatchActiveCommand is the legacy projection-based compatibility entry -// point used by older internal callers. New transports use -// RouteDecisionResponse and never use CurrentRunView.Messages for routing. -func (m *Manager) DispatchActiveCommand(ctx context.Context, botID, sessionID, commandType, targetID string, payload []byte) (bool, error) { - if m == nil || m.backend == nil { - return false, nil - } - botID = strings.TrimSpace(botID) - sessionID = strings.TrimSpace(sessionID) - targetID = strings.TrimSpace(targetID) - if botID == "" || sessionID == "" || targetID == "" { - return false, nil - } - if commandType != CommandToolApprovalResponse && commandType != CommandUserInputResponse { - return false, fmt.Errorf("unsupported active runtime command %q", commandType) - } - snapshot, err := m.Snapshot(ctx, botID, sessionID) - if err != nil { - return false, err - } - run := snapshot.CurrentRunView - if run == nil { - return false, nil - } - canonicalTargetID, targetPresent := runtimeCommandTargetID(run, commandType, targetID) - if !targetPresent { - return false, nil - } - cmd := Command{ - Type: commandType, ID: activeCommandID(botID, sessionID, run, commandType, canonicalTargetID), - BotID: botID, SessionID: sessionID, RunID: strings.TrimSpace(run.RunID), - Generation: strings.TrimSpace(run.Generation), TargetID: canonicalTargetID, - Payload: append([]byte(nil), payload...), PayloadHash: activeCommandPayloadHash(commandType, payload), - } - timeout := m.commandTimeout() - if m.distributed != nil { - loadCtx, cancel := context.WithTimeout(ctx, min(timeout, 100*time.Millisecond)) - result, ok, loadErr := m.loadCommandResult(loadCtx, cmd.ID) - cancel() - if loadErr != nil { - return true, loadErr - } else if ok { - return true, commandResultErrorFor(cmd, result) - } - } - if !isActiveRunStatus(run.Status) { - if reconciled, reconcileErr := m.reconcileRoutedCommand(ctx, cmd); reconciled { - if reconcileErr != nil { - return true, reconcileErr - } - result := m.persistCommandResult(ctx, cmd, reconcileErr) - return true, commandResultErrorFor(cmd, result) - } - return false, nil - } - createdAt, err := m.backend.Now(ctx) - if err != nil { - return true, fmt.Errorf("load runtime command time: %w", err) - } - cmd.CreatedAt = createdAt - cmd.ExpiresAt = createdAt.Add(timeout) - if m.distributed == nil { - commandCtx, cancel, commandErr := m.activeCommandContext(ctx, cmd) - defer cancel() - if commandErr != nil { - return true, commandErr - } - return true, m.applyRoutedCommand(commandCtx, cmd) - } - ownerID := strings.TrimSpace(run.OwnerID) - if ownerID == "" { - return true, errors.New("target runtime owner is unknown") - } - if ownerID == m.ownerID { - result := m.executeRoutedCommand(ctx, cmd) - return true, commandResultErrorFor(cmd, result) - } - dispatchErr := m.dispatchRemoteCommand(ctx, ownerID, cmd) - if dispatchErr != nil { - if reconciled, reconcileErr := m.reconcileRoutedCommand(ctx, cmd); reconciled { - if reconcileErr != nil { - return true, reconcileErr - } - result := m.persistCommandResult(ctx, cmd, reconcileErr) - return true, commandResultErrorFor(cmd, result) - } - } - return true, dispatchErr -} - -// DispatchRunCommand is the transport-facing decision route. In addition to -// the canonical decision id it checks the server-issued run id, preventing a -// stale UI response from being applied to a newer run in the same session. -func (m *Manager) DispatchRunCommand(ctx context.Context, botID, sessionID, runID, commandType, targetID string, payload []byte) (bool, error) { - runID = strings.TrimSpace(runID) - if runID == "" { - return false, nil - } - snapshot, err := m.Snapshot(ctx, botID, sessionID) - if err != nil { - return false, err - } - if snapshot.CurrentRunView == nil || strings.TrimSpace(snapshot.CurrentRunView.RunID) != runID { - return false, nil - } - return m.DispatchActiveCommand(ctx, botID, sessionID, commandType, targetID, payload) -} - func (m *Manager) dispatchRemoteCommand(ctx context.Context, ownerID string, cmd Command) error { ownerID = strings.TrimSpace(ownerID) cmd.ID = strings.TrimSpace(cmd.ID) @@ -782,15 +672,6 @@ func (m *Manager) dispatchRemoteCommand(ctx context.Context, ownerID string, cmd return m.waitCommandResult(ctx, cmd, waiter.result, m.commandTimeout(), ownerID) } -func activeCommandID(botID, sessionID string, run *CurrentRunView, commandType, targetID string) string { - parts := []string{ - strings.TrimSpace(botID), strings.TrimSpace(sessionID), strings.TrimSpace(run.RunID), - strings.TrimSpace(run.Generation), strings.TrimSpace(commandType), strings.TrimSpace(targetID), - } - sum := sha256.Sum256([]byte(strings.Join(parts, "\x00"))) - return fmt.Sprintf("active-response-%x", sum[:]) -} - func commandPayloadHash(payload []byte) string { sum := sha256.Sum256(payload) return fmt.Sprintf("sha256:%x", sum[:]) @@ -921,124 +802,15 @@ func (m *Manager) requestAbort(ctx context.Context, ctrl *runControl) (bool, err run.UpdatedAt = now return snapshot, true, nil }, func(snapshot Snapshot) RuntimeDelta { - return runtimeRunPatch(snapshot, true, false, false, false) + return runtimeRunPatch(snapshot, true, false, false) }) return acknowledged, err } -func (m *Manager) Steer(ctx context.Context, botID, sessionID, runID, text string) (SteerState, error) { - return m.steer(ctx, botID, sessionID, runID, "", text) -} - -func (m *Manager) SteerRun(ctx context.Context, handle RunHandle, text string) (SteerState, error) { - handle = handle.normalized() - if !handle.valid() { - return SteerState{}, ErrRunOwnershipLost - } - return m.steer(ctx, handle.BotID, handle.SessionID, handle.RunID, handle.Generation, text) -} - -func (m *Manager) steer(ctx context.Context, botID, sessionID, runID, expectedGeneration, text string) (SteerState, error) { - if m == nil || m.backend == nil { - return SteerState{}, errors.New("session runtime manager is not configured") - } - botID = strings.TrimSpace(botID) - sessionID = strings.TrimSpace(sessionID) - runID = strings.TrimSpace(runID) - expectedGeneration = strings.TrimSpace(expectedGeneration) - text = strings.TrimSpace(text) - if botID == "" || sessionID == "" || text == "" { - return SteerState{}, errors.New("bot_id, session_id, and text are required") - } - snapshot, err := m.Snapshot(ctx, botID, sessionID) - if err != nil { - return SteerState{}, err - } - if snapshot.CurrentRunView == nil { - return SteerState{}, errors.New("no active runtime run") - } - if runID == "" { - runID = strings.TrimSpace(snapshot.CurrentRunView.RunID) - } - if snapshot.CurrentRunView.RunID != runID { - return SteerState{}, errors.New("target runtime run is not active") - } - if expectedGeneration != "" && strings.TrimSpace(snapshot.CurrentRunView.Generation) != expectedGeneration { - return SteerState{}, ErrRunOwnershipLost - } - generation := strings.TrimSpace(snapshot.CurrentRunView.Generation) - if expectedGeneration != "" { - generation = expectedGeneration - } - handle := RunHandle{BotID: botID, SessionID: sessionID, RunID: runID, Generation: generation}.normalized() - var steer SteerState - var ownerID string - var commandGeneration string - var commandCreatedAt time.Time - _, _, err = m.updateActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { - if snapshot.CurrentRunView.RunID != runID || !strings.EqualFold(snapshot.CurrentRunView.Status, RunStatusRunning) { - return snapshot, false, errors.New("target runtime run is not active") - } - if snapshot.CurrentRunView.Steer != nil && isPendingSteerStatus(snapshot.CurrentRunView.Steer.Status) { - return snapshot, false, errors.New("another runtime steer command is still pending") - } - steer = SteerState{ - ID: uuid.NewString(), - Status: SteerStatusPending, - Text: text, - CreatedAt: now, - UpdatedAt: now, - } - commandCreatedAt = now - snapshot.Seq++ - snapshot.UpdatedAt = now - snapshot.CurrentRunView.Steer = &steer - snapshot.CurrentRunView.UpdatedAt = now - ownerID = strings.TrimSpace(snapshot.CurrentRunView.OwnerID) - commandGeneration = strings.TrimSpace(snapshot.CurrentRunView.Generation) - return snapshot, true, nil - }, func(snapshot Snapshot) RuntimeDelta { - return runtimeRunPatch(snapshot, false, false, true, false) - }) - if err != nil { - return SteerState{}, err - } - - cmd := Command{ - Type: CommandSteer, BotID: botID, SessionID: sessionID, RunID: runID, - Generation: commandGeneration, SteerID: steer.ID, Text: text, CreatedAt: commandCreatedAt, - ExpiresAt: commandCreatedAt.Add(m.commandTimeout()), - } - if ctrl := m.localControlForHandle(handle); ctrl != nil { - m.applyCommand(ctx, cmd) - } else { - if ownerID == "" { - return steer, errors.New("target runtime owner is unknown") - } - if m.distributed == nil { - return steer, errors.New("active runtime is not local") - } - if err := m.distributed.PublishCommand(ctx, ownerID, cmd); err != nil { - _ = m.updateSteerStatus(context.WithoutCancel(ctx), handle, steer.ID, SteerStatusRejected, err.Error()) - return steer, err - } - } - m.rejectPendingSteerAfterTimeout(context.WithoutCancel(ctx), handle, steer.ID) - return steer, nil -} - func (m *Manager) applyCommand(ctx context.Context, cmd Command) { switch strings.TrimSpace(cmd.Type) { case CommandAbort, CommandToolApprovalResponse, CommandUserInputResponse, CommandHistoryReset: m.publishStoredCommandResult(ctx, cmd, m.executeRoutedCommand(ctx, cmd)) - case CommandSteer: - commandCtx, cancel, err := m.activeCommandContext(ctx, cmd) - if err != nil { - _ = m.updateSteerStatus(context.WithoutCancel(ctx), runHandleForCommand(cmd), cmd.SteerID, SteerStatusRejected, steerNotAcknowledgedError) - return - } - m.applySteerCommand(commandCtx, cmd) - cancel() case CommandResult: m.completePendingCommand(cmd) } @@ -1112,7 +884,7 @@ func (m *Manager) applyRoutedCommand(ctx context.Context, cmd Command) error { if strings.TrimSpace(cmd.Type) == CommandHistoryReset { return m.applyHistoryResetCommand(commandCtx, cmd, ctrl) } - if !cmd.DecisionResolved && !runtimeCommandTargetPresent(run, cmd.Type, cmd.TargetID) { + if !cmd.DecisionResolved { return ErrCommandTargetNotActive } m.mu.Lock() @@ -1158,36 +930,13 @@ func (m *Manager) executeRoutedCommand(ctx context.Context, cmd Command) Command return result } commandCtx, cancel, err := m.activeCommandContext(ctx, cmd) - reconciled := false if err == nil { err = m.applyRoutedCommand(commandCtx, cmd) - if errors.Is(err, ErrCommandTargetNotActive) { - if handled, reconcileErr := m.reconcileRoutedCommand(commandCtx, cmd); handled { - reconciled = true - err = reconcileErr - } - } } cancel() - if reconciled && err != nil { - return newCommandResult(cmd, err) - } return m.persistCommandResult(ctx, cmd, err) } -func (m *Manager) reconcileRoutedCommand(ctx context.Context, cmd Command) (bool, error) { - if m == nil { - return false, nil - } - m.mu.Lock() - reconciler := m.commandReconciler - m.mu.Unlock() - if reconciler == nil { - return false, nil - } - return reconciler(ctx, cmd) -} - func newCommandResult(request Command, err error) Command { result := Command{ Type: CommandResult, ID: request.ID, BotID: request.BotID, SessionID: request.SessionID, @@ -1539,185 +1288,3 @@ func (m *Manager) releaseCommandAdmission(cmd Command) { delete(m.admittedCommands, strings.TrimSpace(cmd.ID)) m.mu.Unlock() } - -func runtimeCommandTargetID(run *CurrentRunView, commandType, targetID string) (string, bool) { - targetID = strings.TrimSpace(targetID) - if run == nil || targetID == "" { - return "", false - } - for _, message := range run.Messages { - switch commandType { - case CommandToolApprovalResponse: - if message.Approval != nil && (strings.TrimSpace(message.Approval.ApprovalID) == targetID || strconv.Itoa(message.Approval.ShortID) == targetID) { - canonical := strings.TrimSpace(message.Approval.ApprovalID) - if canonical == "" { - canonical = strconv.Itoa(message.Approval.ShortID) - } - return canonical, true - } - case CommandUserInputResponse: - if message.UserInput != nil && (strings.TrimSpace(message.UserInput.UserInputID) == targetID || strconv.Itoa(message.UserInput.ShortID) == targetID) { - canonical := strings.TrimSpace(message.UserInput.UserInputID) - if canonical == "" { - canonical = strconv.Itoa(message.UserInput.ShortID) - } - return canonical, true - } - } - } - return "", false -} - -func runtimeCommandTargetPresent(run *CurrentRunView, commandType, targetID string) bool { - _, ok := runtimeCommandTargetID(run, commandType, targetID) - return ok -} - -func (m *Manager) applySteerCommand(ctx context.Context, cmd Command) { - handle := runHandleForCommand(cmd) - if err := m.ValidateRunOwnership(ctx, handle); err != nil { - _ = m.updateSteerStatus(context.WithoutCancel(ctx), handle, cmd.SteerID, SteerStatusRejected, ErrRunOwnershipLost.Error()) - return - } - if !m.steerCommandIsPending(ctx, cmd) { - return - } - ctrl := m.localControlForScope(cmd.BotID, cmd.SessionID, cmd.RunID) - if ctrl == nil || ctrl.generation != strings.TrimSpace(cmd.Generation) { - _ = m.updateSteerStatus(context.WithoutCancel(ctx), handle, cmd.SteerID, SteerStatusRejected, ErrRunOwnershipLost.Error()) - return - } - errText := "" - if ctrl.injectCh != nil && strings.TrimSpace(cmd.Text) != "" { - queued, err := m.transitionSteerStatus(ctx, handle, cmd.SteerID, SteerStatusQueued, "") - if err != nil { - m.logger.Warn("acknowledge queued steer failed", slog.Any("error", err), slog.String("run_id", cmd.RunID)) - return - } - if !queued { - return - } - sent, sendError := ctrl.sendInject(ctx, turn.InjectMessage{ - Text: strings.TrimSpace(cmd.Text), - Applied: func() { - if err := m.updateSteerStatus(context.WithoutCancel(ctx), handle, cmd.SteerID, SteerStatusApplied, ""); err != nil { - m.logger.Warn("acknowledge applied steer failed", slog.Any("error", err), slog.String("run_id", cmd.RunID)) - } - }, - }) - if sent { - return - } - errText = sendError - } else { - errText = "active runtime is not available" - } - if err := m.updateSteerStatus(context.WithoutCancel(ctx), handle, cmd.SteerID, SteerStatusRejected, errText); err != nil { - m.logger.Warn("update steer status failed", slog.Any("error", err), slog.String("run_id", cmd.RunID)) - } -} - -func (m *Manager) steerCommandIsPending(ctx context.Context, cmd Command) bool { - if strings.TrimSpace(cmd.SteerID) == "" { - return false - } - snapshot, ok, err := m.backend.Load(ctx, Key{BotID: cmd.BotID, SessionID: cmd.SessionID}) - if err != nil { - m.logger.Warn("load steer state failed", slog.Any("error", err), slog.String("run_id", cmd.RunID)) - return false - } - if !ok || !runMatchesHandle(snapshot.CurrentRunView, runHandleForCommand(cmd)) { - return false - } - steer := snapshot.CurrentRunView.Steer - return steer != nil && steer.ID == strings.TrimSpace(cmd.SteerID) && strings.EqualFold(steer.Status, SteerStatusPending) -} - -func (m *Manager) updateSteerStatus(ctx context.Context, handle RunHandle, steerID, status, errText string) error { - _, err := m.transitionSteerStatus(ctx, handle, steerID, status, errText) - return err -} - -func (m *Manager) transitionSteerStatus(ctx context.Context, handle RunHandle, steerID, status, errText string) (bool, error) { - _, changed, err := m.updateActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { - if !runMatchesHandle(snapshot.CurrentRunView, handle) { - return snapshot, false, nil - } - if snapshot.CurrentRunView.Steer == nil || snapshot.CurrentRunView.Steer.ID != steerID { - return snapshot, false, nil - } - currentStatus := snapshot.CurrentRunView.Steer.Status - if !validSteerTransition(currentStatus, status) { - return snapshot, false, nil - } - snapshot.Seq++ - snapshot.UpdatedAt = now - snapshot.CurrentRunView.UpdatedAt = now - snapshot.CurrentRunView.Steer.Status = status - snapshot.CurrentRunView.Steer.Error = strings.TrimSpace(errText) - snapshot.CurrentRunView.Steer.UpdatedAt = now - return snapshot, true, nil - }, func(snapshot Snapshot) RuntimeDelta { - return runtimeRunPatch(snapshot, false, false, true, false) - }) - return changed, err -} - -const steerNotAcknowledgedError = "runtime steer command was not acknowledged" - -func (m *Manager) rejectPendingSteerAfterTimeout(ctx context.Context, handle RunHandle, steerID string) { - timeout := m.commandAckTTL - if timeout <= 0 { - return - } - time.AfterFunc(timeout, func() { - select { - case <-m.closeCh: - return - default: - } - err := m.rejectUnacknowledgedSteer(ctx, handle, steerID) - if err != nil { - m.logger.Warn("reject pending steer failed", slog.Any("error", err), slog.String("run_id", handle.RunID)) - } - }) -} - -func (m *Manager) rejectUnacknowledgedSteer(ctx context.Context, handle RunHandle, steerID string) error { - _, _, err := m.updateActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { - if !runMatchesHandle(snapshot.CurrentRunView, handle) { - return snapshot, false, nil - } - steer := snapshot.CurrentRunView.Steer - if steer == nil || steer.ID != steerID || !strings.EqualFold(steer.Status, SteerStatusPending) { - return snapshot, false, nil - } - snapshot.Seq++ - snapshot.UpdatedAt = now - snapshot.CurrentRunView.UpdatedAt = now - steer.Status = SteerStatusRejected - steer.Error = steerNotAcknowledgedError - steer.UpdatedAt = now - return snapshot, true, nil - }, func(snapshot Snapshot) RuntimeDelta { - return runtimeRunPatch(snapshot, false, false, true, false) - }) - return err -} - -func isPendingSteerStatus(status string) bool { - return strings.EqualFold(status, SteerStatusPending) || strings.EqualFold(status, SteerStatusQueued) -} - -func validSteerTransition(current, next string) bool { - switch strings.ToLower(strings.TrimSpace(next)) { - case SteerStatusQueued: - return strings.EqualFold(current, SteerStatusPending) - case SteerStatusRejected: - return isPendingSteerStatus(current) - case SteerStatusApplied: - return isPendingSteerStatus(current) - default: - return false - } -} diff --git a/internal/agent/runtime/session/decision_output_test.go b/internal/agent/runtime/session/decision_output_test.go index 77fa485927..39db182a08 100644 --- a/internal/agent/runtime/session/decision_output_test.go +++ b/internal/agent/runtime/session/decision_output_test.go @@ -26,7 +26,7 @@ func TestDecisionOutputConcurrentRetriesAndEarlyAcknowledgement(t *testing.T) { token = int64(7) ) runs := newFakeLedger() - runs.insertClaimed(runID, sessionID, token, "live-generation") + runs.InsertClaimed(runID, sessionID, token, "live-generation") if _, applied, err := runs.SetWaitingDecision(context.Background(), runID, token); err != nil || !applied { t.Fatalf("park fake ledger run: applied=%v err=%v", applied, err) } diff --git a/internal/agent/runtime/session/decision_route_test.go b/internal/agent/runtime/session/decision_route_test.go index 11471e3e67..c56218ad8c 100644 --- a/internal/agent/runtime/session/decision_route_test.go +++ b/internal/agent/runtime/session/decision_route_test.go @@ -9,99 +9,87 @@ import ( "testing" "time" + "github.com/felinics/memoh/internal/agent/runtime/native" "github.com/felinics/memoh/internal/agent/runtime/session/ledger" chatview "github.com/felinics/memoh/internal/agent/view" ) -func TestRunControlCommandContextPreservesOwnershipLossCause(t *testing.T) { - type contextKey struct{} - lifecycleCtx, lifecycleCancel := context.WithCancel( - context.WithValue(context.Background(), contextKey{}, "run-scope"), - ) - ctrl := &runControl{ - lifecycleCtx: lifecycleCtx, - lifecycleCancel: lifecycleCancel, - } - ctx, cancel := ctrl.commandContext(context.Background()) - defer cancel() - if got := ctx.Value(contextKey{}); got != "run-scope" { - t.Fatalf("command context value = %v, want run-scope", got) - } - - ctrl.revokeOwnership(ErrRunOwnershipLost) - ctrl.stopCommands() - - <-ctx.Done() - if cause := context.Cause(ctx); !errors.Is(cause, ErrRunOwnershipLost) { - t.Fatalf("command context cause = %v, want %v", cause, ErrRunOwnershipLost) +func TestRunControlCommandContextCancellation(t *testing.T) { + for _, tc := range []struct { + name string + deadline time.Duration + trigger func(*runControl, context.CancelCauseFunc) + wantErr, wantCause error + }{ + {"ownership loss", 0, func(ctrl *runControl, _ context.CancelCauseFunc) { + ctrl.revokeOwnership(ErrRunOwnershipLost) + ctrl.stopCommands() + }, context.Canceled, ErrRunOwnershipLost}, + {"deadline expiry", 20 * time.Millisecond, nil, context.DeadlineExceeded, context.DeadlineExceeded}, + // The cause can be DeadlineExceeded even when the future deadline has + // not fired. Err must still be Canceled and propagation immediate. + {"parent cancellation", time.Minute, func(_ *runControl, cancel context.CancelCauseFunc) { + cancel(context.DeadlineExceeded) + }, context.Canceled, context.DeadlineExceeded}, + } { + t.Run(tc.name, func(t *testing.T) { + type contextKey struct{} + lifecycle, stop := context.WithCancel(context.WithValue(context.Background(), contextKey{}, "run-scope")) + defer stop() + ctrl := &runControl{lifecycleCtx: lifecycle, lifecycleCancel: stop} + parent, cancelParent := context.WithCancelCause(context.Background()) + defer cancelParent(nil) + if tc.deadline > 0 { + var cancel context.CancelFunc + parent, cancel = context.WithTimeout(parent, tc.deadline) + defer cancel() + } + ctx, cancel := ctrl.commandContext(parent) + defer cancel() + if _, hasDeadline := ctx.Deadline(); hasDeadline != (tc.deadline > 0) { + t.Fatalf("deadline propagated=%v, want %v", hasDeadline, tc.deadline > 0) + } + if got := ctx.Value(contextKey{}); got != "run-scope" { + t.Fatalf("command context value=%v", got) + } + if tc.trigger != nil { + tc.trigger(ctrl, cancelParent) + } + select { + case <-ctx.Done(): + case <-time.After(time.Second): + t.Fatal("command cancellation did not propagate") + } + if !errors.Is(ctx.Err(), tc.wantErr) || !errors.Is(context.Cause(ctx), tc.wantCause) { + t.Fatalf("Err=%v Cause=%v, want %v / %v", ctx.Err(), context.Cause(ctx), tc.wantErr, tc.wantCause) + } + }) } } -// A command carries the acknowledgement deadline of the request that routed -// it. Relaying only cancellation would report every expiry as context.Canceled -// and hide expired commands from callers that branch on DeadlineExceeded. -func TestRunControlCommandContextKeepsParentDeadline(t *testing.T) { - type contextKey struct{} - lifecycleCtx, lifecycleCancel := context.WithCancel( - context.WithValue(context.Background(), contextKey{}, "run-scope"), - ) - defer lifecycleCancel() - ctrl := &runControl{ - lifecycleCtx: lifecycleCtx, - lifecycleCancel: lifecycleCancel, - } - - parent, cancelParent := context.WithTimeout(context.Background(), 20*time.Millisecond) - defer cancelParent() - ctx, cancel := ctrl.commandContext(parent) - defer cancel() - if _, ok := ctx.Deadline(); !ok { - t.Fatal("command context has no deadline") - } - if got := ctx.Value(contextKey{}); got != "run-scope" { - t.Fatalf("command context value = %v, want run-scope", got) - } - - select { - case <-ctx.Done(): - case <-time.After(time.Second): - t.Fatal("command context did not expire with its parent") - } - if err := ctx.Err(); !errors.Is(err, context.DeadlineExceeded) { - t.Fatalf("command context error = %v, want context deadline exceeded", err) - } - if cause := context.Cause(ctx); !errors.Is(cause, context.DeadlineExceeded) { - t.Fatalf("command context cause = %v, want context deadline exceeded", cause) - } -} - -func TestRunControlCommandContextPropagatesCancellationCauseBeforeDeadline(t *testing.T) { - lifecycleCtx, lifecycleCancel := context.WithCancel(context.Background()) - defer lifecycleCancel() - ctrl := &runControl{ - lifecycleCtx: lifecycleCtx, - lifecycleCancel: lifecycleCancel, - } - - sourceCtx, cancelSource := context.WithCancelCause(context.Background()) - parent, cancelDeadline := context.WithDeadline(sourceCtx, time.Now().Add(time.Minute)) - defer cancelDeadline() - ctx, cancel := ctrl.commandContext(parent) - defer cancel() - - // A DeadlineExceeded cause does not mean the deadline itself fired. This - // cancellation must propagate immediately and retain Canceled as ctx.Err(). - cancelSource(context.DeadlineExceeded) - select { - case <-ctx.Done(): - case <-time.After(time.Second): - t.Fatal("command context did not propagate parent cancellation") +// A visible tool block cannot authorize a control command. Only the durable +// decision router may supply the resolved target to owner-side execution. +func TestDecisionCommandRejectsProjectionOnlyTarget(t *testing.T) { + manager := testRuntimeManager(t, NewMemoryBackend(), "projection-only-owner") + handle, err := manager.StartRunHandle(context.Background(), testBotID, testSessionID, testRunID, nil, func() {}, nil) + if err != nil { + t.Fatal(err) } - if err := ctx.Err(); !errors.Is(err, context.Canceled) { - t.Fatalf("command context error = %v, want context canceled", err) + _, err = manager.HandleAgentEvent(context.Background(), handle, native.StreamEvent{ + Type: native.EventUserInputRequest, ToolName: "ask_user", ToolCallID: "call-projection", + UserInputID: "decision-projection", Status: "pending", + }) + if err != nil { + t.Fatal(err) } - if cause := context.Cause(ctx); !errors.Is(cause, context.DeadlineExceeded) { - t.Fatalf("command context cause = %v, want context deadline exceeded", cause) + called := false + manager.SetCommandHandler(func(context.Context, Command) error { called = true; return nil }) + err = manager.applyRoutedCommand(context.Background(), Command{ + Type: CommandUserInputResponse, BotID: testBotID, SessionID: testSessionID, + RunID: testRunID, Generation: handle.Generation, TargetID: "decision-projection", + }) + if !errors.Is(err, ErrCommandTargetNotActive) || called { + t.Fatalf("unresolved command: err=%v handler called=%v", err, called) } } @@ -143,7 +131,7 @@ func TestRouteDecisionResponseUsesDurableTargetAndReplaysAfterTerminal(t *testin token = int64(7) ) runs := newFakeLedger() - runs.insertClaimed(runID, sessionID, token, "live-generation") + runs.InsertClaimed(runID, sessionID, token, "live-generation") if _, applied, err := runs.SetWaitingDecision(context.Background(), runID, token); err != nil || !applied { t.Fatalf("park fake ledger run: applied=%v err=%v", applied, err) } @@ -233,3 +221,71 @@ func TestRouteDecisionResponseUsesDurableTargetAndReplaysAfterTerminal(t *testin } var _ ledger.Store = (*fakeLedger)(nil) + +// Exercise the actual durable decision ingress on both shared backends, with +// no decision present in the UI projection. The transport-only tests construct +// command envelopes directly and cannot substitute for this contract. +func runDistributedDecisionRouteContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { + t.Helper() + ctx := context.Background() + runs := newFakeLedger() + backends := suite.newSharedBackends(t, 2) + owner := testRuntimeManagerWithOptions(t, backends[0], Options{OwnerID: "decision-owner", Ledger: runs, Fence: &fakeFence{}, OwnerLeaseTTL: 2 * time.Second}) + remote := testRuntimeManagerWithOptions(t, backends[1], Options{OwnerID: "decision-remote", Ledger: runs, Fence: &fakeFence{}, OwnerLeaseTTL: 2 * time.Second}) + admission, err := owner.Admit(ctx, AdmitInput{ + BotID: testBotID, SessionID: "durable-decision", InvocationID: "invoke-decision", Payload: []byte(`{"text":"question"}`), + Execution: Execution{Admission: func(context.Context, RunHandle) (RunAdmissionView, error) { return RunAdmissionView{}, nil }, Cancel: func() {}}, + }) + if err != nil { + t.Fatal(err) + } + if _, err := owner.HandleAgentEvent(ctx, admission.Handle, native.StreamEvent{Type: native.EventUserInputRequest, UserInputID: "decision-durable", ToolCallID: "call-durable", Status: "pending"}); err != nil { + t.Fatal(err) + } + store := &fakeDecisionStore{target: DecisionTarget{ + Type: CommandUserInputResponse, ID: "decision-durable", BotID: testBotID, SessionID: "durable-decision", + RunID: admission.RunID, TurnID: admission.TurnID, Status: "pending", FencingToken: admission.Handle.FencingToken, + }} + owner.SetDecisionStore(store) + remote.SetDecisionStore(store) + if _, _, err := backends[0].Update(ctx, Key{BotID: testBotID, SessionID: "durable-decision"}, func(snapshot Snapshot, _ bool) (Snapshot, bool, error) { + snapshot.CurrentRunView.Messages = nil + return snapshot, true, nil + }); err != nil { + t.Fatal(err) + } + var executions atomic.Int32 + owner.SetCommandHandler(func(_ context.Context, cmd Command) error { + if !cmd.DecisionResolved || cmd.FencingToken != admission.Handle.FencingToken { + return errors.New("decision lost its durable ownership") + } + executions.Add(1) + return nil + }) + response := DecisionResponse{Type: CommandUserInputResponse, ControlID: "control-durable", DecisionID: "decision-durable", BotID: testBotID, SessionID: "durable-decision", RunID: admission.RunID, Payload: []byte(`{"answers":[{"question_id":"q1","text":"yes"}]}`)} + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + result, err := remote.RouteDecisionResponse(ctx, response) + if err != nil || !result.Handled || !result.Applied { + t.Errorf("decision result=%+v err=%v", result, err) + } + }() + } + wg.Wait() + if executions.Load() != 1 { + t.Fatalf("duplicate executions=%d", executions.Load()) + } + if err := owner.FinishRun(ctx, admission.Handle, RunStatusAborted, ""); err != nil { + t.Fatal(err) + } + if result, err := remote.RouteDecisionResponse(ctx, response); err != nil || !result.Replayed || !result.Applied { + t.Fatalf("terminal replay=%+v err=%v", result, err) + } + response.Payload = []byte(`{"answers":[{"question_id":"q1","text":"different"}]}`) + if _, err := remote.RouteDecisionResponse(ctx, response); !errors.Is(err, ErrCommandPayloadConflict) { + t.Fatalf("conflicting replay=%v", err) + } +} diff --git a/internal/agent/runtime/session/finalize.go b/internal/agent/runtime/session/finalize.go index b5e907c070..db7bb19e2b 100644 --- a/internal/agent/runtime/session/finalize.go +++ b/internal/agent/runtime/session/finalize.go @@ -79,8 +79,8 @@ func (m *Manager) prepareLedgerFinish( // then resolves a prepared proposal to its intended terminal outcome. A run // that never crossed the durable proposal boundary still becomes `lost`. // -// A zero fencing token means the run was started through a pre-ledger entry -// point and has no durable row to transition, not that fencing was skipped. +// Backend-only reservation tests use zero fencing tokens and have no durable +// row to transition. Production admission always supplies a positive token. func (m *Manager) finalizeLedgerRun(ctx context.Context, handle RunHandle, status, errorCode, message string) (TerminalRun, error) { if m.runs == nil || handle.FencingToken <= 0 { return TerminalRun{}, nil diff --git a/internal/agent/runtime/session/inject_ownership_test.go b/internal/agent/runtime/session/inject_ownership_test.go index 219b5b4547..68626a8d58 100644 --- a/internal/agent/runtime/session/inject_ownership_test.go +++ b/internal/agent/runtime/session/inject_ownership_test.go @@ -26,7 +26,7 @@ func TestFinishRunStopsInjectSendsWithoutClosingBorrowedChannel(t *testing.T) { steerDone := make(chan struct{}) go func() { <-start - _, _ = manager.Steer(context.Background(), testBotID, sessionID, runID, "race teardown") + _, _ = ctrl.sendInject(context.Background(), turn.InjectMessage{Text: "race teardown"}) close(steerDone) }() close(start) diff --git a/internal/agent/runtime/session/live_queue.go b/internal/agent/runtime/session/live_queue.go index f3b2c3b7a2..5e5ff150b6 100644 --- a/internal/agent/runtime/session/live_queue.go +++ b/internal/agent/runtime/session/live_queue.go @@ -2,8 +2,10 @@ package sessionruntime import ( "bytes" + "cmp" "context" "errors" + "slices" "sort" "strings" "time" @@ -45,6 +47,7 @@ const ( ) var ( + ErrQueueSteerUnsupported = errors.New("queue: active run has no steer consumer") ErrQueueNoActiveRun = errors.New("queue: no active run") ErrQueueInvalidReference = errors.New("queue: invalid claim reference") ErrQueueNotPending = errors.New("queue: item is not an accepted pending item") @@ -153,6 +156,11 @@ type followUpQueueState struct { UpdatedAt time.Time `json:"updated_at"` } +// SteerRunAvailable reports whether the current executor accepts new step inputs. +func SteerRunAvailable(run *CurrentRunView) bool { + return run != nil && run.SteerSupported && (run.Status == RunStatusRunning || run.Status == RunStatusWaitingDecision) +} + func activeRun(snapshot Snapshot, ok bool) (*CurrentRunView, bool) { if !ok || snapshot.CurrentRunView == nil || !isActiveRunStatus(snapshot.CurrentRunView.Status) { return nil, false @@ -441,82 +449,75 @@ func nextFollowUpPosition(state followUpQueueState) int64 { return position + 1 } -func reorderSteerState(state *steerQueueState, itemRef, beforeRef SteerPendingRef) ([]SteerItem, error) { - if state == nil || itemRef.ItemID == "" || itemRef.ItemID == beforeRef.ItemID { - return nil, ErrQueueInvalidReference +// reorderQueuePositions sorts lightweight references to the current positions. +// Only the returned public items need payload copies; ordering must not clone +// a second complete queue or construct an ID-to-position map. +func reorderQueuePositions[T any, ID ~string](items []T, item, before ID, describe func(*T) (ID, QueueStatus, *int64)) error { + if item == "" || item == before { + return ErrQueueInvalidReference } - indices := make([]int, 0, len(state.Items)) - itemPos, beforePos := -1, -1 - for i := range state.Items { - if state.Items[i].Status != QueueAccepted { - continue + type positionRef struct { + id ID + position *int64 + } + pending := make([]positionRef, 0, len(items)) + for i := range items { + id, status, position := describe(&items[i]) + if status == QueueAccepted { + pending = append(pending, positionRef{id: id, position: position}) } - indices = append(indices, i) - if state.Items[i].ID == itemRef.ItemID { - itemPos = len(indices) - 1 + } + slices.SortStableFunc(pending, func(a, b positionRef) int { return cmp.Compare(*a.position, *b.position) }) + itemIndex, beforeIndex := -1, -1 + for i, ref := range pending { + if ref.id == item { + itemIndex = i } - if state.Items[i].ID == beforeRef.ItemID { - beforePos = len(indices) - 1 + if ref.id == before { + beforeIndex = i } } - if itemPos < 0 || (beforeRef.ItemID != "" && beforePos < 0) { - return nil, ErrQueueNotPending - } - order := append([]int(nil), indices...) - moving := order[itemPos] - order = append(order[:itemPos], order[itemPos+1:]...) - insertAt := len(order) - if beforeRef.ItemID != "" { - insertAt = 0 - for insertAt < len(order) && state.Items[order[insertAt]].ID != beforeRef.ItemID { - insertAt++ - } + if itemIndex < 0 || (before != "" && beforeIndex < 0) { + return ErrQueueNotPending } - order = append(order, 0) - copy(order[insertAt+1:], order[insertAt:]) - order[insertAt] = moving - for position, index := range order { - state.Items[index].Position = int64(position + 1) + moving := pending[itemIndex] + pending = append(pending[:itemIndex], pending[itemIndex+1:]...) + if before == "" { + beforeIndex = len(pending) + } else if itemIndex < beforeIndex { + beforeIndex-- } - return pendingSteers(*state, 0), nil + pending = append(pending, moving) + copy(pending[beforeIndex+1:], pending[beforeIndex:len(pending)-1]) + pending[beforeIndex] = moving + for i, ref := range pending { + *ref.position = int64(i + 1) + } + return nil } -func reorderFollowUpState(state *followUpQueueState, itemRef, beforeRef FollowUpPendingRef) ([]FollowUpItem, error) { - if state == nil || itemRef.ItemID == "" || itemRef.ItemID == beforeRef.ItemID { +func reorderSteerState(state *steerQueueState, itemRef, beforeRef SteerPendingRef) ([]SteerItem, error) { + if state == nil { return nil, ErrQueueInvalidReference } - indices := make([]int, 0, len(state.Items)) - itemPos, beforePos := -1, -1 - for i := range state.Items { - if state.Items[i].Status != QueueAccepted { - continue - } - indices = append(indices, i) - if state.Items[i].ID == itemRef.ItemID { - itemPos = len(indices) - 1 - } - if state.Items[i].ID == beforeRef.ItemID { - beforePos = len(indices) - 1 - } + err := reorderQueuePositions(state.Items, itemRef.ItemID, beforeRef.ItemID, func(item *SteerItem) (SteerItemID, QueueStatus, *int64) { + return item.ID, item.Status, &item.Position + }) + if err != nil { + return nil, err } - if itemPos < 0 || (beforeRef.ItemID != "" && beforePos < 0) { - return nil, ErrQueueNotPending - } - order := append([]int(nil), indices...) - moving := order[itemPos] - order = append(order[:itemPos], order[itemPos+1:]...) - insertAt := len(order) - if beforeRef.ItemID != "" { - insertAt = 0 - for insertAt < len(order) && state.Items[order[insertAt]].ID != beforeRef.ItemID { - insertAt++ - } + return pendingSteers(*state, 0), nil +} + +func reorderFollowUpState(state *followUpQueueState, itemRef, beforeRef FollowUpPendingRef) ([]FollowUpItem, error) { + if state == nil { + return nil, ErrQueueInvalidReference } - order = append(order, 0) - copy(order[insertAt+1:], order[insertAt:]) - order[insertAt] = moving - for position, index := range order { - state.Items[index].Position = int64(position + 1) + err := reorderQueuePositions(state.Items, itemRef.ItemID, beforeRef.ItemID, func(item *FollowUpItem) (FollowUpItemID, QueueStatus, *int64) { + return item.ID, item.Status, &item.Position + }) + if err != nil { + return nil, err } return pendingFollowUps(*state, 0), nil } diff --git a/internal/agent/runtime/session/live_queue_manager.go b/internal/agent/runtime/session/live_queue_manager.go index e22928b82f..71f02ba054 100644 --- a/internal/agent/runtime/session/live_queue_manager.go +++ b/internal/agent/runtime/session/live_queue_manager.go @@ -1,6 +1,9 @@ package sessionruntime -import "context" +import ( + "context" + "time" +) func (m *Manager) liveQueueBackend() (LiveQueueBackend, error) { if m == nil || m.backend == nil { @@ -13,6 +16,27 @@ func (m *Manager) liveQueueBackend() (LiveQueueBackend, error) { return queue, nil } +// EnableSteer advertises an actual execution consumer, not just an allocated +// channel. Other instances therefore reject queues aimed at old/unsupported owners. +func (m *Manager) EnableSteer(ctx context.Context, handle RunHandle) error { + _, _, err := m.updateActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { + run := snapshot.CurrentRunView + if !runMatchesHandle(run, handle) || !m.runOwnerMatches(run) || + (run.Status != RunStatusRunning && run.Status != RunStatusWaitingDecision) { + return snapshot, false, ErrRunOwnershipLost + } + if run.SteerSupported { + return snapshot, false, nil + } + run.SteerSupported = true + run.UpdatedAt = now + snapshot.Seq++ + snapshot.UpdatedAt = now + return snapshot, true, nil + }, func(snapshot Snapshot) RuntimeDelta { return RuntimeDelta{CurrentRunView: snapshot.CurrentRunView} }) + return err +} + func (m *Manager) EnqueueSteer(ctx context.Context, key Key, itemID, invocationID string, payload []byte) (SteerItem, error) { queue, err := m.liveQueueBackend() if err != nil { diff --git a/internal/agent/runtime/session/live_queue_redis_test.go b/internal/agent/runtime/session/live_queue_redis_test.go index 97e6bbe139..dbdedf797d 100644 --- a/internal/agent/runtime/session/live_queue_redis_test.go +++ b/internal/agent/runtime/session/live_queue_redis_test.go @@ -2,7 +2,6 @@ package sessionruntime import ( "context" - "errors" "os" "testing" "time" @@ -46,7 +45,7 @@ func TestRedisLiveQueueContractOptional(t *testing.T) { snapshot.SessionID = key.SessionID snapshot.CurrentRunView = &CurrentRunView{ RunID: ref.RunID, OwnerID: ref.OwnerID, Generation: ref.Generation, - Status: RunStatusRunning, + SteerSupported: true, Status: RunStatusRunning, } return snapshot, true, nil }) @@ -58,100 +57,5 @@ func TestRedisLiveQueueContractOptional(t *testing.T) { OwnerID: ref.OwnerID, Generation: ref.Generation, FencingToken: ref.FencingToken, } - steerOne, err := first.EnqueueSteer(ctx, key, "steer-1", "invoke-steer-1", []byte("one")) - if err != nil { - t.Fatalf("enqueue steer: %v", err) - } - steerTwo, err := second.EnqueueSteer(ctx, key, "steer-2", "invoke-steer-2", []byte("two")) - if err != nil { - t.Fatalf("enqueue second steer: %v", err) - } - follow, err := second.EnqueueFollowUp(ctx, key, "follow-1", "invoke-follow-1", []byte("follow")) - if err != nil { - t.Fatalf("enqueue follow-up: %v", err) - } - if steerOne.Position >= steerTwo.Position || follow.Position != 1 { - t.Fatalf("independent positions = steer(%d,%d) follow(%d)", steerOne.Position, steerTwo.Position, follow.Position) - } - - steers, follows, err := first.PendingQueues(ctx, key, 0) - if err != nil { - t.Fatalf("list queues from second instance: %v", err) - } - if len(steers) != 2 || string(steers[0].Payload) != "one" || string(steers[1].Payload) != "two" { - t.Fatalf("steer FIFO = %#v", steers) - } - if len(follows) != 1 || string(follows[0].Payload) != "follow" { - t.Fatalf("follow-up queue = %#v", follows) - } - - if _, err := first.ReorderSteer(ctx, key, SteerPendingRef{ItemID: steerTwo.ID}, SteerPendingRef{ItemID: steerOne.ID}); err != nil { - t.Fatalf("accepted-only reorder: %v", err) - } - steers, _, err = second.PendingQueues(ctx, key, 0) - if err != nil || len(steers) != 2 || steers[0].ID != steerTwo.ID || steers[1].ID != steerOne.ID { - t.Fatalf("reordered steer queue = %#v, err=%v", steers, err) - } - - claimed, claim, ok, err := second.ClaimNextSteer(ctx, handle, false) - if err != nil || !ok || claimed.ID != steerTwo.ID { - t.Fatalf("claim steer = %#v, %#v, %v, %v", claimed, claim, ok, err) - } - replayed, replayClaim, ok, err := first.ClaimNextSteer(ctx, handle, false) - if err != nil || !ok || replayed.ID != claimed.ID || replayClaim != claim { - t.Fatalf("cross-instance claim replay = %#v, %#v, %v, %v", replayed, replayClaim, ok, err) - } - stale := claim - stale.OwnerID = "stale-owner" - if err := first.ApplySteer(ctx, key, stale); !errors.Is(err, ErrRunOwnershipLost) { - t.Fatalf("stale steer apply = %v", err) - } - if err := first.ApplySteer(ctx, key, claim); err != nil { - t.Fatalf("apply steer: %v", err) - } - if _, err := second.ReorderSteer(ctx, key, SteerPendingRef{ItemID: steerTwo.ID}, SteerPendingRef{ItemID: steerOne.ID}); !errors.Is(err, ErrQueueNotPending) { - t.Fatalf("claimed/applied reorder = %v", err) - } - promotable, err := first.EnqueueFollowUp(ctx, key, "follow-promote", "invoke-follow-promote", []byte("promote")) - if err != nil { - t.Fatalf("enqueue promotable follow-up: %v", err) - } - promoted, err := second.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: promotable.ID}) - if err != nil { - t.Fatalf("promote follow-up: %v", err) - } - if promoted.Steer.ID == "" || string(promoted.Steer.ID) == string(promotable.ID) { - t.Fatalf("promotion reused follow-up identity: follow=%q steer=%q", promotable.ID, promoted.Steer.ID) - } - replayedPromotion, err := first.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: promotable.ID}) - if err != nil || replayedPromotion.Steer.ID != promoted.Steer.ID { - t.Fatalf("promotion replay = %#v, err=%v", replayedPromotion, err) - } - - followed, followClaim, ok, err := first.ClaimNextFollowUp(ctx, key, ref.RunID) - if err != nil || !ok || followed.ID != follow.ID { - t.Fatalf("claim follow-up = %#v, %#v, %v, %v", followed, followClaim, ok, err) - } - replayedFollowed, replayFollowClaim, ok, err := second.ClaimNextFollowUp(ctx, key, ref.RunID) - if err != nil || !ok || replayedFollowed.ID != follow.ID || replayFollowClaim != followClaim { - t.Fatalf("cross-instance follow-up replay = %#v, %#v, %v, %v", replayedFollowed, replayFollowClaim, ok, err) - } - if err := second.ApplyFollowUp(ctx, key, followClaim); err != nil { - t.Fatalf("apply follow-up: %v", err) - } - if _, _, ok, err := first.ClaimNextFollowUp(ctx, key, "different-terminal-run"); err != nil || ok { - t.Fatalf("applied follow-up was claimable again: ok=%v err=%v", ok, err) - } - - // Terminal close from another instance rejects the remaining steers and - // seals the run while its live snapshot is still active. - if err := second.CloseSteerRun(ctx, key, ref.RunID); err != nil { - t.Fatalf("close steer run: %v", err) - } - if steers, _, err := first.PendingQueues(ctx, key, 0); err != nil || len(steers) != 0 { - t.Fatalf("pending steers after close = %#v, err=%v", steers, err) - } - if _, err := first.EnqueueSteer(ctx, key, "steer-late", "invoke-steer-late", []byte("late")); !errors.Is(err, ErrQueueNoActiveRun) { - t.Fatalf("late steer after close = %v, want %v", err, ErrQueueNoActiveRun) - } + runLiveQueueContract(t, first, second, key, handle) } diff --git a/internal/agent/runtime/session/live_queue_test.go b/internal/agent/runtime/session/live_queue_test.go index b76dc05225..edcd6e004c 100644 --- a/internal/agent/runtime/session/live_queue_test.go +++ b/internal/agent/runtime/session/live_queue_test.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" "testing" + + "github.com/stretchr/testify/require" ) func liveQueueFixture(t *testing.T) (*MemoryBackend, Key, RunHandle) { @@ -14,7 +16,7 @@ func liveQueueFixture(t *testing.T) (*MemoryBackend, Key, RunHandle) { key := Key{BotID: "bot", SessionID: "session"} _, _, err := b.Update(context.Background(), key, func(snapshot Snapshot, _ bool) (Snapshot, bool, error) { snapshot.BotID, snapshot.SessionID = key.BotID, key.SessionID - snapshot.CurrentRunView = &CurrentRunView{RunID: "run-1", TurnID: "turn-1", Generation: "gen-1", OwnerID: "owner-1", Status: RunStatusRunning} + snapshot.CurrentRunView = &CurrentRunView{RunID: "run-1", TurnID: "turn-1", Generation: "gen-1", OwnerID: "owner-1", SteerSupported: true, Status: RunStatusRunning} return snapshot, true, nil }) if err != nil { @@ -24,34 +26,122 @@ func liveQueueFixture(t *testing.T) (*MemoryBackend, Key, RunHandle) { return b, key, handle } -func TestMemoryLiveQueuesAreIndependentAndFIFO(t *testing.T) { - b, key, _ := liveQueueFixture(t) +func TestMemoryLiveQueueContract(t *testing.T) { + b, key, handle := liveQueueFixture(t) + runLiveQueueContract(t, b, b, key, handle) +} + +// Memory runs the same sequence as two independent Redis/Valkey clients. +func runLiveQueueContract(t *testing.T, first, second LiveQueueBackend, key Key, handle RunHandle) { + t.Helper() ctx := context.Background() - s1, err := b.EnqueueSteer(ctx, key, "s1", "i1", []byte("one")) - if err != nil { - t.Fatal(err) - } - s2, err := b.EnqueueSteer(ctx, key, "s2", "i2", []byte("two")) - if err != nil { - t.Fatal(err) - } - f1, err := b.EnqueueFollowUp(ctx, key, "f1", "i1", []byte("follow")) - if err != nil { - t.Fatal(err) - } - if s1.Position >= s2.Position || s1.ID == SteerItemID(f1.ID) { - t.Fatalf("unexpected independent positions or ids: %#v %#v %#v", s1, s2, f1) - } - steers, follows, err := b.PendingQueues(ctx, key, 0) - if err != nil { - t.Fatal(err) + steerOne, err := first.EnqueueSteer(ctx, key, "steer-1", "invoke-steer-1", []byte("one")) + require.NoError(t, err, "enqueue steer") + steerTwo, err := second.EnqueueSteer(ctx, key, "steer-2", "invoke-steer-2", []byte("two")) + require.NoError(t, err, "enqueue second steer") + follow, err := second.EnqueueFollowUp(ctx, key, "follow-1", "invoke-steer-1", []byte("follow")) + require.NoError(t, err, "enqueue follow-up") + require.NotEqual(t, string(steerOne.ID), string(follow.ID), "queue identities stay separate") + if steerOne.Position >= steerTwo.Position || follow.Position != 1 { + t.Fatalf("independent positions = steer(%d,%d) follow(%d)", steerOne.Position, steerTwo.Position, follow.Position) } + + steers, follows, err := first.PendingQueues(ctx, key, 0) + require.NoError(t, err, "list queues from second instance") if len(steers) != 2 || string(steers[0].Payload) != "one" || string(steers[1].Payload) != "two" { t.Fatalf("steer FIFO = %#v", steers) } if len(follows) != 1 || string(follows[0].Payload) != "follow" { t.Fatalf("follow-up queue = %#v", follows) } + + if _, err := first.ReorderSteer(ctx, key, SteerPendingRef{ItemID: steerTwo.ID}, SteerPendingRef{ItemID: steerOne.ID}); err != nil { + t.Fatalf("accepted-only reorder: %v", err) + } + steers, _, err = second.PendingQueues(ctx, key, 0) + if err != nil || len(steers) != 2 || steers[0].ID != steerTwo.ID || steers[1].ID != steerOne.ID { + t.Fatalf("reordered steer queue = %#v, err=%v", steers, err) + } + + claimed, claim, ok, err := second.ClaimNextSteer(ctx, handle, false) + if err != nil || !ok || claimed.ID != steerTwo.ID { + t.Fatalf("claim steer = %#v, %#v, %v, %v", claimed, claim, ok, err) + } + replayed, replayClaim, ok, err := first.ClaimNextSteer(ctx, handle, false) + if err != nil || !ok || replayed.ID != claimed.ID || replayClaim != claim { + t.Fatalf("cross-instance claim replay = %#v, %#v, %v, %v", replayed, replayClaim, ok, err) + } + third, err := first.EnqueueSteer(ctx, key, "steer-3", "invoke-steer-3", []byte("three")) + require.NoError(t, err) + ordered, err := second.ReorderSteer(ctx, key, SteerPendingRef{ItemID: third.ID}, SteerPendingRef{ItemID: steerOne.ID}) + require.NoError(t, err) + require.Len(t, ordered, 2) + require.Equal(t, third.ID, ordered[0].ID) + require.Equal(t, steerOne.ID, ordered[1].ID) + _, err = first.ReorderSteer(ctx, key, SteerPendingRef{ItemID: claimed.ID}, SteerPendingRef{ItemID: steerOne.ID}) + require.ErrorIs(t, err, ErrQueueNotPending) + stale := claim + stale.OwnerID = "stale-owner" + if err := first.ApplySteer(ctx, key, stale); !errors.Is(err, ErrRunOwnershipLost) { + t.Fatalf("stale steer apply = %v", err) + } + if err := first.ApplySteer(ctx, key, claim); err != nil { + t.Fatalf("apply steer: %v", err) + } + if _, err := second.ReorderSteer(ctx, key, SteerPendingRef{ItemID: steerTwo.ID}, SteerPendingRef{ItemID: steerOne.ID}); !errors.Is(err, ErrQueueNotPending) { + t.Fatalf("claimed/applied reorder = %v", err) + } + promotable, err := first.EnqueueFollowUp(ctx, key, "follow-promote", "invoke-follow-promote", []byte("promote")) + require.NoError(t, err, "enqueue promotable follow-up") + promoted, err := second.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: promotable.ID}) + require.NoError(t, err, "promote follow-up") + if promoted.Steer.ID == "" || string(promoted.Steer.ID) == string(promotable.ID) { + t.Fatalf("promotion reused follow-up identity: follow=%q steer=%q", promotable.ID, promoted.Steer.ID) + } + replayedPromotion, err := first.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: promotable.ID}) + if err != nil || replayedPromotion.Steer.ID != promoted.Steer.ID { + t.Fatalf("promotion replay = %#v, err=%v", replayedPromotion, err) + } + + steers, follows, err = first.PendingQueues(ctx, key, 0) + require.NoError(t, err) + require.Len(t, steers, 3) + require.Equal(t, promoted.Steer.ID, steers[2].ID) + require.Len(t, follows, 1) + require.Equal(t, follow.ID, follows[0].ID, "only the promoted follow-up should disappear") + + followed, followClaim, ok, err := first.ClaimNextFollowUp(ctx, key, handle.RunID) + if err != nil || !ok || followed.ID != follow.ID { + t.Fatalf("claim follow-up = %#v, %#v, %v, %v", followed, followClaim, ok, err) + } + replayedFollowed, replayFollowClaim, ok, err := second.ClaimNextFollowUp(ctx, key, handle.RunID) + if err != nil || !ok || replayedFollowed.ID != follow.ID || replayFollowClaim != followClaim { + t.Fatalf("cross-instance follow-up replay = %#v, %#v, %v, %v", replayedFollowed, replayFollowClaim, ok, err) + } + require.NoError(t, first.ReleaseFollowUp(ctx, key, followClaim)) + // Release removes the old terminal claim, allowing another trigger to claim. + followed, followClaim, ok, err = second.ClaimNextFollowUp(ctx, key, "after-release") + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, follow.ID, followed.ID) + if err := second.ApplyFollowUp(ctx, key, followClaim); err != nil { + t.Fatalf("apply follow-up: %v", err) + } + if _, _, ok, err := first.ClaimNextFollowUp(ctx, key, "different-terminal-run"); err != nil || ok { + t.Fatalf("applied follow-up was claimable again: ok=%v err=%v", ok, err) + } + + // Terminal close from another instance rejects the remaining steers and + // seals the run while its live snapshot is still active. + if err := second.CloseSteerRun(ctx, key, handle.RunID); err != nil { + t.Fatalf("close steer run: %v", err) + } + if steers, _, err := first.PendingQueues(ctx, key, 0); err != nil || len(steers) != 0 { + t.Fatalf("pending steers after close = %#v, err=%v", steers, err) + } + if _, err := first.EnqueueSteer(ctx, key, "steer-late", "invoke-steer-late", []byte("late")); !errors.Is(err, ErrQueueNoActiveRun) { + t.Fatalf("late steer after close = %v, want %v", err, ErrQueueNoActiveRun) + } } func TestMemoryLiveQueueAcceptedOnlyMutationAndReplay(t *testing.T) { @@ -86,30 +176,6 @@ func TestMemoryLiveQueueAcceptedOnlyMutationAndReplay(t *testing.T) { } } -func TestMemoryLiveQueueReorderOnlyAccepted(t *testing.T) { - b, key, handle := liveQueueFixture(t) - ctx := context.Background() - for _, id := range []string{"s1", "s2", "s3"} { - if _, err := b.EnqueueSteer(ctx, key, id, "invoke-"+id, []byte(id)); err != nil { - t.Fatal(err) - } - } - _, claim, ok, err := b.ClaimNextSteer(ctx, handle, false) - if err != nil || !ok { - t.Fatal(err) - } - items, err := b.ReorderSteer(ctx, key, SteerPendingRef{ItemID: "s3"}, SteerPendingRef{ItemID: "s2"}) - if err != nil { - t.Fatal(err) - } - if len(items) != 2 || items[0].ID != "s3" || items[1].ID != "s2" { - t.Fatalf("reordered pending items = %#v", items) - } - if _, err := b.ReorderSteer(ctx, key, SteerPendingRef{ItemID: claim.ItemID}, SteerPendingRef{ItemID: "s2"}); !errors.Is(err, ErrQueueNotPending) { - t.Fatalf("claimed reorder error = %v", err) - } -} - func TestMemoryLiveQueueClaimFencingAndSingleWinner(t *testing.T) { b, key, handle := liveQueueFixture(t) ctx := context.Background() @@ -156,60 +222,6 @@ func TestMemoryLiveQueueClaimFencingAndSingleWinner(t *testing.T) { } } -func TestMemoryFollowUpClaimReplayAndRelease(t *testing.T) { - b, key, _ := liveQueueFixture(t) - ctx := context.Background() - item, err := b.EnqueueFollowUp(ctx, key, "f1", "invoke-f", []byte("follow")) - if err != nil { - t.Fatal(err) - } - claimed, claim, ok, err := b.ClaimNextFollowUp(ctx, key, "run-1") - if err != nil || !ok || claimed.ID != item.ID { - t.Fatalf("claim = %#v, %#v, %v, %v", claimed, claim, ok, err) - } - replayed, replayClaim, ok, err := b.ClaimNextFollowUp(ctx, key, "run-1") - if err != nil || !ok || replayed.ID != item.ID || replayClaim != claim { - t.Fatalf("claim replay = %#v, %#v, %v, %v", replayed, replayClaim, ok, err) - } - if err := b.ReleaseFollowUp(ctx, key, claim); err != nil { - t.Fatal(err) - } - if _, _, ok, err := b.ClaimNextFollowUp(ctx, key, "run-2"); err != nil || !ok { - t.Fatalf("released follow-up was not claimable: %v, %v", ok, err) - } -} - -func TestMemoryFollowUpPromotionKeepsQueueIdentitiesSeparate(t *testing.T) { - b, key, _ := liveQueueFixture(t) - ctx := context.Background() - follow, err := b.EnqueueFollowUp(ctx, key, "f1", "invoke-f", []byte("follow")) - if err != nil { - t.Fatal(err) - } - - promoted, err := b.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: follow.ID}) - if err != nil { - t.Fatal(err) - } - if promoted.Steer.ID == "" || string(promoted.Steer.ID) == string(follow.ID) { - t.Fatalf("promotion reused follow-up identity: follow=%q steer=%q", follow.ID, promoted.Steer.ID) - } - replay, err := b.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: follow.ID}) - if err != nil { - t.Fatal(err) - } - if replay.Steer.ID != promoted.Steer.ID { - t.Fatalf("promotion replay created a second steer: first=%q replay=%q", promoted.Steer.ID, replay.Steer.ID) - } - steers, follows, err := b.PendingQueues(ctx, key, 0) - if err != nil { - t.Fatal(err) - } - if len(steers) != 1 || steers[0].ID != promoted.Steer.ID || len(follows) != 0 { - t.Fatalf("promoted queues = steers:%#v follows:%#v", steers, follows) - } -} - func TestMemoryCloseSteerRunRejectsPendingAndClaimedSteers(t *testing.T) { b, key, handle := liveQueueFixture(t) ctx := context.Background() @@ -274,9 +286,14 @@ func TestMemoryLiveQueueCapacityBound(t *testing.T) { t.Fatalf("replay at capacity = %#v, %v", replay, err) } // Queues are bounded independently. - if _, err := b.EnqueueFollowUp(ctx, key, "f1", "invoke-f1", []byte("follow")); err != nil { - t.Fatalf("follow-up enqueue while steer queue is full: %v", err) - } + follow, err := b.EnqueueFollowUp(ctx, key, "f1", "invoke-f1", []byte("follow")) + require.NoError(t, err, "follow-up enqueue while steer queue is full") + _, err = b.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: follow.ID}) + require.ErrorIs(t, err, ErrQueueCapacityExceeded) + _, follows, err := b.PendingQueues(ctx, key, 0) + require.NoError(t, err) + require.Len(t, follows, 1) + require.Equal(t, QueueAccepted, follows[0].Status, "failed promotion must preserve the follow-up") if err := b.CancelSteer(ctx, key, "s0"); err != nil { t.Fatal(err) } @@ -288,6 +305,13 @@ func TestMemoryLiveQueueCapacityBound(t *testing.T) { func TestMemoryLiveQueueCompactsTerminalItems(t *testing.T) { b, key, _ := liveQueueFixture(t) ctx := context.Background() + item, err := b.EnqueueFollowUp(ctx, key, "f-applied", "invoke-f-applied", []byte("first")) + require.NoError(t, err) + _, claim, ok, err := b.ClaimNextFollowUp(ctx, key, "run-done") + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, item.ID, claim.ItemID) + require.NoError(t, b.ApplyFollowUp(ctx, key, claim)) if _, err := b.EnqueueFollowUp(ctx, key, "keep", "invoke-keep", []byte("keep")); err != nil { t.Fatal(err) } @@ -324,6 +348,16 @@ func TestMemoryLiveQueueCompactsTerminalItems(t *testing.T) { if _, newest := ids[FollowUpItemID(fmt.Sprintf("f%d", total-1))]; !newest { t.Fatal("newest terminal item was compacted away") } + require.NotContains(t, ids, item.ID, "applied item should leave retention") + _, err = b.EnqueueFollowUp(ctx, key, "f-next", "invoke-f-next", []byte("next")) + require.NoError(t, err) + // Compaction must retain the terminal claim even after its item is gone. + _, _, ok, err = b.ClaimNextFollowUp(ctx, key, "run-done") + require.NoError(t, err) + require.False(t, ok, "replayed terminal must not claim a second item") + _, _, ok, err = b.ClaimNextFollowUp(ctx, key, "run-later") + require.NoError(t, err) + require.True(t, ok, "a new terminal can claim pending work") } func TestMemoryClaimNextSteerAdvancesClaimToNewOwner(t *testing.T) { @@ -377,6 +411,9 @@ func TestMemoryLiveQueueClaimsForRealAdmission(t *testing.T) { if err != nil || !admission.Started { t.Fatalf("admit: %+v, %v", admission, err) } + if err := f.manager.EnableSteer(ctx, admission.Handle); err != nil { + t.Fatal(err) + } key := Key{BotID: testBotID, SessionID: testSessionID} item, err := f.manager.EnqueueSteer(ctx, key, "s1", "invoke-s1", []byte(`{"text":"steer"}`)) if err != nil { @@ -394,60 +431,3 @@ func TestMemoryLiveQueueClaimsForRealAdmission(t *testing.T) { } f.finish(t, admission) } - -func TestMemoryPromoteFollowUpRespectsSteerCapacity(t *testing.T) { - b, key, _ := liveQueueFixture(t) - ctx := context.Background() - for i := 0; i < MaxPendingQueueItems; i++ { - id := fmt.Sprintf("s%d", i) - if _, err := b.EnqueueSteer(ctx, key, id, "invoke-"+id, []byte(id)); err != nil { - t.Fatal(err) - } - } - follow, err := b.EnqueueFollowUp(ctx, key, "f1", "invoke-f1", []byte("follow")) - if err != nil { - t.Fatal(err) - } - if _, err := b.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: follow.ID}); !errors.Is(err, ErrQueueCapacityExceeded) { - t.Fatalf("promotion past capacity = %v, want %v", err, ErrQueueCapacityExceeded) - } - if _, follows, err := b.PendingQueues(ctx, key, 0); err != nil || len(follows) != 1 || follows[0].Status != QueueAccepted { - t.Fatalf("follow-up must stay pending after refused promotion: %#v, %v", follows, err) - } -} - -func TestMemoryFollowUpTerminalClaimSurvivesCompaction(t *testing.T) { - b, key, _ := liveQueueFixture(t) - ctx := context.Background() - item, err := b.EnqueueFollowUp(ctx, key, "f-applied", "invoke-f-applied", []byte("first")) - if err != nil { - t.Fatal(err) - } - _, claim, ok, err := b.ClaimNextFollowUp(ctx, key, "run-done") - if err != nil || !ok || claim.ItemID != item.ID { - t.Fatalf("claim = %#v, %v, %v", claim, ok, err) - } - if err := b.ApplyFollowUp(ctx, key, claim); err != nil { - t.Fatal(err) - } - // Push the applied item out of terminal retention. - for i := 0; i < queueTerminalRetention+5; i++ { - id := FollowUpItemID(fmt.Sprintf("f%d", i)) - if _, err := b.EnqueueFollowUp(ctx, key, string(id), "invoke-"+string(id), []byte(id)); err != nil { - t.Fatal(err) - } - if err := b.CancelFollowUp(ctx, key, id); err != nil { - t.Fatal(err) - } - } - if _, err := b.EnqueueFollowUp(ctx, key, "f-next", "invoke-f-next", []byte("next")); err != nil { - t.Fatal(err) - } - // A repeated terminal observation for the same run must not claim again. - if _, _, ok, err := b.ClaimNextFollowUp(ctx, key, "run-done"); err != nil || ok { - t.Fatalf("repeated trigger claimed a second follow-up: ok=%v err=%v", ok, err) - } - if _, _, ok, err := b.ClaimNextFollowUp(ctx, key, "run-later"); err != nil || !ok { - t.Fatalf("a new terminal run could not claim the pending follow-up: ok=%v err=%v", ok, err) - } -} diff --git a/internal/agent/runtime/session/live_queue_transitions.go b/internal/agent/runtime/session/live_queue_transitions.go new file mode 100644 index 0000000000..3acbed7096 --- /dev/null +++ b/internal/agent/runtime/session/live_queue_transitions.go @@ -0,0 +1,235 @@ +package sessionruntime + +import ( + "time" + + "github.com/google/uuid" +) + +// Queue transitions have one implementation. Backends supply serialization, +// ownership validation and their authoritative clock, then store the result. +func (state *steerQueueState) claimNext(handle RunHandle, sealIfEmpty bool, now time.Time) (SteerItem, SteerClaimRef, bool) { + for i := range state.Items { + item := &state.Items[i] + if item.Status == QueueClaimed && item.Claim != nil && item.Claim.RunID == handle.RunID { + if advanceSteerClaim(item, handle) { + state.UpdatedAt = now + } + return cloneSteerItem(*item), *item.Claim, true + } + } + best := -1 + for i := range state.Items { + if state.Items[i].Status == QueueAccepted && state.Items[i].TargetRunID == handle.RunID && + (best < 0 || state.Items[i].Position < state.Items[best].Position) { + best = i + } + } + if best < 0 { + if sealIfEmpty { + state.ClosedRunID = handle.RunID + state.UpdatedAt = now + } + return SteerItem{}, SteerClaimRef{}, false + } + claim := SteerClaimRef{ItemID: state.Items[best].ID, RunID: handle.RunID, OwnerID: handle.OwnerID, Generation: handle.Generation, FencingToken: handle.FencingToken, ClaimToken: uuid.NewString()} + state.Items[best].Status = QueueClaimed + state.Items[best].Claim = &claim + state.UpdatedAt = now + return cloneSteerItem(state.Items[best]), claim, true +} + +func (state *followUpQueueState) claimNext(triggerRunID string, now time.Time) (FollowUpItem, FollowUpClaimRef, bool) { + if state.TerminalClaims == nil { + state.TerminalClaims = make(map[string]string) + } + if itemID := state.TerminalClaims[triggerRunID]; itemID != "" { + for _, item := range state.Items { + if string(item.ID) == itemID && item.Status == QueueClaimed && item.Claim != nil { + return cloneFollowUpItem(item), *item.Claim, true + } + } + return FollowUpItem{}, FollowUpClaimRef{}, false + } + best := -1 + for i := range state.Items { + if state.Items[i].Status == QueueAccepted && (best < 0 || state.Items[i].Position < state.Items[best].Position) { + best = i + } + } + if best < 0 { + return FollowUpItem{}, FollowUpClaimRef{}, false + } + claim := FollowUpClaimRef{ItemID: state.Items[best].ID, TriggerRunID: triggerRunID, ClaimToken: uuid.NewString()} + state.Items[best].Status = QueueClaimed + state.Items[best].Claim = &claim + state.TerminalClaims[triggerRunID] = string(state.Items[best].ID) + state.UpdatedAt = now + return cloneFollowUpItem(state.Items[best]), claim, true +} + +func (state *steerQueueState) apply(ref SteerClaimRef, now time.Time) error { + for i := range state.Items { + claim := state.Items[i].Claim + if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { + state.Items[i].Status = QueueApplied + state.UpdatedAt = now + state.compact() + return nil + } + } + return ErrQueueInvalidReference +} + +func (state *steerQueueState) release(ref SteerClaimRef, now time.Time) error { + for i := range state.Items { + claim := state.Items[i].Claim + if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { + state.Items[i].Status = QueueAccepted + state.Items[i].Claim = nil + state.UpdatedAt = now + return nil + } + } + return ErrQueueInvalidReference +} + +func (state *followUpQueueState) apply(ref FollowUpClaimRef, now time.Time) error { + for i := range state.Items { + claim := state.Items[i].Claim + if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { + state.Items[i].Status = QueueApplied + state.UpdatedAt = now + state.compact() + return nil + } + } + return ErrQueueInvalidReference +} + +func (state *followUpQueueState) release(ref FollowUpClaimRef, now time.Time) error { + for i := range state.Items { + claim := state.Items[i].Claim + if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { + state.Items[i].Status = QueueAccepted + state.Items[i].Claim = nil + delete(state.TerminalClaims, ref.TriggerRunID) + state.UpdatedAt = now + return nil + } + } + return ErrQueueInvalidReference +} + +func (state *steerQueueState) edit(itemID SteerItemID, payload []byte, now time.Time) (SteerItem, error) { + for i := range state.Items { + if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { + state.Items[i].Payload = append([]byte(nil), payload...) + state.UpdatedAt = now + return cloneSteerItem(state.Items[i]), nil + } + } + return SteerItem{}, ErrQueueNotPending +} + +func (state *steerQueueState) cancel(itemID SteerItemID, now time.Time) error { + for i := range state.Items { + if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { + state.Items[i].Status = QueueCanceled + state.UpdatedAt = now + state.compact() + return nil + } + } + return ErrQueueNotPending +} + +func (state *followUpQueueState) edit(itemID FollowUpItemID, payload []byte, now time.Time) (FollowUpItem, error) { + for i := range state.Items { + if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { + state.Items[i].Payload = append([]byte(nil), payload...) + state.UpdatedAt = now + return cloneFollowUpItem(state.Items[i]), nil + } + } + return FollowUpItem{}, ErrQueueNotPending +} + +func (state *followUpQueueState) cancel(itemID FollowUpItemID, now time.Time) error { + for i := range state.Items { + if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { + state.Items[i].Status = QueueCanceled + state.UpdatedAt = now + state.compact() + return nil + } + } + return ErrQueueNotPending +} + +func (steers *steerQueueState) promote(follows *followUpQueueState, key Key, runID string, ref FollowUpPendingRef, now time.Time, newID string) (PromoteFollowUpResult, error) { + if steerID := steers.PromotedFollowUpItems[string(ref.ItemID)]; steerID != "" { + for _, existing := range steers.Items { + if existing.ID == SteerItemID(steerID) { + return PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(existing)}, nil + } + } + return PromoteFollowUpResult{}, ErrQueueInvalidReference + } + for i := range follows.Items { + if follows.Items[i].ID != ref.ItemID || follows.Items[i].Status != QueueAccepted { + continue + } + if countPendingSteers(*steers) >= MaxPendingQueueItems { + return PromoteFollowUpResult{}, ErrQueueCapacityExceeded + } + steer := SteerItem{ + ID: SteerItemID(newID), BotID: key.BotID, SessionID: key.SessionID, + TargetRunID: runID, InvocationID: "promote:" + string(ref.ItemID), + Payload: append([]byte(nil), follows.Items[i].Payload...), Status: QueueAccepted, + Position: nextSteerPosition(*steers), CreatedAt: now, + } + steers.Items = append(steers.Items, steer) + if steers.PromotedFollowUpItems == nil { + steers.PromotedFollowUpItems = make(map[string]string) + } + steers.PromotedFollowUpItems[string(ref.ItemID)] = string(steer.ID) + steers.UpdatedAt = now + follows.Items[i].Status = QueueCanceled + follows.UpdatedAt = now + follows.compact() + return PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(steer)}, nil + } + return PromoteFollowUpResult{}, ErrQueueNotPending +} + +func (state *steerQueueState) enqueue(key Key, itemID, invocationID, runID string, payload []byte, now time.Time) (SteerItem, error) { + if countPendingSteers(*state) >= MaxPendingQueueItems { + return SteerItem{}, ErrQueueCapacityExceeded + } + item := SteerItem{ + ID: SteerItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, + TargetRunID: runID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), + Status: QueueAccepted, Position: nextSteerPosition(*state), CreatedAt: now, + } + state.Items = append(state.Items, item) + state.UpdatedAt = now + if state.ClosedRunID != "" && state.ClosedRunID != runID { + state.ClosedRunID = "" + } + return cloneSteerItem(item), nil +} + +func (state *followUpQueueState) enqueue(key Key, itemID, invocationID, runID string, payload []byte, now time.Time) (FollowUpItem, error) { + if countPendingFollowUps(*state) >= MaxPendingQueueItems { + return FollowUpItem{}, ErrQueueCapacityExceeded + } + item := FollowUpItem{ + ID: FollowUpItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, + EnqueuedDuringRunID: runID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), + Status: QueueAccepted, Position: nextFollowUpPosition(*state), CreatedAt: now, + } + state.Items = append(state.Items, item) + state.UpdatedAt = now + return cloneFollowUpItem(item), nil +} diff --git a/internal/agent/runtime/session/manager.go b/internal/agent/runtime/session/manager.go index 25bfe9a7cf..6c8569a6dc 100644 --- a/internal/agent/runtime/session/manager.go +++ b/internal/agent/runtime/session/manager.go @@ -29,8 +29,8 @@ type Manager struct { // liveness answers "is an owner still alive", which the ledger cannot. Both // backends provide it; only a distributed backend has leases to expire. liveness LivenessBackend - // runs is the durable ledger. A nil ledger means durable admission is not - // wired yet and the manager keeps its pre-ledger behavior. + // runs is required by production Admit. Backend-only tests may omit it + // to exercise live reservations without a durable row. runs ledger.Store // fence is the persistence-ownership cutover applied with each claim. It is // required whenever runs is set: a claim that skipped it would leave a @@ -53,7 +53,6 @@ type Manager struct { mu sync.Mutex controls map[runControlKey]*runControl commandHandler func(context.Context, Command) error - commandReconciler func(context.Context, Command) (bool, error) decisionStore DecisionStore terminalObserver func(context.Context, TerminalRun) decisionFinalizer func(context.Context, RunHandle) error @@ -464,18 +463,6 @@ func (m *Manager) SetCommandHandler(handler func(context.Context, Command) error m.mu.Unlock() } -// SetCommandReconciler installs a read-only domain result checker. Unlike the -// owner-local command handler, it may run on any server after the owner or its -// local control disappears. -func (m *Manager) SetCommandReconciler(reconciler func(context.Context, Command) (bool, error)) { - if m == nil { - return - } - m.mu.Lock() - m.commandReconciler = reconciler - m.mu.Unlock() -} - // SetDecisionStore installs the PostgreSQL-backed decision authority used by // every response transport and by waiting-decision recovery. func (m *Manager) SetDecisionStore(store DecisionStore) { @@ -559,22 +546,27 @@ func (m *Manager) reconcileAndObserveTerminalRun(ctx context.Context, run Termin // reconcileTerminalLive repairs the Redis side of a terminal commit that // outlived its owner. Unlike ordinary owner release, this path is allowed after -// lease expiry, but only while the stored run ref still carries the exact -// durable fencing token observed by the reaper. +// lease expiry. The backend atomically verifies the exact durable token against +// the surviving snapshot (or the lease ref for older snapshots). func (m *Manager) reconcileTerminalLive(ctx context.Context, terminal TerminalRun) { if m == nil || m.distributed == nil || terminal.FencingToken <= 0 { return } key := Key{BotID: terminal.BotID, SessionID: terminal.SessionID} - ref, ok, err := m.distributed.LoadRunRef(ctx, key, terminal.RunID) + current, ok, err := m.backend.Load(ctx, key) if err != nil { - m.logger.Warn("load runtime ref for terminal reconciliation failed", slog.Any("error", err), slog.String("run_id", terminal.RunID)) + m.logger.Warn("load runtime snapshot for terminal reconciliation failed", slog.Any("error", err), slog.String("run_id", terminal.RunID)) return } - if !ok || ref.FencingToken != terminal.FencingToken { + if !ok || current.CurrentRunView == nil || current.CurrentRunView.RunID != terminal.RunID { return } + run := current.CurrentRunView status := liveRunStatus(ledger.State(terminal.State)) + if run.Status == status && run.OwnerLeaseExpiresAt == nil && run.ProposedTerminalStatus == "" { + return + } + ref := RunRef{BotID: key.BotID, SessionID: key.SessionID, RunID: run.RunID, OwnerID: run.OwnerID, Generation: run.Generation, FencingToken: terminal.FencingToken} snapshot, changed, err := m.distributed.ReconcileTerminalRun(ctx, key, ref, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { run := snapshot.CurrentRunView if run == nil || run.RunID != ref.RunID || run.Generation != ref.Generation || run.OwnerID != ref.OwnerID { @@ -593,7 +585,6 @@ func (m *Manager) reconcileTerminalLive(ctx context.Context, terminal TerminalRu run.ErrorCode = "" run.Error = "" } - rejectPendingSteerOnRunFinish(run, now) return snapshot, true, nil }) if err != nil { @@ -605,7 +596,7 @@ func (m *Manager) reconcileTerminalLive(ctx context.Context, terminal TerminalRu if !changed { return } - delta := runtimeRunPatch(snapshot, true, true, true, true) + delta := runtimeRunPatch(snapshot, true, true, true) if err := m.publishRuntimeDelta(ctx, snapshot, terminal.RunID, delta); err != nil { m.logger.Warn("publish reconciled runtime terminal failed; subscribers will reload snapshot", slog.Any("error", err), slog.String("run_id", terminal.RunID)) } @@ -812,7 +803,7 @@ func (m *Manager) Start(ctx context.Context) error { } // startReaper is a no-op without a ledger: there is nothing durable to reap, so -// a manager wired for live state only keeps its pre-ledger behavior. +// backend-only tests can isolate the live reservation algorithms. func (m *Manager) startReaper(ctx context.Context) error { if m.runs == nil || m.liveness == nil { return nil @@ -909,17 +900,6 @@ func (m *Manager) shutdown(ctx context.Context) error { return errors.Join(releaseErr, controlErr, reaperErr, backendErr) } -func (m *Manager) StartRun(ctx context.Context, botID, sessionID, runID string, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) error { - _, err := m.StartRunHandle(ctx, botID, sessionID, runID, abortCh, cancel, injectCh) - return err -} - -func (m *Manager) StartRunHandle(ctx context.Context, botID, sessionID, runID string, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { - return m.StartRunWithAdmissionBuilderHandle(ctx, botID, sessionID, runID, func(context.Context, RunHandle) (RunAdmissionView, error) { - return RunAdmissionView{}, nil - }, abortCh, cancel, injectCh) -} - // OwnerID returns this manager's stable execution-owner identity. func (m *Manager) OwnerID() string { if m == nil { @@ -938,38 +918,8 @@ func (m *Manager) LivenessGeneration(ctx context.Context) (string, error) { return m.livenessGeneration(ctx) } -// StartRunWithAdmissionBuilderHandle reserves the cross-server run before -// executing builder, then publishes the running view only after the canonical -// request turn and optional replacement operation are ready. -func (m *Manager) StartRunWithAdmissionBuilderHandle(ctx context.Context, botID, sessionID, runID string, builder func(context.Context, RunHandle) (RunAdmissionView, error), abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { - handle, _, err := m.startRun(ctx, runStart{ - botID: botID, - sessionID: sessionID, - runID: runID, - builder: builder, - abortCh: abortCh, - cancel: cancel, - injectCh: injectCh, - }) - return handle, err -} - -func (m *Manager) StartRunWithAdmissionBuilderAndOwnershipHandle(ctx context.Context, botID, sessionID, runID string, builder func(context.Context, RunHandle) (RunAdmissionView, error), ownershipCancel context.CancelCauseFunc, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { - handle, _, err := m.startRun(ctx, runStart{ - botID: botID, - sessionID: sessionID, - runID: runID, - builder: builder, - ownershipCancel: ownershipCancel, - abortCh: abortCh, - cancel: cancel, - injectCh: injectCh, - }) - return handle, err -} - // runStart is one live reservation request. It is a struct rather than a -// parameter list because the ledger path and the pre-ledger entry points differ +// parameter list because durable admission and backend-only test fixtures differ // only in whether they carry a fencing token, and that difference should be // visible at the call site instead of being a positional zero. type runStart struct { @@ -1131,6 +1081,7 @@ func (m *Manager) startRun(ctx context.Context, start runStart) (RunHandle, Curs TurnID: start.turnID, InvocationID: start.invocationID, Generation: runGeneration, + FencingToken: start.fencingToken, Status: RunStatusAdmitting, OwnerID: ownerID, OwnerLeaseExpiresAt: leaseExpiresAt, @@ -1258,7 +1209,6 @@ func (m *Manager) startRun(ctx context.Context, start runStart) (RunHandle, Curs snapshot.Seq++ snapshot.UpdatedAt = now run.Status = RunStatusRunning - run.RequestUserTurn = admission.RequestUserTurn run.Operation = admission.Operation switch { case admission.RequestUserTurn != nil: @@ -1585,17 +1535,6 @@ func (m *Manager) resolveTerminalStatus(ctx context.Context, handle RunHandle, s } } -const steerRunFinishedError = "runtime run finished before steer was applied" - -func rejectPendingSteerOnRunFinish(run *CurrentRunView, now time.Time) { - if run == nil || run.Steer == nil || !isPendingSteerStatus(run.Steer.Status) { - return - } - run.Steer.Status = SteerStatusRejected - run.Steer.Error = steerRunFinishedError - run.Steer.UpdatedAt = now -} - func (m *Manager) finishRunState(ctx context.Context, handle RunHandle, status, errorCode, finishMessage string) (bool, error) { admissionTerminal := false _, changed, err := m.releaseActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { @@ -1633,13 +1572,12 @@ func (m *Manager) finishRunState(ctx context.Context, handle RunHandle, status, snapshot.CurrentRunView.OwnerLeaseExpiresAt = nil snapshot.CurrentRunView.ProposedTerminalStatus = "" snapshot.CurrentRunView.FinishProposedAt = nil - rejectPendingSteerOnRunFinish(snapshot.CurrentRunView, now) return snapshot, true, nil }, func(snapshot Snapshot) RuntimeDelta { if admissionTerminal { return RuntimeDelta{CurrentRunView: snapshot.CurrentRunView} } - return runtimeRunPatch(snapshot, true, true, true, m.distributed != nil) + return runtimeRunPatch(snapshot, true, true, m.distributed != nil) }) return changed, err } @@ -1752,10 +1690,9 @@ func (m *Manager) prepareAgentTerminalEvent( return agentTerminalProposal{}, nil } if prepared.State.Terminal() { - // CommitStep may durably finalize this exact run before its delayed native - // terminal event reaches the live projection. prepareLedgerFinish has - // already verified this handle's fencing token, so replay that terminal - // outcome only when it agrees with the event we are publishing. + // A repeated terminal event can meet an already-finalized ledger row. + // prepareLedgerFinish verified this handle's fence; only the same + // durable outcome may be replayed into the live projection. if prepared.State != terminalLedgerState(status, errorCode, "") { return agentTerminalProposal{}, ErrRunOwnershipLost } @@ -1931,7 +1868,6 @@ func (m *Manager) handleAgentEvent(ctx context.Context, handle RunHandle, event proposedAt = now } run.FinishProposedAt = &proposedAt - rejectPendingSteerOnRunFinish(run, now) case native.EventAgentAbort: if !terminalProposal.prepared { return snapshot, true, nil @@ -1943,7 +1879,6 @@ func (m *Manager) handleAgentEvent(ctx context.Context, handle RunHandle, event proposedAt = now } run.FinishProposedAt = &proposedAt - rejectPendingSteerOnRunFinish(run, now) case native.EventError: run.ErrorCode = strings.TrimSpace(event.Code) run.Error = strings.TrimSpace(event.Error) @@ -1955,11 +1890,11 @@ func (m *Manager) handleAgentEvent(ctx context.Context, handle RunHandle, event }, func(snapshot Snapshot) RuntimeDelta { switch event.Type { case native.EventAgentEnd, native.EventAgentAbort: - delta.Run = runtimeRunPatch(snapshot, true, terminalProposal.prepared, terminalProposal.prepared, false).Run + delta.Run = runtimeRunPatch(snapshot, true, terminalProposal.prepared, false).Run case native.EventAgentStart, native.EventToolApprovalRequest, native.EventUserInputRequest: - delta.Run = runtimeRunPatch(snapshot, true, false, false, false).Run + delta.Run = runtimeRunPatch(snapshot, true, false, false).Run case native.EventError, native.EventRetry: - delta.Run = runtimeRunPatch(snapshot, false, true, false, false).Run + delta.Run = runtimeRunPatch(snapshot, false, true, false).Run } return delta }) @@ -2061,7 +1996,10 @@ func (m *Manager) liveSnapshot(ctx context.Context, botID, sessionID string) (Sn return Snapshot{}, err } } - if m.distributed != nil { + // Durable runtimes have one terminal authority: the ledger/reaper protocol. + // A read-side expiry must not publish lost or delete the run ref before + // the reaper reconciles an already-committed finishing proposal. + if m.distributed != nil && m.runs == nil { now, err := m.backend.Now(ctx) if err != nil { return Snapshot{}, fmt.Errorf("load runtime backend time: %w", err) @@ -2079,7 +2017,7 @@ func (m *Manager) liveSnapshot(ctx context.Context, botID, sessionID string) (Sn } return current, true, nil }, func(snapshot Snapshot) RuntimeDelta { - return runtimeRunPatch(snapshot, true, true, false, true) + return runtimeRunPatch(snapshot, true, true, true) }) if err != nil { return Snapshot{}, err @@ -2132,6 +2070,7 @@ func (m *Manager) hydrateSnapshotFromLedger(ctx context.Context, snapshot Snapsh TurnID: run.TurnID, InvocationID: run.InvocationID, Generation: run.LiveGeneration, + FencingToken: run.FencingToken, Status: liveRunStatus(run.State), OwnerID: run.OwnerID, StartedAt: run.CreatedAt, diff --git a/internal/agent/runtime/session/manager_test.go b/internal/agent/runtime/session/manager_test.go index e1c3d75990..b20c1417e0 100644 --- a/internal/agent/runtime/session/manager_test.go +++ b/internal/agent/runtime/session/manager_test.go @@ -948,7 +948,7 @@ func runManagerAbortAcknowledgesReservedRunBeforeTerminalCompletion(t *testing.T manager := testRuntimeManagerWithOptions(t, backend, Options{ OwnerID: "owner-start-abort", StateTTL: time.Hour, - OwnerLeaseTTL: 60 * time.Millisecond, + OwnerLeaseTTL: time.Second, // This scenario keeps ownership while admission is gated. CommandAckTTL: 50 * time.Millisecond, }) runCtx, cancelRun := context.WithCancel(context.Background()) @@ -1259,18 +1259,6 @@ func runCommonRuntimeManagerContract(t *testing.T, suite runtimeBackendContractS t.Parallel() runRuntimeManagerSharesRequestUserTurnContract(t, suite) }) - t.Run("keeps queued steer valid past command acknowledgement timeout", func(t *testing.T) { - t.Parallel() - runRuntimeManagerKeepsQueuedSteerPastAckTimeoutContract(t, suite) - }) - t.Run("rejects queued steer when the run finishes", func(t *testing.T) { - t.Parallel() - runRuntimeManagerRejectsQueuedSteerOnFinishContract(t, suite) - }) - t.Run("rejects queued steer when the agent sends a terminal event", func(t *testing.T) { - t.Parallel() - runRuntimeManagerRejectsQueuedSteerOnAgentTerminalContract(t, suite) - }) t.Run("recovers subscriber buffer overflow", func(t *testing.T) { t.Parallel() runRuntimeManagerSignalsSubscriberOverflowContract(t, suite) @@ -1425,19 +1413,6 @@ func runRuntimeManagerFencesDelayedOwnerMutationsContract(t *testing.T, suite ru if err != nil { t.Fatalf("start first generation: %v", err) } - if _, err := manager.Steer(context.Background(), testBotID, testSessionID, testRunID, "old generation steer"); err != nil { - t.Fatalf("steer first generation: %v", err) - } - var delayedApplied func() - select { - case injected := <-oldInject: - delayedApplied = injected.Applied - case <-time.After(time.Second): - t.Fatal("first generation steer was not delivered") - } - if delayedApplied == nil { - t.Fatal("first generation steer has no applied callback") - } if err := manager.FinishRun(context.Background(), oldHandle, RunStatusCompleted, ""); err != nil { t.Fatalf("finish first generation: %v", err) } @@ -1453,9 +1428,6 @@ func runRuntimeManagerFencesDelayedOwnerMutationsContract(t *testing.T, suite ru if ok, err := manager.AbortRun(context.Background(), oldHandle); ok || !errors.Is(err, ErrRunOwnershipLost) { t.Fatalf("stale abort = ok:%v err:%v, want ErrRunOwnershipLost", ok, err) } - if _, err := manager.SteerRun(context.Background(), oldHandle, "late steer command"); !errors.Is(err, ErrRunOwnershipLost) { - t.Fatalf("stale steer error = %v, want ErrRunOwnershipLost", err) - } select { case <-newAbort: t.Fatal("stale abort signaled the new generation") @@ -1470,7 +1442,6 @@ func runRuntimeManagerFencesDelayedOwnerMutationsContract(t *testing.T, suite ru if err := manager.FinishRun(context.Background(), oldHandle, RunStatusErrored, "late finish"); !errors.Is(err, ErrRunOwnershipLost) { t.Fatalf("late finish error = %v, want ErrRunOwnershipLost", err) } - delayedApplied() if manager.localControlForHandle(newHandle) == nil { t.Fatal("late mutation removed the current run control") } @@ -1486,13 +1457,15 @@ func runRuntimeManagerFencesDelayedOwnerMutationsContract(t *testing.T, suite ru func runDistributedRuntimeManagerContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { t.Helper() + t.Run("repairs terminal after lease expiry using exact receipt", func(t *testing.T) { runExpiredLeaseTerminalReceiptContract(t, suite) }) + t.Run("routes durable decisions without UI projection", func(t *testing.T) { runDistributedDecisionRouteContract(t, suite) }) t.Run("does not prepare rejected run operations", func(t *testing.T) { t.Parallel() runRuntimeManagerDoesNotBuildRejectedOperationContract(t, suite) }) - t.Run("routes abort and steer across managers", func(t *testing.T) { + t.Run("routes abort across managers", func(t *testing.T) { t.Parallel() - runRuntimeManagerRoutesAbortAndSteerAcrossManagersContract(t, suite) + runRuntimeManagerRoutesAbortAcrossManagersContract(t, suite) }) t.Run("routes abort past a stale local generation", func(t *testing.T) { t.Parallel() @@ -1534,10 +1507,6 @@ func runDistributedRuntimeManagerContract(t *testing.T, suite distributedRuntime t.Parallel() runRuntimeManagerAcknowledgesAppliedResponseAfterFinish(t, suite) }) - t.Run("reconciles a response after owner control is lost", func(t *testing.T) { - t.Parallel() - runRuntimeManagerReconcilesResponseAfterOwnerControlLoss(t, suite) - }) t.Run("keeps command routing alive after startup context cancellation", func(t *testing.T) { t.Parallel() runRuntimeManagerCommandRoutingOutlivesStartContext(t, suite) @@ -1734,12 +1703,12 @@ func runRuntimeManagerHandsExhaustedDurableFinishToReaper(t *testing.T, suite di transient := errors.New("database remains unavailable") if phase == "prepare" { - runs.setPrepareErr(transient) + runs.SetPrepareErr(transient) } else { if _, err := manager.HandleAgentEvent(context.Background(), admission.Handle, native.StreamEvent{Type: native.EventAgentEnd}); err != nil { t.Fatalf("prepare terminal event: %v", err) } - runs.setFinalizeErr(transient) + runs.SetFinalizeErr(transient) } if err := manager.FinishRun(context.Background(), admission.Handle, RunStatusCompleted, ""); err == nil { t.Fatal("FinishRun() error = nil, want initial durable failure") @@ -1752,8 +1721,8 @@ func runRuntimeManagerHandsExhaustedDurableFinishToReaper(t *testing.T, suite di if manager.localControlForHandle(admission.Handle) != nil { t.Fatal("durable retry budget expired without releasing local control") } - runs.setPrepareErr(nil) - runs.setFinalizeErr(nil) + runs.SetPrepareErr(nil) + runs.SetFinalizeErr(nil) want := ledger.StateLost if phase == "finalize" { @@ -1761,52 +1730,16 @@ func runRuntimeManagerHandsExhaustedDurableFinishToReaper(t *testing.T, suite di } reaperDeadline := time.Now().Add(2 * time.Second) for time.Now().Before(reaperDeadline) { - if got := runs.state(admission.RunID); got == want { + if got := runs.State(admission.RunID); got == want { return } time.Sleep(10 * time.Millisecond) } - t.Fatalf("reaper state = %q, want %q after %s retry timeout", runs.state(admission.RunID), want, phase) + t.Fatalf("reaper state = %q, want %q after %s retry timeout", runs.State(admission.RunID), want, phase) }) } } -func runRuntimeManagerReconcilesResponseAfterOwnerControlLoss(t *testing.T, suite distributedRuntimeBackendContractSuite) { - t.Helper() - backends := suite.newSharedBackends(t, 2) - owner := testRuntimeManager(t, backends[0], "response-reconcile-owner") - restarted := testRuntimeManager(t, backends[1], "response-reconcile-owner") - if err := owner.StartRun(context.Background(), testBotID, testSessionID, "stream-response-reconcile", make(chan struct{}, 1), func() {}, make(chan turn.InjectMessage, 1)); err != nil { - t.Fatalf("start response run: %v", err) - } - handle := requireRunHandle(t, owner, testBotID, testSessionID, "stream-response-reconcile") - if _, err := owner.HandleAgentEvent(context.Background(), handle, native.StreamEvent{ - Type: native.EventToolApprovalRequest, ToolName: "exec", ToolCallID: "call-response-reconcile", - ApprovalID: "approval-response-reconcile", Status: "pending", - }); err != nil { - t.Fatalf("record response target: %v", err) - } - owner.forgetLocalControlForHandle(context.Background(), handle) - var reconciled atomic.Int64 - restarted.SetCommandReconciler(func(_ context.Context, command Command) (bool, error) { - reconciled.Add(1) - if command.TargetID != "approval-response-reconcile" || command.Generation != handle.Generation { - t.Fatalf("reconcile command = %#v", command) - } - return true, nil - }) - handled, err := restarted.DispatchActiveCommand( - context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, - "approval-response-reconcile", []byte(`{"decision":"approve"}`), - ) - if err != nil || !handled { - t.Fatalf("reconciled response = handled:%v err:%v", handled, err) - } - if reconciled.Load() != 1 { - t.Fatalf("reconciler calls = %d, want 1", reconciled.Load()) - } -} - func runManagerAbortBeforeClaim(t *testing.T, suite distributedRuntimeBackendContractSuite) { t.Helper() backend := &preClaimGateBackend{ @@ -2292,7 +2225,7 @@ func runRuntimeManagerRejectsDelayedOldGenerationCommand(t *testing.T, suite dis func runRuntimeManagerRejectsExpiredLeaseRevivalContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { t.Helper() - const leaseTTL = 100 * time.Millisecond + const leaseTTL = 500 * time.Millisecond backend := suite.newBackend(t) manager := testRuntimeManagerWithOptions(t, backend, Options{ OwnerID: "owner-expired-revival", @@ -2352,57 +2285,6 @@ func runRuntimeManagerRejectsExpiredLeaseRevivalContract(t *testing.T, suite dis } } -func runRuntimeManagerKeepsQueuedSteerPastAckTimeoutContract(t *testing.T, suite runtimeBackendContractSuite) { - t.Helper() - - const commandAckTTL = 500 * time.Millisecond - manager := testRuntimeManagerWithOptions(t, suite.newBackend(t), Options{ - OwnerID: "owner-queued-steer", - StateTTL: time.Hour, - OwnerLeaseTTL: time.Second, - CommandAckTTL: commandAckTTL, - }) - injectCh := make(chan turn.InjectMessage, 1) - if err := manager.StartRun(context.Background(), testBotID, testSessionID, testRunID, make(chan struct{}, 1), func() {}, injectCh); err != nil { - t.Fatalf("start run: %v", err) - } - steer, err := manager.Steer(context.Background(), testBotID, testSessionID, testRunID, "wait for the next model step") - if err != nil { - t.Fatalf("steer: %v", err) - } - - queued := waitRuntimeSnapshot(t, manager, testBotID, testSessionID, func(snapshot Snapshot) bool { - return snapshot.CurrentRunView != nil && - snapshot.CurrentRunView.Steer != nil && - snapshot.CurrentRunView.Steer.ID == steer.ID && - snapshot.CurrentRunView.Steer.Status == SteerStatusQueued - }) - if queued.CurrentRunView == nil || queued.CurrentRunView.Steer == nil { - t.Fatal("queued steer is missing") - } - time.Sleep(2 * commandAckTTL) - snapshot, err := manager.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot queued steer after acknowledgement timeout: %v", err) - } - if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Steer == nil || snapshot.CurrentRunView.Steer.ID != steer.ID || snapshot.CurrentRunView.Steer.Status != SteerStatusQueued { - t.Fatalf("steer after acknowledgement timeout = %#v, want queued", snapshot.CurrentRunView) - } - - select { - case injected := <-injectCh: - if injected.Applied == nil { - t.Fatal("queued steer is missing its applied acknowledgement") - } - injected.Applied() - case <-time.After(time.Second): - t.Fatal("queued steer was not delivered to the agent") - } - waitRuntimeSnapshot(t, manager, testBotID, testSessionID, func(snapshot Snapshot) bool { - return snapshot.CurrentRunView != nil && snapshot.CurrentRunView.Steer != nil && snapshot.CurrentRunView.Steer.Status == SteerStatusApplied - }) -} - func runRuntimeManagerCancelsExecutionAfterOwnershipLossContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { t.Helper() @@ -2712,122 +2594,6 @@ func TestRuntimeManagerBuilderFailureReleasesReservation(t *testing.T) { } } -func runRuntimeManagerRejectsQueuedSteerOnFinishContract(t *testing.T, suite runtimeBackendContractSuite) { - t.Helper() - - manager := testRuntimeManager(t, suite.newBackend(t), "owner-finished-steer") - injectCh := make(chan turn.InjectMessage, 1) - if err := manager.StartRun(context.Background(), testBotID, testSessionID, testRunID, make(chan struct{}, 1), func() {}, injectCh); err != nil { - t.Fatalf("start run: %v", err) - } - steer, err := manager.Steer(context.Background(), testBotID, testSessionID, testRunID, "adjust before finish") - if err != nil { - t.Fatalf("steer: %v", err) - } - waitRuntimeSnapshot(t, manager, testBotID, testSessionID, func(snapshot Snapshot) bool { - return snapshot.CurrentRunView != nil && snapshot.CurrentRunView.Steer != nil && - snapshot.CurrentRunView.Steer.ID == steer.ID && snapshot.CurrentRunView.Steer.Status == SteerStatusQueued - }) - if err := manager.FinishRun(context.Background(), requireRunHandle(t, manager, testBotID, testSessionID, testRunID), RunStatusCompleted, ""); err != nil { - t.Fatalf("finish run: %v", err) - } - snapshot, err := manager.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot: %v", err) - } - if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Steer == nil || snapshot.CurrentRunView.Steer.Status != SteerStatusRejected { - t.Fatalf("finished steer = %#v, want rejected", snapshot.CurrentRunView) - } - if snapshot.CurrentRunView.Steer.Error != steerRunFinishedError { - t.Fatalf("finished steer error = %q", snapshot.CurrentRunView.Steer.Error) - } - select { - case injected := <-injectCh: - if injected.Applied != nil { - injected.Applied() - } - default: - t.Fatal("queued steer was not delivered") - } - snapshot, err = manager.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot after late apply: %v", err) - } - if snapshot.CurrentRunView.Steer.Status != SteerStatusRejected { - t.Fatalf("late apply changed terminal steer = %#v", snapshot.CurrentRunView.Steer) - } -} - -func runRuntimeManagerRejectsQueuedSteerOnAgentTerminalContract(t *testing.T, suite runtimeBackendContractSuite) { - t.Helper() - - for _, tc := range []struct { - name string - event native.StreamEvent - wantStatus string - }{ - {name: "end", event: native.StreamEvent{Type: native.EventAgentEnd}, wantStatus: RunStatusCompleted}, - {name: "abort", event: native.StreamEvent{Type: native.EventAgentAbort}, wantStatus: RunStatusAborted}, - } { - t.Run(tc.name, func(t *testing.T) { - manager := testRuntimeManager(t, suite.newBackend(t), "owner-agent-terminal-steer-"+tc.name) - sub, err := manager.Subscribe(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("subscribe: %v", err) - } - defer sub.Close() - injectCh := make(chan turn.InjectMessage, 1) - handle, err := manager.StartRunHandle(context.Background(), testBotID, testSessionID, testRunID, make(chan struct{}, 1), func() {}, injectCh) - if err != nil { - t.Fatalf("start run: %v", err) - } - steer, err := manager.SteerRun(context.Background(), handle, "adjust before agent "+tc.name) - if err != nil { - t.Fatalf("steer: %v", err) - } - select { - case <-injectCh: - case <-time.After(time.Second): - t.Fatal("queued steer was not delivered") - } - waitRuntimeSnapshot(t, manager, testBotID, testSessionID, func(snapshot Snapshot) bool { - return snapshot.CurrentRunView != nil && snapshot.CurrentRunView.Steer != nil && - snapshot.CurrentRunView.Steer.ID == steer.ID && snapshot.CurrentRunView.Steer.Status == SteerStatusQueued - }) - if _, err := manager.HandleAgentEvent(context.Background(), handle, tc.event); err != nil { - t.Fatalf("handle agent %s: %v", tc.name, err) - } - - snapshot, err := manager.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot: %v", err) - } - if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Steer == nil || snapshot.CurrentRunView.Steer.Status != SteerStatusRejected { - t.Fatalf("agent-terminal steer = %#v, want rejected", snapshot.CurrentRunView) - } - if snapshot.CurrentRunView.Steer.Error != steerRunFinishedError { - t.Fatalf("agent-terminal steer error = %q", snapshot.CurrentRunView.Steer.Error) - } - event := waitRuntimeEvent(t, sub.C, func(event Event) bool { - return event.Delta != nil && event.Delta.Run != nil && event.Delta.Run.Status != nil && *event.Delta.Run.Status == RunStatusFinishing - }) - if event.Delta.Run.Steer == nil || event.Delta.Run.Steer.Status != SteerStatusRejected { - t.Fatalf("agent-terminal delta = %#v, want rejected steer", event.Delta) - } - if snapshot.CurrentRunView.ProposedTerminalStatus != tc.wantStatus || snapshot.CurrentRunView.FinishProposedAt == nil { - t.Fatalf("agent-terminal proposal = %#v, want %s", snapshot.CurrentRunView, tc.wantStatus) - } - if err := manager.FinishRun(context.Background(), handle, "", ""); err != nil { - t.Fatalf("finalize agent %s: %v", tc.name, err) - } - snapshot, err = manager.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil || snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Status != tc.wantStatus { - t.Fatalf("final agent status = %#v, err %v, want %s", snapshot.CurrentRunView, err, tc.wantStatus) - } - }) - } -} - func runRuntimeManagerSharesReplacementOperationContract(t *testing.T, suite runtimeBackendContractSuite) { t.Helper() @@ -2906,20 +2672,20 @@ func runRuntimeManagerSharesRequestUserTurnContract(t *testing.T, suite runtimeB t.Fatalf("observer snapshot: %v", err) } got := snapshot.CurrentRunView - if got == nil || got.RequestUserTurn == nil { + if got == nil || got.requestUserTurn() == nil { t.Fatalf("current run request user turn = %#v", got) } - if got.RequestUserTurn.Text != requestTurn.Text || got.RequestUserTurn.ExternalMessageID != testRunID { - t.Fatalf("request user turn = %#v", got.RequestUserTurn) + if got.requestUserTurn().Text != requestTurn.Text || got.requestUserTurn().ExternalMessageID != testRunID { + t.Fatalf("request user turn = %#v", got.requestUserTurn()) } - if len(got.RequestUserTurn.Attachments) != 1 || got.RequestUserTurn.Attachments[0].ContentHash != "sha256:notes" { - t.Fatalf("request user turn attachments = %#v", got.RequestUserTurn.Attachments) + if len(got.requestUserTurn().Attachments) != 1 || got.requestUserTurn().Attachments[0].ContentHash != "sha256:notes" { + t.Fatalf("request user turn attachments = %#v", got.requestUserTurn().Attachments) } requestTurn.Text = "mutated by caller" requestTurn.Attachments[0].Name = "mutated.txt" - if got.RequestUserTurn.Text != "inspect the workspace" || got.RequestUserTurn.Attachments[0].Name != "notes.txt" { - t.Fatalf("runtime request user turn aliases caller state: %#v", got.RequestUserTurn) + if got.requestUserTurn().Text != "inspect the workspace" || got.requestUserTurn().Attachments[0].Name != "notes.txt" { + t.Fatalf("runtime request user turn aliases caller state: %#v", got.requestUserTurn()) } } @@ -3132,7 +2898,7 @@ func TestRuntimeManagerDropsEpochlessEventsAfterEpochIsEstablished(t *testing.T) } } -func runRuntimeManagerRoutesAbortAndSteerAcrossManagersContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { +func runRuntimeManagerRoutesAbortAcrossManagersContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { t.Helper() backends := suite.newSharedBackends(t, 2) @@ -3177,47 +2943,6 @@ func runRuntimeManagerRoutesAbortAndSteerAcrossManagersContract(t *testing.T, su if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Status != RunStatusAborted { t.Fatalf("abort status changed after errored finish: %#v", snapshot.CurrentRunView) } - - steerInjectCh := make(chan turn.InjectMessage, 1) - if err := owner.StartRun(context.Background(), testBotID, testSessionID, "stream-steer", make(chan struct{}, 1), func() {}, steerInjectCh); err != nil { - t.Fatalf("start steer run: %v", err) - } - steer, err := remote.Steer(context.Background(), testBotID, testSessionID, "stream-steer", "adjust course") - if err != nil { - t.Fatalf("steer: %v", err) - } - if steer.Status != SteerStatusPending { - t.Fatalf("initial steer = %#v", steer) - } - select { - case injected := <-steerInjectCh: - if injected.Text != "adjust course" { - t.Fatalf("injected text = %q", injected.Text) - } - pending, err := remote.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot pending steer: %v", err) - } - if pending.CurrentRunView == nil || pending.CurrentRunView.Steer == nil || pending.CurrentRunView.Steer.Status != SteerStatusQueued { - t.Fatalf("steer was not acknowledged as queued before agent consumption: %#v", pending.CurrentRunView) - } - if _, err := remote.Steer(context.Background(), testBotID, testSessionID, "stream-steer", "overlapping adjustment"); err == nil { - t.Fatal("concurrent steer should be rejected while the first command is pending") - } - if injected.Applied == nil { - t.Fatal("steer injection is missing its agent-consumption acknowledgement") - } - injected.Applied() - case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for steer injection") - } - - snapshot = waitRuntimeSnapshot(t, remote, testBotID, testSessionID, func(s Snapshot) bool { - return s.CurrentRunView != nil && s.CurrentRunView.Steer != nil && s.CurrentRunView.Steer.Status == SteerStatusApplied - }) - if snapshot.CurrentRunView.Steer.ID == "" { - t.Fatalf("steer state = %#v", snapshot.CurrentRunView.Steer) - } } func runRuntimeManagerRoutesActiveResponsesAcrossManagersContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { @@ -3250,7 +2975,7 @@ func runRuntimeManagerRoutesActiveResponsesAcrossManagersContract(t *testing.T, {commandType: CommandToolApprovalResponse, targetID: "approval-1"}, {commandType: CommandUserInputResponse, targetID: "input-1"}, } { - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, testSessionID, request.commandType, request.targetID, []byte(`{"ok":true}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, testSessionID, request.commandType, request.targetID, []byte(`{"ok":true}`)) if err != nil || !handled { t.Fatalf("dispatch %s = handled:%v err:%v", request.commandType, handled, err) } @@ -3263,7 +2988,7 @@ func runRuntimeManagerRoutesActiveResponsesAcrossManagersContract(t *testing.T, t.Fatalf("timed out waiting for %s", request.commandType) } } - if handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-old", nil); err != nil || handled { + if handled, err := remote.dispatchTestCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-old", nil); err != nil || handled { t.Fatalf("unrelated target = handled:%v err:%v", handled, err) } } @@ -3287,7 +3012,7 @@ func runRuntimeManagerPreservesRemoteCommandDeadlineError(t *testing.T, suite di t.Fatalf("record deadline approval: %v", err) } - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, "session-deadline", CommandToolApprovalResponse, "approval-deadline", []byte(`{"action":"approve"}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, "session-deadline", CommandToolApprovalResponse, "approval-deadline", []byte(`{"action":"approve"}`)) if !handled || !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("remote deadline result = handled:%v err:%v, want context.DeadlineExceeded", handled, err) } @@ -3316,7 +3041,7 @@ func runRuntimeManagerPreservesOwnershipDeadlineError(t *testing.T, suite distri } ownerBackend.failing.Store(true) - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, "session-ownership-deadline", CommandToolApprovalResponse, "approval-ownership-deadline", []byte(`{"action":"approve"}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, "session-ownership-deadline", CommandToolApprovalResponse, "approval-ownership-deadline", []byte(`{"action":"approve"}`)) if !handled || !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("ownership deadline result = handled:%v err:%v, want context.DeadlineExceeded", handled, err) } @@ -3345,7 +3070,7 @@ func runRuntimeManagerAcknowledgesAppliedResponseAfterFinish(t *testing.T, suite t.Fatalf("record approval request: %v", err) } - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-applied", []byte(`{"action":"approve"}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-applied", []byte(`{"action":"approve"}`)) if err != nil || !handled { t.Fatalf("applied response = handled:%v err:%v, want acknowledged success", handled, err) } @@ -3415,7 +3140,7 @@ func runRuntimeManagerCancelsActiveResponseOnFinish(t *testing.T, suite distribu } dispatchDone := make(chan dispatchResult, 1) go func() { - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-finish", []byte(`{"action":"approve"}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, testSessionID, CommandToolApprovalResponse, "approval-finish", []byte(`{"action":"approve"}`)) dispatchDone <- dispatchResult{handled: handled, err: err} }() receiveTestResult(t, "active response handler start", handlerStarted) @@ -3468,7 +3193,7 @@ func runRuntimeManagerExpiresActiveResponseHandlers(t *testing.T, suite distribu {commandType: CommandToolApprovalResponse, targetID: "approval-expiry"}, {commandType: CommandUserInputResponse, targetID: "input-expiry"}, } { - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, "session-response-expiry", request.commandType, request.targetID, []byte(`{"ok":true}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, "session-response-expiry", request.commandType, request.targetID, []byte(`{"ok":true}`)) if !handled || err == nil { t.Fatalf("expiring %s dispatch = handled:%v err:%v, want deadline error", request.commandType, handled, err) } @@ -3513,7 +3238,7 @@ func runRuntimeManagerCancelsActiveResponseOnClose(t *testing.T, suite distribut } dispatchDone := make(chan dispatchResult, 1) go func() { - handled, err := manager.DispatchActiveCommand(context.Background(), testBotID, "session-response-close", CommandToolApprovalResponse, "approval-close", []byte(`{"action":"approve"}`)) + handled, err := manager.dispatchTestCommand(context.Background(), testBotID, "session-response-close", CommandToolApprovalResponse, "approval-close", []byte(`{"action":"approve"}`)) dispatchDone <- dispatchResult{handled: handled, err: err} }() receiveTestResult(t, "active response handler start", handlerStarted) @@ -3901,45 +3626,6 @@ func runRuntimeManagerDroppedCommandAckContract(t *testing.T, suite distributedR if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Status != RunStatusRunning { t.Fatalf("dropped abort status = %#v, want still running", snapshot.CurrentRunView) } - - steer, err := remote.Steer(context.Background(), testBotID, testSessionID, testRunID, "adjust course") - if err != nil { - t.Fatalf("dropped steer initial publish: %v", err) - } - if steer.Status != SteerStatusPending { - t.Fatalf("initial dropped steer = %#v", steer) - } - snapshot = waitRuntimeSnapshot(t, remote, testBotID, testSessionID, func(s Snapshot) bool { - return s.CurrentRunView != nil && - s.CurrentRunView.Steer != nil && - s.CurrentRunView.Steer.ID == steer.ID && - s.CurrentRunView.Steer.Status == SteerStatusRejected - }) - if snapshot.CurrentRunView.Steer.Error != "runtime steer command was not acknowledged" { - t.Fatalf("dropped steer state = %#v", snapshot.CurrentRunView.Steer) - } - - owner.applyCommand(context.Background(), Command{ - Type: CommandSteer, - BotID: testBotID, - SessionID: testSessionID, - RunID: testRunID, - SteerID: steer.ID, - Text: "late adjust course", - CreatedAt: time.Now().UTC().Add(-time.Second), - }) - select { - case injected := <-injectCh: - t.Fatalf("late rejected steer was injected: %#v", injected) - default: - } - snapshot, err = remote.Snapshot(context.Background(), testBotID, testSessionID) - if err != nil { - t.Fatalf("snapshot after late steer command: %v", err) - } - if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Steer == nil || snapshot.CurrentRunView.Steer.Status != SteerStatusRejected { - t.Fatalf("late steer command state = %#v, want rejected", snapshot.CurrentRunView) - } } func runRuntimeManagerRetriesDroppedCommandContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { @@ -4008,7 +3694,7 @@ func runRuntimeManagerReleasesPendingCommandOnClose(t *testing.T, suite distribu } dispatchDone := make(chan dispatchResult, 1) go func() { - handled, dispatchErr := remote.DispatchActiveCommand( + handled, dispatchErr := remote.dispatchTestCommand( dispatchCtx, testBotID, testSessionID, CommandToolApprovalResponse, "approval-pending-close", json.RawMessage(`{"status":"approved"}`), ) @@ -4095,7 +3781,7 @@ func runRuntimeManagerPreservesPreparedOutcomeOnClose(t *testing.T, suite distri if _, err := owner.HandleAgentEvent(context.Background(), admission.Handle, native.StreamEvent{Type: native.EventAgentEnd}); err != nil { t.Fatalf("prepare terminal event: %v", err) } - if got := runs.state(admission.RunID); got != ledger.StateFinishing { + if got := runs.State(admission.RunID); got != ledger.StateFinishing { t.Fatalf("ledger before close = %q, want finishing", got) } const activeSessionID = "session-running-close" @@ -4123,10 +3809,10 @@ func runRuntimeManagerPreservesPreparedOutcomeOnClose(t *testing.T, suite distri t.Fatalf("close owner: %v", err) } closed = true - if got := runs.state(admission.RunID); got != ledger.StateCompleted { + if got := runs.State(admission.RunID); got != ledger.StateCompleted { t.Fatalf("ledger after close = %q, want completed", got) } - if got := runs.state(activeAdmission.RunID); got != ledger.StateLost { + if got := runs.State(activeAdmission.RunID); got != ledger.StateLost { t.Fatalf("active ledger after close = %q, want lost", got) } receiveTestResult(t, "active run cancellation", activeCanceled) @@ -4194,12 +3880,12 @@ func runRuntimeManagerDoesNotBlockCommandResultsBehindSlowHandlers(t *testing.T, blockedDispatchDone := make(chan error, 1) go func() { - _, err := ownerB.DispatchActiveCommand(context.Background(), testBotID, "session-command-hol-a", CommandToolApprovalResponse, "approval-command-hol-a", []byte(`{"decision":"approve"}`)) + _, err := ownerB.dispatchTestCommand(context.Background(), testBotID, "session-command-hol-a", CommandToolApprovalResponse, "approval-command-hol-a", []byte(`{"decision":"approve"}`)) blockedDispatchDone <- err }() receiveTestResult(t, "blocked owner command handler", blockedHandlerEntered) - handled, err := ownerA.DispatchActiveCommand(context.Background(), testBotID, "session-command-hol-b", CommandToolApprovalResponse, "approval-command-hol-b", []byte(`{"decision":"approve"}`)) + handled, err := ownerA.dispatchTestCommand(context.Background(), testBotID, "session-command-hol-b", CommandToolApprovalResponse, "approval-command-hol-b", []byte(`{"decision":"approve"}`)) if err != nil || !handled { t.Fatalf("unrelated command behind blocked handler = handled:%v err:%v", handled, err) } diff --git a/internal/agent/runtime/session/memory.go b/internal/agent/runtime/session/memory.go index 61805cf440..bd64c2362f 100644 --- a/internal/agent/runtime/session/memory.go +++ b/internal/agent/runtime/session/memory.go @@ -422,8 +422,12 @@ func cloneSnapshot(snapshot Snapshot) (Snapshot, error) { currentRun.Messages = nil snapshot.CurrentRunView = ¤tRun } + data, err := marshalSnapshot(snapshot) + if err != nil { + return Snapshot{}, err + } var out Snapshot - if err := cloneJSON(snapshot, &out); err != nil { + if err := unmarshalSnapshot(data, &out); err != nil { return Snapshot{}, err } if out.CurrentRunView != nil { diff --git a/internal/agent/runtime/session/memory_live_queue.go b/internal/agent/runtime/session/memory_live_queue.go index 6161c47594..b392e9548e 100644 --- a/internal/agent/runtime/session/memory_live_queue.go +++ b/internal/agent/runtime/session/memory_live_queue.go @@ -50,21 +50,14 @@ func (b *MemoryBackend) EnqueueSteer(ctx context.Context, key Key, itemID, invoc if !active || state.ClosedRunID == run.RunID { return SteerItem{}, ErrQueueNoActiveRun } - if countPendingSteers(state) >= MaxPendingQueueItems { - return SteerItem{}, ErrQueueCapacityExceeded + if !SteerRunAvailable(run) { + return SteerItem{}, ErrQueueSteerUnsupported } - item := SteerItem{ - ID: SteerItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, - TargetRunID: run.RunID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), - Status: QueueAccepted, Position: nextSteerPosition(state), CreatedAt: now, - } - state.Items = append(state.Items, item) - state.UpdatedAt = now - if state.ClosedRunID != "" && state.ClosedRunID != run.RunID { - state.ClosedRunID = "" + item, err := state.enqueue(key, itemID, invocationID, run.RunID, payload, now) + if err == nil { + b.steerQueues[key.String()] = state } - b.steerQueues[key.String()] = state - return cloneSteerItem(item), nil + return item, err } func (b *MemoryBackend) EnqueueFollowUp(ctx context.Context, key Key, itemID, invocationID string, payload []byte) (FollowUpItem, error) { @@ -90,18 +83,11 @@ func (b *MemoryBackend) EnqueueFollowUp(ctx context.Context, key Key, itemID, in if !active { return FollowUpItem{}, ErrQueueNoActiveRun } - if countPendingFollowUps(state) >= MaxPendingQueueItems { - return FollowUpItem{}, ErrQueueCapacityExceeded - } - item := FollowUpItem{ - ID: FollowUpItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, - EnqueuedDuringRunID: run.RunID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), - Status: QueueAccepted, Position: nextFollowUpPosition(state), CreatedAt: now, + item, err := state.enqueue(key, itemID, invocationID, run.RunID, payload, now) + if err == nil { + b.followUpQueues[key.String()] = state } - state.Items = append(state.Items, item) - state.UpdatedAt = now - b.followUpQueues[key.String()] = state - return cloneFollowUpItem(item), nil + return item, err } func (b *MemoryBackend) PendingQueues(ctx context.Context, key Key, limit int) ([]SteerItem, []FollowUpItem, error) { @@ -181,15 +167,11 @@ func (b *MemoryBackend) UpdateSteer(ctx context.Context, key Key, itemID SteerIt return SteerItem{}, ErrQueueInvalidReference } state := b.steerQueues[key.String()] - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Payload = append([]byte(nil), payload...) - state.UpdatedAt = time.Now().UTC() - b.steerQueues[key.String()] = state - return cloneSteerItem(state.Items[i]), nil - } + item, err := state.edit(itemID, payload, time.Now().UTC()) + if err == nil { + b.steerQueues[key.String()] = state } - return SteerItem{}, ErrQueueNotPending + return item, err } func (b *MemoryBackend) UpdateFollowUp(ctx context.Context, key Key, itemID FollowUpItemID, payload []byte) (FollowUpItem, error) { @@ -205,15 +187,11 @@ func (b *MemoryBackend) UpdateFollowUp(ctx context.Context, key Key, itemID Foll return FollowUpItem{}, ErrQueueInvalidReference } state := b.followUpQueues[key.String()] - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Payload = append([]byte(nil), payload...) - state.UpdatedAt = time.Now().UTC() - b.followUpQueues[key.String()] = state - return cloneFollowUpItem(state.Items[i]), nil - } + item, err := state.edit(itemID, payload, time.Now().UTC()) + if err == nil { + b.followUpQueues[key.String()] = state } - return FollowUpItem{}, ErrQueueNotPending + return item, err } func (b *MemoryBackend) CancelSteer(ctx context.Context, key Key, itemID SteerItemID) error { @@ -229,16 +207,11 @@ func (b *MemoryBackend) CancelSteer(ctx context.Context, key Key, itemID SteerIt return ErrQueueInvalidReference } state := b.steerQueues[key.String()] - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Status = QueueCanceled - state.UpdatedAt = time.Now().UTC() - state.compact() - b.steerQueues[key.String()] = state - return nil - } + err := state.cancel(itemID, time.Now().UTC()) + if err == nil { + b.steerQueues[key.String()] = state } - return ErrQueueNotPending + return err } func (b *MemoryBackend) CloseSteerRun(ctx context.Context, key Key, runID string) error { @@ -280,16 +253,11 @@ func (b *MemoryBackend) CancelFollowUp(ctx context.Context, key Key, itemID Foll return ErrQueueInvalidReference } state := b.followUpQueues[key.String()] - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Status = QueueCanceled - state.UpdatedAt = time.Now().UTC() - state.compact() - b.followUpQueues[key.String()] = state - return nil - } + err := state.cancel(itemID, time.Now().UTC()) + if err == nil { + b.followUpQueues[key.String()] = state } - return ErrQueueNotPending + return err } func (b *MemoryBackend) PromoteFollowUpToSteer(ctx context.Context, key Key, ref FollowUpPendingRef) (PromoteFollowUpResult, error) { @@ -311,42 +279,16 @@ func (b *MemoryBackend) PromoteFollowUpToSteer(ctx context.Context, key Key, ref if !active || steers.ClosedRunID == run.RunID { return PromoteFollowUpResult{}, ErrQueueNoActiveRun } - follows := b.followUpQueues[key.String()] - if steerID := steers.PromotedFollowUpItems[string(ref.ItemID)]; steerID != "" { - for _, existing := range steers.Items { - if existing.ID == SteerItemID(steerID) { - return PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(existing)}, nil - } - } - return PromoteFollowUpResult{}, ErrQueueInvalidReference + if !SteerRunAvailable(run) { + return PromoteFollowUpResult{}, ErrQueueSteerUnsupported } - for i := range follows.Items { - if follows.Items[i].ID != ref.ItemID || follows.Items[i].Status != QueueAccepted { - continue - } - if countPendingSteers(steers) >= MaxPendingQueueItems { - return PromoteFollowUpResult{}, ErrQueueCapacityExceeded - } - steer := SteerItem{ - ID: SteerItemID(uuid.NewString()), BotID: key.BotID, SessionID: key.SessionID, - TargetRunID: run.RunID, InvocationID: "promote:" + string(ref.ItemID), - Payload: append([]byte(nil), follows.Items[i].Payload...), Status: QueueAccepted, - Position: nextSteerPosition(steers), CreatedAt: now, - } - steers.Items = append(steers.Items, steer) - if steers.PromotedFollowUpItems == nil { - steers.PromotedFollowUpItems = make(map[string]string) - } - steers.PromotedFollowUpItems[string(ref.ItemID)] = string(steer.ID) - steers.UpdatedAt = now - follows.Items[i].Status = QueueCanceled - follows.UpdatedAt = now - follows.compact() + follows := b.followUpQueues[key.String()] + result, err := steers.promote(&follows, key, run.RunID, ref, now, uuid.NewString()) + if err == nil { b.steerQueues[key.String()] = steers b.followUpQueues[key.String()] = follows - return PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(steer)}, nil } - return PromoteFollowUpResult{}, ErrQueueNotPending + return result, err } func (b *MemoryBackend) ClaimNextSteer(ctx context.Context, handle RunHandle, sealIfEmpty bool) (SteerItem, SteerClaimRef, bool, error) { @@ -367,38 +309,13 @@ func (b *MemoryBackend) ClaimNextSteer(ctx context.Context, handle RunHandle, se if !ok || !runMatchesHandle(snapshot.CurrentRunView, handle) || !runViewOwnedBy(snapshot.CurrentRunView, handle.OwnerID) || !isActiveRunStatus(snapshot.CurrentRunView.Status) { return SteerItem{}, SteerClaimRef{}, false, ErrRunOwnershipLost } - state := b.steerQueues[handle.key().String()] - for i := range state.Items { - item := &state.Items[i] - if item.Status == QueueClaimed && item.Claim != nil && item.Claim.RunID == handle.RunID { - if advanceSteerClaim(item, handle) { - state.UpdatedAt = now - b.steerQueues[handle.key().String()] = state - } - return cloneSteerItem(*item), *item.Claim, true, nil - } - } - best := -1 - for i := range state.Items { - if state.Items[i].Status == QueueAccepted && state.Items[i].TargetRunID == handle.RunID && - (best < 0 || state.Items[i].Position < state.Items[best].Position) { - best = i - } - } - if best < 0 { - if sealIfEmpty { - state.ClosedRunID = handle.RunID - state.UpdatedAt = now - b.steerQueues[handle.key().String()] = state - } - return SteerItem{}, SteerClaimRef{}, false, nil + if !SteerRunAvailable(snapshot.CurrentRunView) { + return SteerItem{}, SteerClaimRef{}, false, ErrQueueSteerUnsupported } - claim := SteerClaimRef{ItemID: state.Items[best].ID, RunID: handle.RunID, OwnerID: handle.OwnerID, Generation: handle.Generation, FencingToken: handle.FencingToken, ClaimToken: uuid.NewString()} - state.Items[best].Status = QueueClaimed - state.Items[best].Claim = &claim - state.UpdatedAt = now + state := b.steerQueues[handle.key().String()] + item, claim, claimed := state.claimNext(handle, sealIfEmpty, now) b.steerQueues[handle.key().String()] = state - return cloneSteerItem(state.Items[best]), claim, true, nil + return item, claim, claimed, nil } func (b *MemoryBackend) ApplySteer(ctx context.Context, key Key, ref SteerClaimRef) error { @@ -418,17 +335,11 @@ func (b *MemoryBackend) ApplySteer(ctx context.Context, key Key, ref SteerClaimR return ErrRunOwnershipLost } state := b.steerQueues[key.String()] - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueApplied - state.UpdatedAt = time.Now().UTC() - state.compact() - b.steerQueues[key.String()] = state - return nil - } + err := state.apply(ref, time.Now().UTC()) + if err == nil { + b.steerQueues[key.String()] = state } - return ErrQueueInvalidReference + return err } func (b *MemoryBackend) ReleaseSteer(ctx context.Context, key Key, ref SteerClaimRef) error { @@ -448,17 +359,11 @@ func (b *MemoryBackend) ReleaseSteer(ctx context.Context, key Key, ref SteerClai return ErrRunOwnershipLost } state := b.steerQueues[key.String()] - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueAccepted - state.Items[i].Claim = nil - state.UpdatedAt = time.Now().UTC() - b.steerQueues[key.String()] = state - return nil - } + err := state.release(ref, time.Now().UTC()) + if err == nil { + b.steerQueues[key.String()] = state } - return ErrQueueInvalidReference + return err } func (b *MemoryBackend) ClaimNextFollowUp(ctx context.Context, key Key, triggerRunID string) (FollowUpItem, FollowUpClaimRef, bool, error) { @@ -478,33 +383,9 @@ func (b *MemoryBackend) ClaimNextFollowUp(ctx context.Context, key Key, triggerR return FollowUpItem{}, FollowUpClaimRef{}, false, err } state := b.followUpQueues[key.String()] - if state.TerminalClaims == nil { - state.TerminalClaims = make(map[string]string) - } - if itemID := state.TerminalClaims[triggerRunID]; itemID != "" { - for _, item := range state.Items { - if string(item.ID) == itemID && item.Status == QueueClaimed && item.Claim != nil { - return cloneFollowUpItem(item), *item.Claim, true, nil - } - } - return FollowUpItem{}, FollowUpClaimRef{}, false, nil - } - best := -1 - for i := range state.Items { - if state.Items[i].Status == QueueAccepted && (best < 0 || state.Items[i].Position < state.Items[best].Position) { - best = i - } - } - if best < 0 { - return FollowUpItem{}, FollowUpClaimRef{}, false, nil - } - claim := FollowUpClaimRef{ItemID: state.Items[best].ID, TriggerRunID: triggerRunID, ClaimToken: uuid.NewString()} - state.Items[best].Status = QueueClaimed - state.Items[best].Claim = &claim - state.TerminalClaims[triggerRunID] = string(state.Items[best].ID) - state.UpdatedAt = time.Now().UTC() + item, claim, claimed := state.claimNext(triggerRunID, time.Now().UTC()) b.followUpQueues[key.String()] = state - return cloneFollowUpItem(state.Items[best]), claim, true, nil + return item, claim, claimed, nil } func (b *MemoryBackend) ApplyFollowUp(ctx context.Context, key Key, ref FollowUpClaimRef) error { @@ -520,17 +401,11 @@ func (b *MemoryBackend) ApplyFollowUp(ctx context.Context, key Key, ref FollowUp return err } state := b.followUpQueues[key.String()] - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueApplied - state.UpdatedAt = time.Now().UTC() - state.compact() - b.followUpQueues[key.String()] = state - return nil - } + err := state.apply(ref, time.Now().UTC()) + if err == nil { + b.followUpQueues[key.String()] = state } - return ErrQueueInvalidReference + return err } func (b *MemoryBackend) ReleaseFollowUp(ctx context.Context, key Key, ref FollowUpClaimRef) error { @@ -546,18 +421,11 @@ func (b *MemoryBackend) ReleaseFollowUp(ctx context.Context, key Key, ref Follow return err } state := b.followUpQueues[key.String()] - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueAccepted - state.Items[i].Claim = nil - delete(state.TerminalClaims, ref.TriggerRunID) - state.UpdatedAt = time.Now().UTC() - b.followUpQueues[key.String()] = state - return nil - } + err := state.release(ref, time.Now().UTC()) + if err == nil { + b.followUpQueues[key.String()] = state } - return ErrQueueInvalidReference + return err } var _ LiveQueueBackend = (*MemoryBackend)(nil) diff --git a/internal/agent/runtime/session/queue/queue.go b/internal/agent/runtime/session/queue/queue.go deleted file mode 100644 index eb0c5abcf9..0000000000 --- a/internal/agent/runtime/session/queue/queue.go +++ /dev/null @@ -1,41 +0,0 @@ -// Package queue keeps the public queue vocabulary separate from the session -// runtime implementation. Storage is owned by the configured live backend. -package queue - -import sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - -type ( - Status = sessionruntime.QueueStatus - SteerItemID = sessionruntime.SteerItemID - FollowUpItemID = sessionruntime.FollowUpItemID - SteerPendingRef = sessionruntime.SteerPendingRef - FollowUpPendingRef = sessionruntime.FollowUpPendingRef - SteerClaimRef = sessionruntime.SteerClaimRef - FollowUpClaimRef = sessionruntime.FollowUpClaimRef - SteerItem = sessionruntime.SteerItem - FollowUpItem = sessionruntime.FollowUpItem - PromoteFollowUpResult = sessionruntime.PromoteFollowUpResult -) - -const ( - Accepted = sessionruntime.QueueAccepted - Claimed = sessionruntime.QueueClaimed - Applied = sessionruntime.QueueApplied - Rejected = sessionruntime.QueueRejected - Expired = sessionruntime.QueueExpired - Canceled = sessionruntime.QueueCanceled - - DefaultPendingListLimit = 256 - MaxPendingItems = sessionruntime.MaxPendingQueueItems - - ErrorTargetRunNotActive = sessionruntime.QueueErrorTargetRunNotActive -) - -var ( - ErrNoActiveRun = sessionruntime.ErrQueueNoActiveRun - ErrInvalidReference = sessionruntime.ErrQueueInvalidReference - ErrNotPending = sessionruntime.ErrQueueNotPending - ErrInvocationConflict = sessionruntime.ErrQueueInvocationConflict - ErrCapacityExceeded = sessionruntime.ErrQueueCapacityExceeded - ErrAdmissionOverloaded = sessionruntime.ErrQueueAdmissionOverloaded -) diff --git a/internal/agent/runtime/session/queue_capability_test.go b/internal/agent/runtime/session/queue_capability_test.go new file mode 100644 index 0000000000..8192cd4ab8 --- /dev/null +++ b/internal/agent/runtime/session/queue_capability_test.go @@ -0,0 +1,67 @@ +package sessionruntime + +import ( + "context" + "errors" + "testing" + + "github.com/felinics/memoh/internal/agent/runtime/native" +) + +func TestSteerRequiresConsumerAndRejectsFinishingRun(t *testing.T) { + for _, tc := range []struct { + name, status string + supported bool + }{ + {"old or unsupported owner", RunStatusRunning, false}, + {"terminal proposal", RunStatusFinishing, true}, + {"abort in progress", RunStatusAborting, true}, + } { + t.Run(tc.name, func(t *testing.T) { + backend, key, _ := liveQueueFixture(t) + ctx := context.Background() + follow, err := backend.EnqueueFollowUp(ctx, key, "follow", "follow-invocation", []byte(`{"text":"later"}`)) + if err != nil { + t.Fatal(err) + } + if _, _, err := backend.Update(ctx, key, func(snapshot Snapshot, _ bool) (Snapshot, bool, error) { + snapshot.CurrentRunView.Status = tc.status + snapshot.CurrentRunView.SteerSupported = tc.supported + return snapshot, true, nil + }); err != nil { + t.Fatal(err) + } + if _, err := backend.EnqueueSteer(ctx, key, "steer", "steer-invocation", []byte(`{"text":"now"}`)); !errors.Is(err, ErrQueueSteerUnsupported) { + t.Fatalf("steer admission=%v", err) + } + if _, err := backend.PromoteFollowUpToSteer(ctx, key, FollowUpPendingRef{ItemID: follow.ID}); !errors.Is(err, ErrQueueSteerUnsupported) { + t.Fatalf("promotion=%v", err) + } + steers, follows, err := backend.PendingQueues(ctx, key, 0) + if err != nil || len(steers) != 0 || len(follows) != 1 { + t.Fatalf("rejected promotion changed queues: %v %v %v", steers, follows, err) + } + }) + } +} + +func TestContinuationStepIndexUsesCurrentOwnerCursor(t *testing.T) { + f := newAdmitFixture(t) + ctx := context.Background() + admission, err := f.manager.Admit(ctx, f.input("step-cursor", `{"text":"hello"}`)) + if err != nil { + t.Fatal(err) + } + if _, err := f.manager.HandleAgentEvent(ctx, admission.Handle, native.StreamEvent{Type: native.EventStepEnd, StepNumber: 4}); err != nil { + t.Fatal(err) + } + if index, err := f.manager.ContinuationStepIndex(admission.Handle); err != nil || index != 5 { + t.Fatalf("continuation cursor=%d err=%v", index, err) + } + stale := admission.Handle + stale.Generation = "old-generation" + if _, err := f.manager.ContinuationStepIndex(stale); !errors.Is(err, ErrRunOwnershipLost) { + t.Fatalf("stale cursor lookup=%v", err) + } + f.finish(t, admission) +} diff --git a/internal/agent/runtime/session/queue_reorder_regression_test.go b/internal/agent/runtime/session/queue_reorder_regression_test.go new file mode 100644 index 0000000000..fc860eef74 --- /dev/null +++ b/internal/agent/runtime/session/queue_reorder_regression_test.go @@ -0,0 +1,31 @@ +package sessionruntime + +import ( + "context" + "fmt" + "testing" +) + +func TestFollowUpRepeatedReorderPreservesCurrentOrder(t *testing.T) { + b, key, _ := liveQueueFixture(t) + ctx := context.Background() + for _, id := range []string{"a", "b", "c", "d"} { + if _, err := b.EnqueueFollowUp(ctx, key, id, id, []byte(id)); err != nil { + t.Fatal(err) + } + } + if _, err := b.ReorderFollowUp(ctx, key, FollowUpPendingRef{ItemID: "d"}, FollowUpPendingRef{ItemID: "a"}); err != nil { + t.Fatal(err) + } + got, err := b.ReorderFollowUp(ctx, key, FollowUpPendingRef{ItemID: "c"}, FollowUpPendingRef{ItemID: "b"}) + if err != nil { + t.Fatal(err) + } + ids := []string{} + for _, item := range got { + ids = append(ids, string(item.ID)) + } + if fmt.Sprint(ids) != "[d a c b]" { + t.Fatalf("second reorder = %v, want [d a c b]; first move was lost", ids) + } +} diff --git a/internal/agent/runtime/session/reaper_test.go b/internal/agent/runtime/session/reaper_test.go index 4797bee4d4..b68f5bd56b 100644 --- a/internal/agent/runtime/session/reaper_test.go +++ b/internal/agent/runtime/session/reaper_test.go @@ -110,7 +110,7 @@ func newTestReaperWithLiveness(t *testing.T, runs *fakeLedger, live LivenessBack func TestReaperMarksExpiredLeaseLost(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-expired", "session-expired", 5, "generation-1") + runs.InsertClaimed("run-expired", "session-expired", 5, "generation-1") live := newFakeLiveness("generation-1") live.setCandidates(LeaseCandidate{ Key: Key{BotID: testBotID, SessionID: "session-expired"}, @@ -122,10 +122,10 @@ func TestReaperMarksExpiredLeaseLost(t *testing.T) { reaper.tick(context.Background()) - if got := runs.state("run-expired"); got != "lost" { + if got := runs.State("run-expired"); got != "lost" { t.Fatalf("state = %q, want lost", got) } - if got := runs.errorCode("run-expired"); got != runErrorOwnerLeaseExpired { + if got := runs.ErrorCode("run-expired"); got != runErrorOwnerLeaseExpired { t.Fatalf("error code = %q, want %q", got, runErrorOwnerLeaseExpired) } if len(live.releasedCandidates()) != 1 || len(live.indexed()) != 0 { @@ -147,7 +147,7 @@ func TestReaperFinalizesDurableFinishProposalInsteadOfMarkingOwnerLost(t *testin } { t.Run(tt.name, func(t *testing.T) { runs := newFakeLedger() - runs.insertClaimed("run-finishing-"+tt.name, "session-finishing-"+tt.name, 5, "generation-1") + runs.InsertClaimed("run-finishing-"+tt.name, "session-finishing-"+tt.name, 5, "generation-1") prepared, applied, err := runs.PrepareFinish(context.Background(), ledger.PrepareFinishParams{ RunID: "run-finishing-" + tt.name, FencingToken: 5, @@ -170,10 +170,10 @@ func TestReaperFinalizesDurableFinishProposalInsteadOfMarkingOwnerLost(t *testin reaper.tick(context.Background()) - if got := runs.state("run-finishing-" + tt.name); got != tt.proposed { + if got := runs.State("run-finishing-" + tt.name); got != tt.proposed { t.Fatalf("state = %q, want proposed %q rather than lost", got, tt.proposed) } - if got := runs.errorCode("run-finishing-" + tt.name); got != tt.errorCode { + if got := runs.ErrorCode("run-finishing-" + tt.name); got != tt.errorCode { t.Fatalf("error code = %q, want %q", got, tt.errorCode) } if len(observed) != 1 || observed[0].State != string(tt.proposed) { @@ -201,11 +201,11 @@ func TestReaperObservesAppliedAndAlreadyTerminalOutcomes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { runs := newFakeLedger() - runs.insertClaimed("run-observed", "session-observed", 5, "generation-1") + runs.InsertClaimed("run-observed", "session-observed", 5, "generation-1") if tt.abortRequested { - runs.mu.Lock() - runs.runs["run-observed"].AbortRequestedAt = time.Now() - runs.mu.Unlock() + runs.Mu.Lock() + runs.Runs["run-observed"].AbortRequestedAt = time.Now() + runs.Mu.Unlock() } if tt.seedState != "" { if _, applied, err := runs.Finalize(context.Background(), ledger.FinalizeParams{ @@ -241,7 +241,7 @@ func TestReaperObservesAppliedAndAlreadyTerminalOutcomes(t *testing.T) { func TestReaperDoesNotObserveNewerActiveOwner(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-newer-owner", "session-newer-owner", 6, "generation-1") + runs.InsertClaimed("run-newer-owner", "session-newer-owner", 6, "generation-1") live := newFakeLiveness("generation-1") live.setCandidates(LeaseCandidate{ Key: Key{BotID: testBotID, SessionID: "session-newer-owner"}, RunID: "run-newer-owner", FencingToken: 5, @@ -257,7 +257,7 @@ func TestReaperDoesNotObserveNewerActiveOwner(t *testing.T) { if len(observed) != 0 { t.Fatalf("newer active owner emitted terminal observations: %+v", observed) } - if got := runs.state("run-newer-owner"); got != ledger.StateRunning { + if got := runs.State("run-newer-owner"); got != ledger.StateRunning { t.Fatalf("ledger state = %q, want running", got) } } @@ -265,11 +265,11 @@ func TestReaperDoesNotObserveNewerActiveOwner(t *testing.T) { func TestReaperRetriesWaitingDecisionRecoveryAfterTokenHandoff(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-waiting-handoff", "session-waiting-handoff", 5, "generation-1") - runs.mu.Lock() - runs.runs["run-waiting-handoff"].State = ledger.StateWaitingDecision - runs.runs["run-waiting-handoff"].FencingToken = 6 - runs.mu.Unlock() + runs.InsertClaimed("run-waiting-handoff", "session-waiting-handoff", 5, "generation-1") + runs.Mu.Lock() + runs.Runs["run-waiting-handoff"].State = ledger.StateWaitingDecision + runs.Runs["run-waiting-handoff"].FencingToken = 6 + runs.Mu.Unlock() live := newFakeLiveness("generation-1") live.setCandidates(LeaseCandidate{ Key: Key{BotID: testBotID, SessionID: "session-waiting-handoff"}, @@ -287,7 +287,7 @@ func TestReaperRetriesWaitingDecisionRecoveryAfterTokenHandoff(t *testing.T) { if len(recovered) != 1 || recovered[0].FencingToken != 5 { t.Fatalf("recovery calls = %+v, want the stale candidate retried once", recovered) } - if got := runs.state("run-waiting-handoff"); got != ledger.StateWaitingDecision { + if got := runs.State("run-waiting-handoff"); got != ledger.StateWaitingDecision { t.Fatalf("ledger state = %q, want waiting_decision", got) } if len(live.indexed()) != 0 || len(live.releasedCandidates()) != 1 { @@ -330,8 +330,8 @@ func TestReaperRunsTerminalReconcilerOnlyAsLeader(t *testing.T) { func TestReaperKeepsCandidateWhenTerminalWriteFails(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-retry", "session-retry", 5, "generation-1") - runs.setFinalizeErr(errors.New("database is unreachable")) + runs.InsertClaimed("run-retry", "session-retry", 5, "generation-1") + runs.SetFinalizeErr(errors.New("database is unreachable")) live := newFakeLiveness("generation-1") live.setCandidates(LeaseCandidate{ Key: Key{BotID: testBotID, SessionID: "session-retry"}, @@ -344,13 +344,13 @@ func TestReaperKeepsCandidateWhenTerminalWriteFails(t *testing.T) { if len(live.indexed()) != 1 { t.Fatal("failed transition must leave the candidate indexed for the next tick") } - if got := runs.state("run-retry"); got != "running" { + if got := runs.State("run-retry"); got != "running" { t.Fatalf("state = %q, want running", got) } - runs.setFinalizeErr(nil) + runs.SetFinalizeErr(nil) reaper.tick(context.Background()) - if got := runs.state("run-retry"); got != "lost" { + if got := runs.State("run-retry"); got != "lost" { t.Fatalf("state after retry = %q, want lost", got) } if len(live.indexed()) != 0 { @@ -363,7 +363,7 @@ func TestReaperKeepsCandidateWhenTerminalWriteFails(t *testing.T) { func TestReaperStaleTokenCannotCondemnReclaimedRun(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-reclaimed", "session-reclaimed", 9, "generation-1") + runs.InsertClaimed("run-reclaimed", "session-reclaimed", 9, "generation-1") live := newFakeLiveness("generation-1") live.setCandidates(LeaseCandidate{ Key: Key{BotID: testBotID, SessionID: "session-reclaimed"}, @@ -374,7 +374,7 @@ func TestReaperStaleTokenCannotCondemnReclaimedRun(t *testing.T) { reaper.tick(context.Background()) - if got := runs.state("run-reclaimed"); got != "running" { + if got := runs.State("run-reclaimed"); got != "running" { t.Fatalf("state = %q, want running; a stale token must not condemn a reclaimed run", got) } } @@ -384,24 +384,24 @@ func TestReaperStaleTokenCannotCondemnReclaimedRun(t *testing.T) { func TestReaperRecoversRunsFromLostBackendGeneration(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-stale-a", "session-stale-a", 1, "generation-old") - runs.insertClaimed("run-stale-b", "session-stale-b", 2, "generation-old") - runs.insertClaimed("run-stale-c", "session-stale-c", 3, "generation-old") - runs.insertClaimed("run-current", "session-current", 4, "generation-new") + runs.InsertClaimed("run-stale-a", "session-stale-a", 1, "generation-old") + runs.InsertClaimed("run-stale-b", "session-stale-b", 2, "generation-old") + runs.InsertClaimed("run-stale-c", "session-stale-c", 3, "generation-old") + runs.InsertClaimed("run-current", "session-current", 4, "generation-new") live := newFakeLiveness("generation-new") reaper := newTestReaper(t, runs, live) reaper.tick(context.Background()) for _, runID := range []string{"run-stale-a", "run-stale-b", "run-stale-c"} { - if got := runs.state(runID); got != "lost" { + if got := runs.State(runID); got != "lost" { t.Fatalf("%s state = %q, want lost", runID, got) } - if got := runs.errorCode(runID); got != runErrorBackendLost { + if got := runs.ErrorCode(runID); got != runErrorBackendLost { t.Fatalf("%s error code = %q, want %q", runID, got, runErrorBackendLost) } } - if got := runs.state("run-current"); got != "running" { + if got := runs.State("run-current"); got != "running" { t.Fatalf("current generation run = %q, want running", got) } } @@ -411,14 +411,14 @@ func TestReaperRecoversRunsFromLostBackendGeneration(t *testing.T) { func TestReaperDefersRecoveryUntilBackendLossGrace(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-blip", "session-blip", 1, "generation-old") + runs.InsertClaimed("run-blip", "session-blip", 1, "generation-old") live := newFakeLiveness("generation-new") reaper := newTestReaper(t, runs, live) reaper.generationObservedAt = time.Now() reaper.tick(context.Background()) - if got := runs.state("run-blip"); got != "running" { + if got := runs.State("run-blip"); got != "running" { t.Fatalf("state = %q, want running during the grace period", got) } } @@ -428,16 +428,16 @@ func TestReaperDefersRecoveryUntilBackendLossGrace(t *testing.T) { func TestReaperRepairsOrphanedAdmissions(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertOrphan("run-orphan", "session-orphan", "inv-orphan", "fingerprint") + runs.InsertOrphan("run-orphan", "session-orphan", "inv-orphan", "fingerprint") live := newFakeLiveness("generation-1") reaper := newTestReaper(t, runs, live) reaper.tick(context.Background()) - if got := runs.state("run-orphan"); got != "lost" { + if got := runs.State("run-orphan"); got != "lost" { t.Fatalf("state = %q, want lost", got) } - if got := runs.errorCode("run-orphan"); got != runErrorAdmissionOrphaned { + if got := runs.ErrorCode("run-orphan"); got != runErrorAdmissionOrphaned { t.Fatalf("error code = %q, want %q", got, runErrorAdmissionOrphaned) } } @@ -447,8 +447,8 @@ func TestReaperRepairsOrphanedAdmissions(t *testing.T) { func TestReaperFollowerDoesNothing(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-follower", "session-follower", 1, "generation-old") - runs.insertOrphan("run-follower-orphan", "session-follower-orphan", "inv", "fingerprint") + runs.InsertClaimed("run-follower", "session-follower", 1, "generation-old") + runs.InsertOrphan("run-follower-orphan", "session-follower-orphan", "inv", "fingerprint") live := newFakeLiveness("generation-new") live.leader = false live.setCandidates(LeaseCandidate{RunID: "run-follower", FencingToken: 1}) @@ -456,10 +456,10 @@ func TestReaperFollowerDoesNothing(t *testing.T) { reaper.tick(context.Background()) - if got := runs.state("run-follower"); got != "running" { + if got := runs.State("run-follower"); got != "running" { t.Fatalf("state = %q, want running", got) } - if got := runs.state("run-follower-orphan"); got != "accepted" { + if got := runs.State("run-follower-orphan"); got != "accepted" { t.Fatalf("orphan state = %q, want accepted", got) } } @@ -469,7 +469,7 @@ func TestReaperFollowerDoesNothing(t *testing.T) { func TestReaperTransitionsAreIdempotentAcrossLeaders(t *testing.T) { t.Parallel() runs := newFakeLedger() - runs.insertClaimed("run-failover", "session-failover", 3, "generation-old") + runs.InsertClaimed("run-failover", "session-failover", 3, "generation-old") live := newFakeLiveness("generation-new") first := newTestReaper(t, runs, live) second := newTestReaper(t, runs, live) @@ -477,10 +477,10 @@ func TestReaperTransitionsAreIdempotentAcrossLeaders(t *testing.T) { first.tick(context.Background()) second.tick(context.Background()) - if got := runs.state("run-failover"); got != "lost" { + if got := runs.State("run-failover"); got != "lost" { t.Fatalf("state = %q, want lost", got) } - writes := runs.terminalWrites() + writes := runs.TerminalWrites() if len(writes) != 1 { t.Fatalf("terminal writes = %d, want 1; the repeat must not apply", len(writes)) } diff --git a/internal/agent/runtime/session/recovery.go b/internal/agent/runtime/session/recovery.go index acefbb294c..ab1c251863 100644 --- a/internal/agent/runtime/session/recovery.go +++ b/internal/agent/runtime/session/recovery.go @@ -209,6 +209,7 @@ func (m *Manager) reserveRecoveredWaitingDecision(ctx context.Context, run ledge view.RunID = handle.RunID view.TurnID = run.TurnID view.Generation = handle.Generation + view.FencingToken = handle.FencingToken view.Status = RunStatusWaitingDecision view.OwnerID = m.ownerID view.OwnerLeaseExpiresAt = &expiresAt diff --git a/internal/agent/runtime/session/recovery_test.go b/internal/agent/runtime/session/recovery_test.go index 7b4a274462..a589732767 100644 --- a/internal/agent/runtime/session/recovery_test.go +++ b/internal/agent/runtime/session/recovery_test.go @@ -157,11 +157,11 @@ func (f *waitingDecisionRecoveryFence) ReclaimWaitingDecision( previousToken, newToken int64, decisions []runtimefence.PreservedDecision, ) error { - f.runs.mu.Lock() + f.runs.Mu.Lock() f.decisions.mu.Lock() defer f.decisions.mu.Unlock() - defer f.runs.mu.Unlock() - run := f.runs.runs[runID] + defer f.runs.Mu.Unlock() + run := f.runs.Runs[runID] if run == nil || run.BotID != botID || run.SessionID != sessionID || run.State != ledger.StateWaitingDecision || run.FencingToken != previousToken { return ErrRunOwnershipLost @@ -209,13 +209,13 @@ func TestWaitingDecisionRecoveryRetriesAfterFenceCommitWhenLiveReservationFails( ) key := Key{BotID: testBotID, SessionID: sessionID} runs := newFakeLedger() - runs.insertClaimed(runID, sessionID, 5, "generation-old") + runs.InsertClaimed(runID, sessionID, 5, "generation-old") if _, applied, err := runs.SetWaitingDecision(context.Background(), runID, 5); err != nil || !applied { t.Fatalf("park run: applied=%v err=%v", applied, err) } - runs.mu.Lock() - runs.token = 5 - runs.mu.Unlock() + runs.Mu.Lock() + runs.Token = 5 + runs.Mu.Unlock() decisions := &fakeDecisionStore{target: DecisionTarget{ Type: CommandUserInputResponse, ID: decisionID, BotID: testBotID, SessionID: sessionID, RunID: runID, TurnID: runID + "-turn", @@ -298,13 +298,13 @@ func TestWaitingDecisionRecoveryPreservesParallelDecisions(t *testing.T) { ) key := Key{BotID: testBotID, SessionID: sessionID} runs := newFakeLedger() - runs.insertClaimed(runID, sessionID, 5, "generation-old") + runs.InsertClaimed(runID, sessionID, 5, "generation-old") if _, applied, err := runs.SetWaitingDecision(context.Background(), runID, 5); err != nil || !applied { t.Fatalf("park run: applied=%v err=%v", applied, err) } - runs.mu.Lock() - runs.token = 5 - runs.mu.Unlock() + runs.Mu.Lock() + runs.Token = 5 + runs.Mu.Unlock() decisions := &fakeDecisionStore{ target: DecisionTarget{ Type: CommandToolApprovalResponse, ID: "decision-approval", diff --git a/internal/agent/runtime/session/redis.go b/internal/agent/runtime/session/redis.go index 3f7160bf68..b1b42cdadd 100644 --- a/internal/agent/runtime/session/redis.go +++ b/internal/agent/runtime/session/redis.go @@ -210,7 +210,7 @@ func (b *RedisBackend) Load(ctx context.Context, key Key) (Snapshot, bool, error return Snapshot{}, false, err } var snapshot Snapshot - if err := json.Unmarshal(data, &snapshot); err != nil { + if err := unmarshalSnapshot(data, &snapshot); err != nil { return Snapshot{}, false, err } return snapshot, true, nil @@ -240,7 +240,7 @@ func (b *RedisBackend) Update(ctx context.Context, key Key, update SnapshotUpdat changed = false return nil } - data, err := json.Marshal(next) + data, err := marshalSnapshot(next) if err != nil { return err } @@ -304,7 +304,7 @@ func (b *RedisBackend) UpdateActiveRun(ctx context.Context, key Key, runID, gene changed = false return nil } - data, err := json.Marshal(next) + data, err := marshalSnapshot(next) if err != nil { return err } @@ -366,14 +366,22 @@ func (b *RedisBackend) releaseRun(ctx context.Context, key Key, ref RunRef, upda if err != nil { return err } - if !ok || !stateOK || current.CurrentRunView == nil { + if !stateOK || current.CurrentRunView == nil { return ErrRunOwnershipLost } run := current.CurrentRunView + if !ok { + // Expiry removes the routing lease, not the snapshot's receipt. + // No receipt means no proof: never infer a fence from run ID alone. + if requireLiveLease || run.FencingToken <= 0 || run.FencingToken != ref.FencingToken { + return ErrRunOwnershipLost + } + storedRef = RunRef{BotID: key.BotID, SessionID: key.SessionID, RunID: run.RunID, OwnerID: run.OwnerID, Generation: run.Generation, FencingToken: run.FencingToken} + } identityMismatch := !storedRef.identityMatches(ref) || run.RunID != ref.RunID || run.Generation != ref.Generation || run.OwnerID != ref.OwnerID leaseInvalid := !isActiveRunStatus(run.Status) || run.OwnerLeaseExpiresAt == nil || !now.Before(*run.OwnerLeaseExpiresAt) - fenceMismatch := !requireLiveLease && storedRef.FencingToken != ref.FencingToken + fenceMismatch := !requireLiveLease && (storedRef.FencingToken != ref.FencingToken || (run.FencingToken > 0 && run.FencingToken != ref.FencingToken)) if identityMismatch || fenceMismatch || (requireLiveLease && leaseInvalid) { return ErrRunOwnershipLost } @@ -386,7 +394,7 @@ func (b *RedisBackend) releaseRun(ctx context.Context, key Key, ref RunRef, upda changed = false return nil } - data, err := json.Marshal(next) + data, err := marshalSnapshot(next) if err != nil { return err } @@ -464,7 +472,7 @@ func (b *RedisBackend) StartRun(ctx context.Context, key Key, ref RunRef, update changed = false return nil } - stateData, err := json.Marshal(next) + stateData, err := marshalSnapshot(next) if err != nil { return err } @@ -710,7 +718,7 @@ func (b *RedisBackend) RenewLease(ctx context.Context, key Key, runID, ownerID, return ErrRunOwnershipLost } var snapshot Snapshot - if err := json.Unmarshal(stateData, &snapshot); err != nil { + if err := unmarshalSnapshot(stateData, &snapshot); err != nil { return err } if snapshot.CurrentRunView == nil { @@ -724,7 +732,7 @@ func (b *RedisBackend) RenewLease(ctx context.Context, key Key, runID, ownerID, return nil } run.OwnerLeaseExpiresAt = &expiresAt - nextStateData, err := json.Marshal(snapshot) + nextStateData, err := marshalSnapshot(snapshot) if err != nil { return err } @@ -1015,7 +1023,7 @@ func loadRedisSnapshot(ctx context.Context, tx *redis.Tx, key string) (Snapshot, return Snapshot{}, false, err } var snapshot Snapshot - if err := json.Unmarshal(data, &snapshot); err != nil { + if err := unmarshalSnapshot(data, &snapshot); err != nil { return Snapshot{}, false, err } return snapshot, true, nil diff --git a/internal/agent/runtime/session/redis_lease_index_test.go b/internal/agent/runtime/session/redis_lease_index_test.go index ed7978ef1d..a33103bd4c 100644 --- a/internal/agent/runtime/session/redis_lease_index_test.go +++ b/internal/agent/runtime/session/redis_lease_index_test.go @@ -261,15 +261,15 @@ func runRedisLeaseIndexContract(t *testing.T, redisURL string) { reserveRuntimeRun(ctx, t, backend, ref, time.Now().Add(-time.Minute)) runs := newFakeLedger() - runs.insertClaimed(runID, ref.SessionID, ref.FencingToken, generation) + runs.InsertClaimed(runID, ref.SessionID, ref.FencingToken, generation) reaper := newTestReaperWithLiveness(t, runs, backend, generation) reaper.tick(ctx) - if got := runs.state(runID); got != "lost" { + if got := runs.State(runID); got != "lost" { t.Fatalf("state = %q, want lost", got) } - if got := runs.errorCode(runID); got != runErrorOwnerLeaseExpired { + if got := runs.ErrorCode(runID); got != runErrorOwnerLeaseExpired { t.Fatalf("error code = %q, want %q", got, runErrorOwnerLeaseExpired) } if _, found := leaseCandidateFor(ctx, t, backend, runID); found { @@ -319,7 +319,7 @@ func runRedisLeaseIndexContract(t *testing.T, redisURL string) { } runs := newFakeLedger() - runs.insertClaimed(runID, ref.SessionID, ref.FencingToken, generation) + runs.InsertClaimed(runID, ref.SessionID, ref.FencingToken, generation) if _, applied, err := runs.PrepareFinish(ctx, ledger.PrepareFinishParams{ RunID: runID, FencingToken: ref.FencingToken, State: ledger.StateCompleted, }); err != nil || !applied { @@ -331,7 +331,7 @@ func runRedisLeaseIndexContract(t *testing.T, redisURL string) { reaper.tick(ctx) - if got := runs.state(runID); got != ledger.StateCompleted { + if got := runs.State(runID); got != ledger.StateCompleted { t.Fatalf("durable state = %q, want completed", got) } snapshot, ok, err := finishingBackend.Load(ctx, key) diff --git a/internal/agent/runtime/session/redis_live_queue.go b/internal/agent/runtime/session/redis_live_queue.go index 7e4cbadc2e..86dc24e92b 100644 --- a/internal/agent/runtime/session/redis_live_queue.go +++ b/internal/agent/runtime/session/redis_live_queue.go @@ -159,18 +159,16 @@ func (b *RedisBackend) EnqueueSteer(ctx context.Context, key Key, itemID, invoca if !active || queue.ClosedRunID == run.RunID { return ErrQueueNoActiveRun } - if countPendingSteers(queue) >= MaxPendingQueueItems { - return ErrQueueCapacityExceeded + if !SteerRunAvailable(run) { + return ErrQueueSteerUnsupported } now, err := tx.Time(ctx).Result() if err != nil { return err } - item = SteerItem{ID: SteerItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, TargetRunID: run.RunID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), Status: QueueAccepted, Position: nextSteerPosition(queue), CreatedAt: now.UTC()} - queue.Items = append(queue.Items, item) - queue.UpdatedAt = now.UTC() - if queue.ClosedRunID != "" && queue.ClosedRunID != run.RunID { - queue.ClosedRunID = "" + item, err = queue.enqueue(key, itemID, invocationID, run.RunID, payload, now.UTC()) + if err != nil { + return err } return storeRedisJSON(ctx, tx, queueKey, queue, b.stateTTL) }) @@ -206,16 +204,14 @@ func (b *RedisBackend) EnqueueFollowUp(ctx context.Context, key Key, itemID, inv if !active { return ErrQueueNoActiveRun } - if countPendingFollowUps(queue) >= MaxPendingQueueItems { - return ErrQueueCapacityExceeded - } now, err := tx.Time(ctx).Result() if err != nil { return err } - item = FollowUpItem{ID: FollowUpItemID(itemID), BotID: key.BotID, SessionID: key.SessionID, EnqueuedDuringRunID: run.RunID, InvocationID: invocationID, Payload: append([]byte(nil), payload...), Status: QueueAccepted, Position: nextFollowUpPosition(queue), CreatedAt: now.UTC()} - queue.Items = append(queue.Items, item) - queue.UpdatedAt = now.UTC() + item, err = queue.enqueue(key, itemID, invocationID, run.RunID, payload, now.UTC()) + if err != nil { + return err + } return storeRedisJSON(ctx, tx, queueKey, queue, b.stateTTL) }) return item, err @@ -281,14 +277,7 @@ func (b *RedisBackend) UpdateSteer(ctx context.Context, key Key, itemID SteerIte return SteerItem{}, ErrQueueInvalidReference } return redisMutate(ctx, b, b.steerQueueKey(key), func(state *steerQueueState, now time.Time) (SteerItem, error) { - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Payload = append([]byte(nil), payload...) - state.UpdatedAt = now - return cloneSteerItem(state.Items[i]), nil - } - } - return SteerItem{}, ErrQueueNotPending + return state.edit(itemID, payload, now) }) } @@ -297,14 +286,7 @@ func (b *RedisBackend) UpdateFollowUp(ctx context.Context, key Key, itemID Follo return FollowUpItem{}, ErrQueueInvalidReference } return redisMutate(ctx, b, b.followUpQueueKey(key), func(state *followUpQueueState, now time.Time) (FollowUpItem, error) { - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Payload = append([]byte(nil), payload...) - state.UpdatedAt = now - return cloneFollowUpItem(state.Items[i]), nil - } - } - return FollowUpItem{}, ErrQueueNotPending + return state.edit(itemID, payload, now) }) } @@ -313,14 +295,7 @@ func (b *RedisBackend) CancelSteer(ctx context.Context, key Key, itemID SteerIte return ErrQueueInvalidReference } _, err := redisMutate(ctx, b, b.steerQueueKey(key), func(state *steerQueueState, now time.Time) (struct{}, error) { - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Status = QueueCanceled - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueNotPending + return struct{}{}, state.cancel(itemID, now) }) return err } @@ -330,14 +305,7 @@ func (b *RedisBackend) CancelFollowUp(ctx context.Context, key Key, itemID Follo return ErrQueueInvalidReference } _, err := redisMutate(ctx, b, b.followUpQueueKey(key), func(state *followUpQueueState, now time.Time) (struct{}, error) { - for i := range state.Items { - if state.Items[i].ID == itemID && state.Items[i].Status == QueueAccepted { - state.Items[i].Status = QueueCanceled - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueNotPending + return struct{}{}, state.cancel(itemID, now) }) return err } @@ -364,14 +332,8 @@ func (b *RedisBackend) PromoteFollowUpToSteer(ctx context.Context, key Key, ref if !active || steers.ClosedRunID == run.RunID { return ErrQueueNoActiveRun } - if steerID := steers.PromotedFollowUpItems[string(ref.ItemID)]; steerID != "" { - for _, existing := range steers.Items { - if existing.ID == SteerItemID(steerID) { - result = PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(existing)} - return nil - } - } - return ErrQueueInvalidReference + if !SteerRunAvailable(run) { + return ErrQueueSteerUnsupported } follows, err := loadRedisJSON[followUpQueueState](ctx, tx, followKey) if err != nil { @@ -381,40 +343,24 @@ func (b *RedisBackend) PromoteFollowUpToSteer(ctx context.Context, key Key, ref if err != nil { return err } - for i := range follows.Items { - if follows.Items[i].ID != ref.ItemID || follows.Items[i].Status != QueueAccepted { - continue - } - if countPendingSteers(steers) >= MaxPendingQueueItems { - return ErrQueueCapacityExceeded - } - steer := SteerItem{ID: SteerItemID(uuid.NewString()), BotID: key.BotID, SessionID: key.SessionID, TargetRunID: run.RunID, InvocationID: "promote:" + string(ref.ItemID), Payload: append([]byte(nil), follows.Items[i].Payload...), Status: QueueAccepted, Position: nextSteerPosition(steers), CreatedAt: now.UTC()} - steers.Items = append(steers.Items, steer) - if steers.PromotedFollowUpItems == nil { - steers.PromotedFollowUpItems = make(map[string]string) - } - steers.PromotedFollowUpItems[string(ref.ItemID)] = string(steer.ID) - steers.UpdatedAt = now.UTC() - follows.Items[i].Status = QueueCanceled - follows.UpdatedAt = now.UTC() - follows.compact() - steerData, err := json.Marshal(steers) - if err != nil { - return err - } - followData, err := json.Marshal(follows) - if err != nil { - return err - } - _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.Set(ctx, steerKey, steerData, b.stateTTL) - pipe.Set(ctx, followKey, followData, b.stateTTL) - return nil - }) - result = PromoteFollowUpResult{FollowUp: ref, Steer: cloneSteerItem(steer)} + result, err = steers.promote(&follows, key, run.RunID, ref, now.UTC(), uuid.NewString()) + if err != nil { + return err + } + steerData, err := json.Marshal(steers) + if err != nil { + return err + } + followData, err := json.Marshal(follows) + if err != nil { return err } - return ErrQueueNotPending + _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.Set(ctx, steerKey, steerData, b.stateTTL) + pipe.Set(ctx, followKey, followData, b.stateTTL) + return nil + }) + return err }) return result, err } @@ -446,49 +392,18 @@ func (b *RedisBackend) ClaimNextSteer(ctx context.Context, handle RunHandle, sea if !ok || !refOK || !runMatchesHandle(snapshot.CurrentRunView, handle) || ref.FencingToken != handle.FencingToken || ref.OwnerID != handle.OwnerID || ref.Generation != handle.Generation || !isActiveRunStatus(snapshot.CurrentRunView.Status) { return ErrRunOwnershipLost } + if !SteerRunAvailable(snapshot.CurrentRunView) { + return ErrQueueSteerUnsupported + } state, err := loadRedisJSON[steerQueueState](ctx, tx, queueKey) if err != nil { return err } - for i := range state.Items { - existing := &state.Items[i] - if existing.Status == QueueClaimed && existing.Claim != nil && existing.Claim.RunID == handle.RunID { - item, claim, claimed = cloneSteerItem(*existing), *existing.Claim, true - if !advanceSteerClaim(existing, handle) { - return nil - } - now, err := tx.Time(ctx).Result() - if err != nil { - return err - } - state.UpdatedAt = now.UTC() - item, claim = cloneSteerItem(*existing), *existing.Claim - return storeRedisJSON(ctx, tx, queueKey, state, b.stateTTL) - } - } - best := -1 - for i := range state.Items { - if state.Items[i].Status == QueueAccepted && state.Items[i].TargetRunID == handle.RunID && (best < 0 || state.Items[i].Position < state.Items[best].Position) { - best = i - } - } - if best < 0 && !sealIfEmpty { - return nil - } now, err := tx.Time(ctx).Result() if err != nil { return err } - if best < 0 { - state.ClosedRunID = handle.RunID - state.UpdatedAt = now.UTC() - return storeRedisJSON(ctx, tx, queueKey, state, b.stateTTL) - } - claim = SteerClaimRef{ItemID: state.Items[best].ID, RunID: handle.RunID, OwnerID: handle.OwnerID, Generation: handle.Generation, FencingToken: handle.FencingToken, ClaimToken: uuid.NewString()} - state.Items[best].Status = QueueClaimed - state.Items[best].Claim = &claim - state.UpdatedAt = now.UTC() - item, claimed = cloneSteerItem(state.Items[best]), true + item, claim, claimed = state.claimNext(handle, sealIfEmpty, now.UTC()) return storeRedisJSON(ctx, tx, queueKey, state, b.stateTTL) }) return item, claim, claimed, err @@ -511,15 +426,7 @@ func (b *RedisBackend) ApplySteer(ctx context.Context, key Key, ref SteerClaimRe if !ok || !runOK || !runMatchesSteerClaim(snapshot.CurrentRunView, ref) || run.FencingToken != ref.FencingToken || run.OwnerID != ref.OwnerID || run.Generation != ref.Generation { return struct{}{}, ErrRunOwnershipLost } - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueApplied - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueInvalidReference + return struct{}{}, state.apply(ref, now) }) return err } @@ -553,16 +460,7 @@ func (b *RedisBackend) ReleaseSteer(ctx context.Context, key Key, ref SteerClaim if !ok || !runOK || !runMatchesSteerClaim(snapshot.CurrentRunView, ref) || run.FencingToken != ref.FencingToken || run.OwnerID != ref.OwnerID || run.Generation != ref.Generation { return struct{}{}, ErrRunOwnershipLost } - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueAccepted - state.Items[i].Claim = nil - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueInvalidReference + return struct{}{}, state.release(ref, now) }) return err } @@ -584,32 +482,8 @@ func (b *RedisBackend) ClaimNextFollowUp(ctx context.Context, key Key, triggerRu claimed bool } claimed, err := redisMutate(ctx, b, b.followUpQueueKey(key), func(state *followUpQueueState, now time.Time) (result, error) { - if state.TerminalClaims == nil { - state.TerminalClaims = make(map[string]string) - } - if itemID := state.TerminalClaims[triggerRunID]; itemID != "" { - for _, item := range state.Items { - if string(item.ID) == itemID && item.Status == QueueClaimed && item.Claim != nil { - return result{item: cloneFollowUpItem(item), claim: *item.Claim, claimed: true}, nil - } - } - return result{}, nil - } - best := -1 - for i := range state.Items { - if state.Items[i].Status == QueueAccepted && (best < 0 || state.Items[i].Position < state.Items[best].Position) { - best = i - } - } - if best < 0 { - return result{}, nil - } - claim := FollowUpClaimRef{ItemID: state.Items[best].ID, TriggerRunID: triggerRunID, ClaimToken: uuid.NewString()} - state.Items[best].Status = QueueClaimed - state.Items[best].Claim = &claim - state.TerminalClaims[triggerRunID] = string(state.Items[best].ID) - state.UpdatedAt = now - return result{item: cloneFollowUpItem(state.Items[best]), claim: claim, claimed: true}, nil + item, claim, ok := state.claimNext(triggerRunID, now) + return result{item: item, claim: claim, claimed: ok}, nil }) return claimed.item, claimed.claim, claimed.claimed, err } @@ -619,15 +493,7 @@ func (b *RedisBackend) ApplyFollowUp(ctx context.Context, key Key, ref FollowUpC return err } _, err := redisMutate(ctx, b, b.followUpQueueKey(key), func(state *followUpQueueState, now time.Time) (struct{}, error) { - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueApplied - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueInvalidReference + return struct{}{}, state.apply(ref, now) }) return err } @@ -637,17 +503,7 @@ func (b *RedisBackend) ReleaseFollowUp(ctx context.Context, key Key, ref FollowU return err } _, err := redisMutate(ctx, b, b.followUpQueueKey(key), func(state *followUpQueueState, now time.Time) (struct{}, error) { - for i := range state.Items { - claim := state.Items[i].Claim - if state.Items[i].ID == ref.ItemID && state.Items[i].Status == QueueClaimed && claim != nil && *claim == ref { - state.Items[i].Status = QueueAccepted - state.Items[i].Claim = nil - delete(state.TerminalClaims, ref.TriggerRunID) - state.UpdatedAt = now - return struct{}{}, nil - } - } - return struct{}{}, ErrQueueInvalidReference + return struct{}{}, state.release(ref, now) }) return err } diff --git a/internal/agent/runtime/session/redis_liveness.go b/internal/agent/runtime/session/redis_liveness.go index 50e1d64d19..bb0bb7d061 100644 --- a/internal/agent/runtime/session/redis_liveness.go +++ b/internal/agent/runtime/session/redis_liveness.go @@ -25,9 +25,8 @@ import ( // The write side belongs to the reservation itself: StartRun adds the member in // the same transaction that reserves the run, RenewLease moves its score in the // same script that extends the lease, and ReleaseRun and DeleteRunRef remove -// it from the stored ref's token. Runs started through the pre-ledger entry -// points carry no token and are deliberately absent — there is no durable row -// for a reaper to transition. +// it from the stored ref's token. Backend-only reservation tests carry no +// token and are absent from the index because they have no durable row. const leaseIndexMemberFields = 4 // expiredLeaseCandidatesScript samples TIME inside the script so a reaper with a diff --git a/internal/agent/runtime/session/redis_test.go b/internal/agent/runtime/session/redis_test.go index b4911ee6f6..0b4738def4 100644 --- a/internal/agent/runtime/session/redis_test.go +++ b/internal/agent/runtime/session/redis_test.go @@ -344,7 +344,7 @@ func runRedisDurableCommandResultContract(t *testing.T, redisURL string) { t.Fatalf("record approval: %v", err) } - handledResult, err := remote.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) + handledResult, err := remote.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) if err != nil || !handledResult { t.Fatalf("dispatch with dropped pubsub result = handled:%v err:%v", handledResult, err) } @@ -364,7 +364,7 @@ func runRedisDurableCommandResultContract(t *testing.T, redisURL string) { OwnerID: "durable-result-restarted", StateTTL: time.Minute, OwnerLeaseTTL: time.Second, CommandAckTTL: 750 * time.Millisecond, }) - handledResult, err = restarted.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) + handledResult, err = restarted.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) if err != nil || !handledResult { t.Fatalf("dispatch after requester restart = handled:%v err:%v", handledResult, err) } @@ -374,13 +374,13 @@ func runRedisDurableCommandResultContract(t *testing.T, redisURL string) { }); err != nil { t.Fatalf("record barrier approval: %v", err) } - if handledResult, err = restarted.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, barrierApprovalID, []byte(`{"decision":"approve"}`)); err != nil || !handledResult { + if handledResult, err = restarted.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, barrierApprovalID, []byte(`{"decision":"approve"}`)); err != nil || !handledResult { t.Fatalf("dispatch barrier command = handled:%v err:%v", handledResult, err) } if barrier := receiveTestResult(t, "barrier command handler", handled); barrier.TargetID != barrierApprovalID { t.Fatalf("stable retry executed before barrier: %#v", barrier) } - handledResult, err = restarted.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"reject"}`)) + handledResult, err = restarted.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"reject"}`)) if !handledResult || !errors.Is(err, ErrCommandPayloadConflict) { t.Fatalf("conflicting stable retry = handled:%v err:%v, want payload conflict", handledResult, err) } @@ -392,7 +392,7 @@ func runRedisDurableCommandResultContract(t *testing.T, redisURL string) { t.Fatalf("record owner-local approval: %v", err) } for attempt := range 2 { - handledResult, err = owner.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localApprovalID, []byte(`{"decision":"approve"}`)) + handledResult, err = owner.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localApprovalID, []byte(`{"decision":"approve"}`)) if err != nil || !handledResult { t.Fatalf("owner-local dispatch %d = handled:%v err:%v", attempt, handledResult, err) } @@ -406,13 +406,13 @@ func runRedisDurableCommandResultContract(t *testing.T, redisURL string) { }); err != nil { t.Fatalf("record owner-local barrier approval: %v", err) } - if handledResult, err = owner.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localBarrierApprovalID, []byte(`{"decision":"approve"}`)); err != nil || !handledResult { + if handledResult, err = owner.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localBarrierApprovalID, []byte(`{"decision":"approve"}`)); err != nil || !handledResult { t.Fatalf("dispatch owner-local barrier = handled:%v err:%v", handledResult, err) } if barrier := receiveTestResult(t, "owner-local barrier command handler", handled); barrier.TargetID != localBarrierApprovalID { t.Fatalf("owner-local stable retry executed before barrier: %#v", barrier) } - handledResult, err = owner.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localApprovalID, []byte(`{"decision":"reject"}`)) + handledResult, err = owner.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, localApprovalID, []byte(`{"decision":"reject"}`)) if !handledResult || !errors.Is(err, ErrCommandPayloadConflict) { t.Fatalf("owner-local conflicting retry = handled:%v err:%v, want payload conflict", handledResult, err) } @@ -491,7 +491,7 @@ func runRedisBoundedCommandWorkersContract(t *testing.T, redisURL string) { for i := range commandCount { i := i go func() { - handled, err := remote.DispatchActiveCommand( + handled, err := remote.dispatchTestCommand( context.Background(), testBotID, fmt.Sprintf("session-bounded-worker-%d", i), CommandToolApprovalResponse, fmt.Sprintf("approval-bounded-worker-%d", i), []byte(`{"decision":"approve"}`), ) @@ -541,7 +541,7 @@ func runRedisBoundedCommandWorkersContract(t *testing.T, redisURL string) { if busyIndex < 0 { t.Fatal("missing busy command index") } - handled, err := remote.DispatchActiveCommand( + handled, err := remote.dispatchTestCommand( context.Background(), testBotID, fmt.Sprintf("session-bounded-worker-%d", busyIndex), CommandToolApprovalResponse, fmt.Sprintf("approval-bounded-worker-%d", busyIndex), []byte(`{"decision":"approve"}`), ) @@ -626,7 +626,7 @@ func runRedisDuplicateCommandSaturationContract(t *testing.T, redisURL string) { results := make(chan dispatchResult, 3) dispatch := func(name, sessionID, approvalID string) { go func() { - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, sessionID, CommandToolApprovalResponse, approvalID, []byte(`{"decision":"approve"}`)) results <- dispatchResult{name: name, handled: handled, err: err} }() } @@ -637,7 +637,7 @@ func runRedisDuplicateCommandSaturationContract(t *testing.T, redisURL string) { if err != nil || snapshotB.CurrentRunView == nil { t.Fatalf("load queued run B: %v %#v", err, snapshotB.CurrentRunView) } - commandBID := activeCommandID(testBotID, sessionB, snapshotB.CurrentRunView, CommandToolApprovalResponse, approvalB) + commandBID := testCommandID(testBotID, sessionB, snapshotB.CurrentRunView, CommandToolApprovalResponse, approvalB) dispatch("queued-b", sessionB, approvalB) deadline := time.Now().Add(time.Second) for { @@ -658,7 +658,7 @@ func runRedisDuplicateCommandSaturationContract(t *testing.T, redisURL string) { if err != nil || snapshotA.CurrentRunView == nil { t.Fatalf("load active run A: %v %#v", err, snapshotA.CurrentRunView) } - commandAID := activeCommandID(testBotID, sessionA, snapshotA.CurrentRunView, CommandToolApprovalResponse, approvalA) + commandAID := testCommandID(testBotID, sessionA, snapshotA.CurrentRunView, CommandToolApprovalResponse, approvalA) deadline = time.Now().Add(time.Second) for { remote.mu.Lock() @@ -751,7 +751,7 @@ func runRedisExpiredActiveResponseTransportContract(t *testing.T, redisURL strin } dispatchDone := make(chan dispatchResult, 1) go func() { - handled, err := remote.DispatchActiveCommand(context.Background(), testBotID, "session-expired-command", request.commandType, request.targetID, []byte(`{"ok":true}`)) + handled, err := remote.dispatchTestCommand(context.Background(), testBotID, "session-expired-command", request.commandType, request.targetID, []byte(`{"ok":true}`)) dispatchDone <- dispatchResult{handled: handled, err: err} }() var command Command @@ -863,19 +863,8 @@ func runRedisSubscriptionReconnectContract(t *testing.T, redisURL string) { if next.Type != EventRuntimeDelta || next.Seq != checkpoint.Seq+1 { t.Fatalf("post-reconnect event = %#v, want continuous delta after seq %d", next, checkpoint.Seq) } - if _, err := remote.Steer(context.Background(), testBotID, "session-reconnect", "stream-reconnect", "steer after reconnect"); err != nil { - t.Fatalf("steer after reconnect: %v", err) - } - select { - case injected := <-injectCh: - if injected.Text != "steer after reconnect" { - t.Fatalf("injected text = %q", injected.Text) - } - if injected.Applied != nil { - injected.Applied() - } - case <-time.After(2 * time.Second): - t.Fatal("remote steer was not delivered after Pub/Sub reconnect") + if applied, err := remote.Abort(context.Background(), testBotID, "session-reconnect", "stream-reconnect"); err != nil || !applied { + t.Fatalf("remote abort after Pub/Sub reconnect: applied=%v err=%v", applied, err) } } diff --git a/internal/agent/runtime/session/reservation_fixture_test.go b/internal/agent/runtime/session/reservation_fixture_test.go new file mode 100644 index 0000000000..3dd8099a0f --- /dev/null +++ b/internal/agent/runtime/session/reservation_fixture_test.go @@ -0,0 +1,38 @@ +package sessionruntime + +import ( + "context" + + "github.com/felinics/memoh/internal/agent/turn" +) + +// Package-local backend/ownership tests isolate live reservation algorithms. +// Application and ACP tests use sessiontest.Start and the public Admit path. +func (m *Manager) StartRun(ctx context.Context, botID, sessionID, runID string, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) error { + _, err := m.StartRunHandle(ctx, botID, sessionID, runID, abortCh, cancel, injectCh) + return err +} + +func (m *Manager) StartRunHandle(ctx context.Context, botID, sessionID, runID string, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { + return m.StartRunWithAdmissionBuilderHandle(ctx, botID, sessionID, runID, func(context.Context, RunHandle) (RunAdmissionView, error) { + return RunAdmissionView{}, nil + }, abortCh, cancel, injectCh) +} + +func (m *Manager) StartRunWithAdmissionBuilderHandle(ctx context.Context, botID, sessionID, runID string, builder func(context.Context, RunHandle) (RunAdmissionView, error), abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { + return m.StartRunWithAdmissionBuilderAndOwnershipHandle(ctx, botID, sessionID, runID, builder, nil, abortCh, cancel, injectCh) +} + +func (m *Manager) StartRunWithAdmissionBuilderAndOwnershipHandle(ctx context.Context, botID, sessionID, runID string, builder func(context.Context, RunHandle) (RunAdmissionView, error), ownershipCancel context.CancelCauseFunc, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (RunHandle, error) { + handle, _, err := m.startRun(ctx, runStart{ + botID: botID, + sessionID: sessionID, + runID: runID, + builder: builder, + ownershipCancel: ownershipCancel, + abortCh: abortCh, + cancel: cancel, + injectCh: injectCh, + }) + return handle, err +} diff --git a/internal/agent/runtime/session/run_view_codec.go b/internal/agent/runtime/session/run_view_codec.go new file mode 100644 index 0000000000..cafd94698a --- /dev/null +++ b/internal/agent/runtime/session/run_view_codec.go @@ -0,0 +1,90 @@ +package sessionruntime + +import ( + "encoding/json" + + chatview "github.com/felinics/memoh/internal/agent/view" +) + +// These wire views share one input adapter without copying the domain's field +// list. Backend snapshot codecs use them directly so nested MarshalJSON calls +// do not serialize and scan the complete message stream a second time. +type runViewFields CurrentRunView + +type runViewWire struct { + runViewFields + RequestUserTurn *chatview.UITurn `json:"request_user_turn,omitempty"` +} + +type snapshotFields Snapshot + +type snapshotWire struct { + snapshotFields + CurrentRunView *runViewWire `json:"current_run_view,omitempty"` +} + +// A steer-only projection must not masquerade as the run's original input. +func (run CurrentRunView) requestUserTurn() *chatview.UITurn { + if len(run.UserTurns) == 0 { + return nil + } + first := &run.UserTurns[0] + if run.TurnID != "" && first.TurnID != "" && run.TurnID != first.TurnID { + return nil + } + return first +} + +func (run CurrentRunView) wireView() runViewWire { + return runViewWire{runViewFields: runViewFields(run), RequestUserTurn: run.requestUserTurn()} +} + +// Decode old snapshots once, then retain only canonical inputs in live state. +// If both forms exist, user_turns wins over a stale legacy copy. +func (wire runViewWire) value() CurrentRunView { + run := CurrentRunView(wire.runViewFields) + if len(run.UserTurns) == 0 { + switch { + case wire.RequestUserTurn != nil: + run.UserTurns = []chatview.UITurn{*wire.RequestUserTurn} + case run.Operation != nil && run.Operation.ReplacementUserTurn != nil: + run.UserTurns = []chatview.UITurn{*run.Operation.ReplacementUserTurn} + } + } + return run +} + +func (run CurrentRunView) MarshalJSON() ([]byte, error) { + return json.Marshal(run.wireView()) +} + +func (run *CurrentRunView) UnmarshalJSON(data []byte) error { + wire := runViewWire{runViewFields: runViewFields(*run)} + if err := json.Unmarshal(data, &wire); err != nil { + return err + } + *run = wire.value() + return nil +} + +func marshalSnapshot(snapshot Snapshot) ([]byte, error) { + wire := snapshotWire{snapshotFields: snapshotFields(snapshot)} + if snapshot.CurrentRunView != nil { + view := snapshot.CurrentRunView.wireView() + wire.CurrentRunView = &view + } + return json.Marshal(wire) +} + +func unmarshalSnapshot(data []byte, snapshot *Snapshot) error { + var wire snapshotWire + if err := json.Unmarshal(data, &wire); err != nil { + return err + } + *snapshot = Snapshot(wire.snapshotFields) + if wire.CurrentRunView != nil { + view := wire.CurrentRunView.value() + snapshot.CurrentRunView = &view + } + return nil +} diff --git a/internal/agent/runtime/session/run_view_codec_test.go b/internal/agent/runtime/session/run_view_codec_test.go new file mode 100644 index 0000000000..8d5cdee9dc --- /dev/null +++ b/internal/agent/runtime/session/run_view_codec_test.go @@ -0,0 +1,61 @@ +package sessionruntime + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + chatview "github.com/felinics/memoh/internal/agent/view" +) + +func TestCurrentRunViewInputCodec(t *testing.T) { + for _, tc := range []struct { + name, data, text, turnID string + legacy bool + }{ + {"legacy", `{"turn_id":"root","request_user_turn":{"turn_id":"root","role":"user","text":"old"}}`, "old", "root", true}, + {"canonical wins", `{"turn_id":"root","request_user_turn":{"turn_id":"root","role":"user","text":"stale"},"user_turns":[{"turn_id":"root","role":"user","text":"current"}]}`, "current", "root", true}, + {"replacement", `{"turn_id":"root","operation":{"kind":"edit","replacement_user_turn":{"turn_id":"root","role":"user","text":"edited"}}}`, "edited", "root", true}, + {"steer only", `{"turn_id":"root","user_turns":[{"turn_id":"steer","role":"user","text":"later"}]}`, "later", "steer", false}, + } { + t.Run(tc.name, func(t *testing.T) { + var run CurrentRunView + var snapshot Snapshot + require.NoError(t, json.Unmarshal([]byte(tc.data), &run)) + require.NoError(t, unmarshalSnapshot([]byte(`{"current_run_view":`+tc.data+`}`), &snapshot)) + require.Equal(t, &run, snapshot.CurrentRunView, "public and backend decoders") + require.Len(t, run.UserTurns, 1) + require.Equal(t, tc.text, run.UserTurns[0].Text) + // Canonical updates must appear in the legacy wire, except that a steer + // cannot become the run's original input. Both encoders must agree. + run.UserTurns[0].Text = "persisted" + data, err := json.Marshal(run) + require.NoError(t, err) + stored, err := marshalSnapshot(Snapshot{CurrentRunView: &run}) + require.NoError(t, err) + var fields struct { + Run json.RawMessage `json:"current_run_view"` + } + require.NoError(t, json.Unmarshal(stored, &fields)) + require.JSONEq(t, string(data), string(fields.Run)) + var wire struct { + RequestUserTurn json.RawMessage `json:"request_user_turn"` + UserTurns []chatview.UITurn `json:"user_turns"` + } + require.NoError(t, json.Unmarshal(data, &wire)) + require.Len(t, wire.UserTurns, 1) + require.Equal(t, tc.turnID, wire.UserTurns[0].TurnID) + if tc.legacy { + var legacy chatview.UITurn + require.NoError(t, json.Unmarshal(wire.RequestUserTurn, &legacy)) + require.Equal(t, "persisted", legacy.Text) + } else { + require.Nil(t, wire.RequestUserTurn) + } + var restored CurrentRunView + require.NoError(t, json.Unmarshal(data, &restored)) + require.Equal(t, run, restored, "roundtrip keeps canonical inputs") + }) + } +} diff --git a/internal/agent/runtime/session/state.go b/internal/agent/runtime/session/state.go index 2f9c544326..7dfb556737 100644 --- a/internal/agent/runtime/session/state.go +++ b/internal/agent/runtime/session/state.go @@ -72,7 +72,7 @@ func decisionEventID(event native.StreamEvent) string { return strings.TrimSpace(event.ToolCallID) } -func runtimeRunPatch(snapshot Snapshot, status, runError, steer, lease bool) RuntimeDelta { +func runtimeRunPatch(snapshot Snapshot, status, runError, lease bool) RuntimeDelta { run := snapshot.CurrentRunView if run == nil { return RuntimeDelta{} @@ -92,10 +92,6 @@ func runtimeRunPatch(snapshot Snapshot, status, runError, steer, lease bool) Run value := run.Error patch.Error = &value } - if steer && run.Steer != nil { - value := *run.Steer - patch.Steer = &value - } if lease { value := time.Time{} if run.OwnerLeaseExpiresAt != nil { diff --git a/internal/agent/runtime/session/terminal_observer_test.go b/internal/agent/runtime/session/terminal_observer_test.go index 915e63a52f..b226d0dcae 100644 --- a/internal/agent/runtime/session/terminal_observer_test.go +++ b/internal/agent/runtime/session/terminal_observer_test.go @@ -58,7 +58,7 @@ func TestFinishRunWithErrorCodePersistsStableCodeWithoutDiagnostic(t *testing.T) t.Fatal(err) } - writes := fixture.runs.terminalWrites() + writes := fixture.runs.TerminalWrites() if len(writes) != 1 { t.Fatalf("terminal writes = %d, want 1", len(writes)) } @@ -77,12 +77,12 @@ func TestFinishRunRetriesTransientDurableFailuresWhileRetainingOwnership(t *test } transient := errors.New("database temporarily unavailable") if phase == "prepare" { - fixture.runs.setPrepareErr(transient) + fixture.runs.SetPrepareErr(transient) } else { if _, err := fixture.manager.HandleAgentEvent(context.Background(), admission.Handle, native.StreamEvent{Type: native.EventAgentEnd}); err != nil { t.Fatalf("prepare terminal event: %v", err) } - fixture.runs.setFinalizeErr(transient) + fixture.runs.SetFinalizeErr(transient) } err = fixture.manager.FinishRun(context.Background(), admission.Handle, RunStatusCompleted, "") @@ -92,8 +92,8 @@ func TestFinishRunRetriesTransientDurableFailuresWhileRetainingOwnership(t *test if fixture.manager.localControlForHandle(admission.Handle) == nil { t.Fatal("owner control was dropped before durable retry could converge") } - fixture.runs.setPrepareErr(nil) - fixture.runs.setFinalizeErr(nil) + fixture.runs.SetPrepareErr(nil) + fixture.runs.SetFinalizeErr(nil) deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { @@ -121,7 +121,7 @@ func TestFinishRunStopsDurableRetryAfterBudget(t *testing.T) { if err != nil { t.Fatal(err) } - fixture.runs.setPrepareErr(errors.New("database remains unavailable")) + fixture.runs.SetPrepareErr(errors.New("database remains unavailable")) if err := fixture.manager.FinishRun(context.Background(), admission.Handle, RunStatusCompleted, ""); err == nil { t.Fatal("FinishRun() error = nil, want initial durable failure") @@ -129,7 +129,7 @@ func TestFinishRunStopsDurableRetryAfterBudget(t *testing.T) { deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { if fixture.manager.localControlForHandle(admission.Handle) == nil { - if got := fixture.runs.state(admission.RunID); got != ledger.StateRunning { + if got := fixture.runs.State(admission.RunID); got != ledger.StateRunning { t.Fatalf("ledger state after unprepared retry timeout = %q, want running for reaper", got) } return @@ -172,12 +172,12 @@ func TestMemoryRuntimeReaperConvergesExhaustedDurableFinish(t *testing.T) { transient := errors.New("database remains unavailable") if phase == "prepare" { - runs.setPrepareErr(transient) + runs.SetPrepareErr(transient) } else { if _, err := manager.HandleAgentEvent(context.Background(), admission.Handle, native.StreamEvent{Type: native.EventAgentEnd}); err != nil { t.Fatalf("prepare terminal event: %v", err) } - runs.setFinalizeErr(transient) + runs.SetFinalizeErr(transient) } if err := manager.FinishRun(context.Background(), admission.Handle, RunStatusCompleted, ""); err == nil { t.Fatal("FinishRun() error = nil, want initial durable failure") @@ -190,7 +190,7 @@ func TestMemoryRuntimeReaperConvergesExhaustedDurableFinish(t *testing.T) { if manager.localControlForHandle(admission.Handle) != nil { t.Fatal("owner control remains after retry budget") } - runs.setFinalizeErr(nil) + runs.SetFinalizeErr(nil) want := ledger.StateLost if phase == "finalize" { @@ -199,13 +199,13 @@ func TestMemoryRuntimeReaperConvergesExhaustedDurableFinish(t *testing.T) { deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { snapshot, snapshotErr := manager.Snapshot(context.Background(), testBotID, "session-memory-handoff-"+phase) - if runs.state(admission.RunID) == want && snapshotErr == nil && snapshot.CurrentRunView != nil && + if runs.State(admission.RunID) == want && snapshotErr == nil && snapshot.CurrentRunView != nil && snapshot.CurrentRunView.Status == liveRunStatus(want) { return } time.Sleep(5 * time.Millisecond) } - t.Fatalf("memory reaper state = %q, want %q", runs.state(admission.RunID), want) + t.Fatalf("memory reaper state = %q, want %q", runs.State(admission.RunID), want) }) } } @@ -226,7 +226,7 @@ func TestFinishRunRejectsOwnerProposedLostWithoutRetry(t *testing.T) { if fixture.manager.localControlForHandle(admission.Handle) == nil { t.Fatal("invalid owner terminal state scheduled a retry that dropped control") } - if got := fixture.runs.state(admission.RunID); got != ledger.StateRunning { + if got := fixture.runs.State(admission.RunID); got != ledger.StateRunning { t.Fatalf("ledger state = %q, want running after rejected owner proposal", got) } } @@ -238,7 +238,7 @@ func TestAgentTerminalProposalFailureDefersOutcomeToFinish(t *testing.T) { t.Fatal(err) } transient := errors.New("proposal write temporarily unavailable") - fixture.runs.setPrepareErr(transient) + fixture.runs.SetPrepareErr(transient) if _, err := fixture.manager.HandleAgentEvent(context.Background(), admission.Handle, native.StreamEvent{Type: native.EventAgentEnd}); err != nil { t.Fatalf("HandleAgentEvent() error = %v, want terminal publication to continue", err) @@ -250,15 +250,15 @@ func TestAgentTerminalProposalFailureDefersOutcomeToFinish(t *testing.T) { if snapshot.CurrentRunView == nil || snapshot.CurrentRunView.Status != RunStatusRunning { t.Fatalf("live run after degraded proposal = %#v, want running", snapshot.CurrentRunView) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateRunning { + if got := fixture.runs.State(admission.RunID); got != ledger.StateRunning { t.Fatalf("ledger after degraded proposal = %q, want running", got) } - fixture.runs.setPrepareErr(nil) + fixture.runs.SetPrepareErr(nil) if err := fixture.manager.FinishRun(context.Background(), admission.Handle, RunStatusCompleted, ""); err != nil { t.Fatalf("FinishRun() after recovery: %v", err) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateCompleted { + if got := fixture.runs.State(admission.RunID); got != ledger.StateCompleted { t.Fatalf("final ledger state = %q, want completed", got) } } @@ -282,7 +282,7 @@ func TestUnnamedFinishCarriesProjectedStableErrorCodeToLedger(t *testing.T) { if err := fixture.manager.FinishRun(context.Background(), admission.Handle, "", ""); err != nil { t.Fatal(err) } - writes := fixture.runs.terminalWrites() + writes := fixture.runs.TerminalWrites() if len(writes) != 1 || writes[0].ErrorCode != "agent.response_interrupted" || writes[0].ErrorMessage != "" { t.Fatalf("terminal writes = %#v", writes) } @@ -382,12 +382,12 @@ func TestFinishRunObservesTerminalNewerFenceButRejectsStaleOwner(t *testing.T) { ctrl.leaseStop = stopLease ctrl.leaseDone = leaseDone ctrl.leaseLifecycleMu.Unlock() - fixture.runs.mu.Lock() - run := fixture.runs.runs[admission.RunID] + fixture.runs.Mu.Lock() + run := fixture.runs.Runs[admission.RunID] run.FencingToken++ run.State = ledger.StateAborted newToken := run.FencingToken - fixture.runs.mu.Unlock() + fixture.runs.Mu.Unlock() var observed []TerminalRun fixture.manager.SetTerminalObserver(func(_ context.Context, run TerminalRun) { observed = append(observed, run) @@ -434,7 +434,7 @@ func TestFinishRunDoesNotObserveWaitingDecision(t *testing.T) { if len(observed) != 0 { t.Fatalf("waiting decision emitted terminal observations: %+v", observed) } - if got := fixture.runs.state(admission.RunID); got != ledger.StateWaitingDecision { + if got := fixture.runs.State(admission.RunID); got != ledger.StateWaitingDecision { t.Fatalf("ledger state = %q, want waiting_decision", got) } } diff --git a/internal/agent/runtime/session/terminal_reconciler_test.go b/internal/agent/runtime/session/terminal_reconciler_test.go index 46ab5006e7..1b44eb178b 100644 --- a/internal/agent/runtime/session/terminal_reconciler_test.go +++ b/internal/agent/runtime/session/terminal_reconciler_test.go @@ -2,6 +2,8 @@ package sessionruntime import ( "context" + "errors" + "fmt" "sync/atomic" "testing" "time" @@ -43,3 +45,41 @@ func TestMemoryManagerRunsTerminalReconcilerUntilClose(t *testing.T) { t.Fatalf("terminal reconciler calls after Close = %d, want %d", got, closedCalls) } } + +func runExpiredLeaseTerminalReceiptContract(t *testing.T, suite distributedRuntimeBackendContractSuite) { + t.Helper() + for _, receipt := range []int64{7, 0, 8} { + t.Run(fmt.Sprintf("receipt-%d", receipt), func(t *testing.T) { + b := suite.newBackend(t) + t.Cleanup(func() { _ = b.Close() }) + ctx := context.Background() + key := Key{BotID: "receipt-bot", SessionID: "receipt-session"} + ref := RunRef{BotID: key.BotID, SessionID: key.SessionID, RunID: "receipt-run", OwnerID: "owner", Generation: "generation", FencingToken: 7} + deadline := time.Now().Add(time.Minute) + if _, changed, err := b.StartRun(ctx, key, ref, func(snapshot Snapshot, _ bool) (Snapshot, bool, error) { + snapshot = EmptySnapshot(key.BotID, key.SessionID) + snapshot.CurrentRunView = &CurrentRunView{RunID: ref.RunID, OwnerID: ref.OwnerID, Generation: ref.Generation, FencingToken: receipt, Status: RunStatusFinishing, OwnerLeaseExpiresAt: &deadline} + return snapshot, true, nil + }); err != nil || !changed { + t.Fatalf("reserve: changed=%v err=%v", changed, err) + } + // Expiry removes the lease key. Terminal reconciliation must prove the + // token from the snapshot, without treating receipt retention as liveness. + if _, err := b.DeleteRunRef(ctx, ref); err != nil { + t.Fatal(err) + } + _, changed, err := b.ReconcileTerminalRun(ctx, key, ref, func(snapshot Snapshot, _ time.Time) (Snapshot, bool, error) { + snapshot.CurrentRunView.Status = RunStatusCompleted + snapshot.CurrentRunView.OwnerLeaseExpiresAt = nil + return snapshot, true, nil + }) + if receipt == 7 { + if err != nil || !changed { + t.Fatalf("matching receipt: changed=%v err=%v", changed, err) + } + } else if !errors.Is(err, ErrRunOwnershipLost) || changed { + t.Fatalf("unproved/successor receipt was overwritten: changed=%v err=%v", changed, err) + } + }) + } +} diff --git a/internal/agent/runtime/session/types.go b/internal/agent/runtime/session/types.go index abe6a29d7e..3d200b18f8 100644 --- a/internal/agent/runtime/session/types.go +++ b/internal/agent/runtime/session/types.go @@ -31,16 +31,10 @@ const ( RunStatusErrored = "errored" RunStatusLost = "lost" - SteerStatusPending = "pending" - SteerStatusQueued = "queued" - SteerStatusApplied = "applied" - SteerStatusRejected = "rejected" - RunOperationRetry = "retry" RunOperationEdit = "edit" CommandAbort = "abort" - CommandSteer = "steer_current_run" CommandToolApprovalResponse = "tool_approval_response" CommandUserInputResponse = "user_input_response" CommandHistoryReset = "history_reset" @@ -181,7 +175,7 @@ type RunHandle struct { // FencingToken is the ledger ownership token for this run. Callers need it // to fence their own durable writes, which is why it travels with the // handle rather than staying inside the runtime. It is zero for runs - // started through the pre-ledger entry points. + // created by backend-only reservation tests. FencingToken int64 } @@ -278,12 +272,16 @@ type CurrentRunView struct { StartedAt time.Time `json:"started_at"` UpdatedAt time.Time `json:"updated_at"` Messages []chatview.UIMessage `json:"messages"` - RequestUserTurn *chatview.UITurn `json:"request_user_turn,omitempty"` // UserTurns is the authoritative ordered set of user inputs already - // admitted into this run. It starts with RequestUserTurn when one exists and - // grows when a live steer is applied. RequestUserTurn remains on the wire - // for backwards compatibility with clients that only understand one input. + // admitted into this run, including the original input and applied steers. + // The legacy request_user_turn is derived only at the JSON boundary. UserTurns []chatview.UITurn `json:"user_turns,omitempty"` + // SteerSupported is published only by an installed step-boundary consumer. + // Missing on old owners and on runtimes without that execution capability. + SteerSupported bool `json:"steer_supported,omitempty"` + // The snapshot outlives the lease key and retains the exact persistence + // fence needed to reconcile a durable terminal after owner expiry. + FencingToken int64 `json:"fencing_token,omitempty"` // SteerTurns locates live queue inputs inside the run's assistant message // stream. Claimed entries are provisional runtime state; applied entries // point at the history turn written by the application. @@ -292,7 +290,6 @@ type CurrentRunView struct { Error string `json:"error,omitempty"` ProposedTerminalStatus string `json:"proposed_terminal_status,omitempty"` FinishProposedAt *time.Time `json:"finish_proposed_at,omitempty"` - Steer *SteerState `json:"steer,omitempty"` Operation *RunOperationView `json:"operation,omitempty"` } @@ -320,15 +317,6 @@ type RunOperationView struct { ReplacementUserTurn *chatview.UITurn `json:"replacement_user_turn,omitempty"` } -type SteerState struct { - ID string `json:"id"` - Status string `json:"status"` - Text string `json:"text,omitempty"` - Error string `json:"error,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - type Event struct { Type string `json:"type"` BotID string `json:"bot_id"` @@ -357,13 +345,12 @@ type RuntimeDelta struct { } type CurrentRunPatch struct { - RunID string `json:"run_id"` - Status *string `json:"status,omitempty"` - ErrorCode *string `json:"error_code,omitempty"` - Error *string `json:"error,omitempty"` - Steer *SteerState `json:"steer,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` - OwnerLeaseExpiresAt *time.Time `json:"owner_lease_expires_at,omitempty"` + RunID string `json:"run_id"` + Status *string `json:"status,omitempty"` + ErrorCode *string `json:"error_code,omitempty"` + Error *string `json:"error,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + OwnerLeaseExpiresAt *time.Time `json:"owner_lease_expires_at,omitempty"` } type RuntimeMessageAppend struct { @@ -392,8 +379,6 @@ type Command struct { // before routing. Owner-side execution must not consult the live UI // projection again: it is derived state and may lag the durable decision. DecisionResolved bool `json:"decision_resolved,omitempty"` - SteerID string `json:"steer_id,omitempty"` - Text string `json:"text,omitempty"` Payload json.RawMessage `json:"payload,omitempty"` PayloadHash string `json:"payload_hash,omitempty"` ErrorCode string `json:"error_code,omitempty"` diff --git a/internal/agent/runtime/session/user_turns.go b/internal/agent/runtime/session/user_turns.go index a7ad61118f..6c10ba0976 100644 --- a/internal/agent/runtime/session/user_turns.go +++ b/internal/agent/runtime/session/user_turns.go @@ -26,6 +26,19 @@ type QueueUserTurnUpdate struct { AfterStepIndex *int } +// ContinuationStepIndex resumes the owner-local step cursor after a parked +// decision. A recovered owner starts a new generation/cursor; it is not a +// durable replay offset and must never be inferred from timestamps or messages. +func (m *Manager) ContinuationStepIndex(handle RunHandle) (int, error) { + ctrl := m.localControlForHandle(handle.normalized()) + if ctrl == nil { + return 0, ErrRunOwnershipLost + } + ctrl.stepMu.Lock() + defer ctrl.stepMu.Unlock() + return ctrl.stepConsumed, nil +} + // PublishQueueUserTurns projects one committed queue step into the live run. // The update is atomic so applying one steer and claiming the next cannot // briefly render them out of order. A claimed steer is shown only after the @@ -72,14 +85,6 @@ func (m *Manager) PublishQueueUserTurns(ctx context.Context, handle RunHandle, u if !runMatchesHandle(run, handle) || !m.runOwnerMatches(run) || !isActiveRunStatus(run.Status) { return snapshot, false, ErrRunOwnershipLost } - if len(run.UserTurns) == 0 { - switch { - case run.RequestUserTurn != nil: - run.UserTurns = append(run.UserTurns, *run.RequestUserTurn) - case run.Operation != nil && run.Operation.ReplacementUserTurn != nil: - run.UserTurns = append(run.UserTurns, *run.Operation.ReplacementUserTurn) - } - } changed := false for _, incoming := range normalized { index := -1 diff --git a/internal/agent/turn/grpctransport/transport_test.go b/internal/agent/turn/grpctransport/transport_test.go index 1882daf678..e06e26ad35 100644 --- a/internal/agent/turn/grpctransport/transport_test.go +++ b/internal/agent/turn/grpctransport/transport_test.go @@ -22,6 +22,11 @@ import ( intrpc "github.com/felinics/memoh/internal/rpc" ) +const ( + legacySessionIDKey = "SessionID" + internalThreadIDKey = "ThreadID" +) + func TestStartTurnRoundTrip(t *testing.T) { fake := &fakeService{} client, cleanup := newTestClient(t, fake, "secret") @@ -125,6 +130,24 @@ func TestLegacySessionIDJSONWireCompatibility(t *testing.T) { if threadID != "thread-1" { t.Fatalf("roundtrip ThreadID = %q, want thread-1", threadID) } + for _, tc := range []struct { + name, data, want string + invalid bool + }{ + {"legacy", `{"SessionID":"legacy"}`, "legacy", false}, + {"internal", `{"ThreadID":"internal"}`, "internal", false}, + {"both", `{"SessionID":"same","ThreadID":"same"}`, "", true}, + {"null-conflict", `{"SessionID":null,"ThreadID":"internal"}`, "", true}, + {"invalid-id", `{"SessionID":123}`, "", true}, + {"absent", `{}`, "", false}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := tt.unmarshal([]byte(tc.data)) + if (err != nil) != tc.invalid || (!tc.invalid && got != tc.want) { + t.Fatalf("decoded %s: id=%q err=%v", tc.data, got, err) + } + }) + } }) } } diff --git a/internal/agent/turn/grpctransport/wire.go b/internal/agent/turn/grpctransport/wire.go index 293950bc1a..ba12c725f5 100644 --- a/internal/agent/turn/grpctransport/wire.go +++ b/internal/agent/turn/grpctransport/wire.go @@ -9,8 +9,6 @@ import ( ) const ( - legacySessionIDKey = "SessionID" - internalThreadIDKey = "ThreadID" // turnDeferredStatusMessage is part of the private gRPC vocabulary. Keep it // centralized because the client uses it to distinguish a deferred // admission result from other ResourceExhausted failures. @@ -21,65 +19,88 @@ const ( // terminology. Keep its JSON field named SessionID so independently deployed // server and channel binaries remain compatible while the domain contract uses // ThreadID exclusively. +// A tagged field at the same embedding depth shadows the domain ThreadID. +// The output still contains SessionID only; the domain JSON used by queue +// payloads is unchanged. Embedding avoids copying the command's field list. +type outgoingThreadID struct { + SessionID string + ThreadID *string `json:"ThreadID,omitempty"` +} + +type incomingThreadID struct { + SessionID json.RawMessage `json:"SessionID"` + ThreadID json.RawMessage `json:"ThreadID"` +} + func marshalStartTurnCommand(cmd turn.StartTurnCommand) ([]byte, error) { - return marshalLegacyThreadID(cmd) + return json.Marshal(struct { + turn.StartTurnCommand + outgoingThreadID + }{cmd, outgoingThreadID{SessionID: cmd.ThreadID}}) } func unmarshalStartTurnCommand(data []byte, cmd *turn.StartTurnCommand) error { - return unmarshalLegacyThreadID(data, cmd) + if cmd == nil { + return json.Unmarshal(data, cmd) + } + wire := struct { + *turn.StartTurnCommand + incomingThreadID + }{StartTurnCommand: cmd} + return unmarshalLegacyThreadID(data, &wire, &wire.incomingThreadID, &cmd.ThreadID) } func marshalToolApprovalResponse(input turn.ToolApprovalResponse) ([]byte, error) { - return marshalLegacyThreadID(input) + return json.Marshal(struct { + turn.ToolApprovalResponse + outgoingThreadID + }{input, outgoingThreadID{SessionID: input.ThreadID}}) } func unmarshalToolApprovalResponse(data []byte, input *turn.ToolApprovalResponse) error { - return unmarshalLegacyThreadID(data, input) + if input == nil { + return json.Unmarshal(data, input) + } + wire := struct { + *turn.ToolApprovalResponse + incomingThreadID + }{ToolApprovalResponse: input} + return unmarshalLegacyThreadID(data, &wire, &wire.incomingThreadID, &input.ThreadID) } func marshalUserInputResponse(input turn.UserInputResponse) ([]byte, error) { - return marshalLegacyThreadID(input) + return json.Marshal(struct { + turn.UserInputResponse + outgoingThreadID + }{input, outgoingThreadID{SessionID: input.ThreadID}}) } func unmarshalUserInputResponse(data []byte, input *turn.UserInputResponse) error { - return unmarshalLegacyThreadID(data, input) -} - -func marshalLegacyThreadID(value any) ([]byte, error) { - data, err := json.Marshal(value) - if err != nil { - return nil, err - } - var fields map[string]json.RawMessage - if err := json.Unmarshal(data, &fields); err != nil { - return nil, err + if input == nil { + return json.Unmarshal(data, input) } - threadID, ok := fields[internalThreadIDKey] - if !ok { - return nil, errors.New("turn rpc: internal ThreadID field missing") - } - delete(fields, internalThreadIDKey) - fields[legacySessionIDKey] = threadID - return json.Marshal(fields) + wire := struct { + *turn.UserInputResponse + incomingThreadID + }{UserInputResponse: input} + return unmarshalLegacyThreadID(data, &wire, &wire.incomingThreadID, &input.ThreadID) } -func unmarshalLegacyThreadID(data []byte, value any) error { - var fields map[string]json.RawMessage - if err := json.Unmarshal(data, &fields); err != nil { +func unmarshalLegacyThreadID(data []byte, wire any, ids *incomingThreadID, threadID *string) error { + if err := json.Unmarshal(data, wire); err != nil { return err } - if _, hasLegacy := fields[legacySessionIDKey]; hasLegacy { - if _, hasInternal := fields[internalThreadIDKey]; hasInternal { - return errors.New("turn rpc: ambiguous SessionID and ThreadID fields") - } - fields[internalThreadIDKey] = fields[legacySessionIDKey] - delete(fields, legacySessionIDKey) + if ids.SessionID != nil && ids.ThreadID != nil { + return errors.New("turn rpc: ambiguous SessionID and ThreadID fields") } - adapted, err := json.Marshal(fields) - if err != nil { - return err + value := ids.ThreadID + if ids.SessionID != nil { + value = ids.SessionID + } + if value == nil { + return nil } - return json.Unmarshal(adapted, value) + return json.Unmarshal(value, threadID) } func eventFromProto(event *turnpb.EventResponse) turn.Event { diff --git a/internal/apperror/error.go b/internal/apperror/error.go index ff2146cbcd..66ed55a574 100644 --- a/internal/apperror/error.go +++ b/internal/apperror/error.go @@ -86,6 +86,7 @@ const ( CodeSessionHistoryInconsistent Code = "session_runtime.history_inconsistent" CodeAgentResponseTimeout Code = "agent.response_timeout" CodeAgentResponseInterrupted Code = "agent.response_interrupted" + CodeQueueSteerUnsupported Code = "queue.steer_unsupported" CodeQueueNoActiveRun Code = "queue_no_active_run" CodeQueueAdmissionOverloaded Code = "queue_admission_overloaded" CodeQueueAdmissionUnavailable Code = "queue_admission_unavailable" @@ -465,6 +466,10 @@ var catalog = map[Code]Definition{ HTTPStatus: http.StatusBadGateway, Detail: "The model response was interrupted. Please try again.", }, + CodeQueueSteerUnsupported: { + HTTPStatus: http.StatusConflict, + Detail: "This run cannot accept steer input. Wait for it to finish and send a new message.", + }, CodeQueueNoActiveRun: { HTTPStatus: http.StatusConflict, Detail: "There is no active run to receive this queued input.", diff --git a/internal/chat/message/runtime_fence_postgres_integration_test.go b/internal/chat/message/runtime_fence_postgres_integration_test.go index efe36490ac..a861b24f97 100644 --- a/internal/chat/message/runtime_fence_postgres_integration_test.go +++ b/internal/chat/message/runtime_fence_postgres_integration_test.go @@ -17,7 +17,6 @@ import ( "github.com/felinics/memoh/internal/db/dbtest" dbsqlc "github.com/felinics/memoh/internal/db/postgres/sqlc" postgresstore "github.com/felinics/memoh/internal/db/postgres/store" - dbstore "github.com/felinics/memoh/internal/db/store" "github.com/felinics/memoh/internal/runtimefence" ) @@ -295,12 +294,8 @@ func TestPostgresRuntimeFenceAgentReplacementStepsFinalizeVisibility(t *testing. Content: []byte(`{"role":"assistant","content":"replacement answer"}`), TurnRequestMessageID: user.ID, SkipHistoryTurn: true, }}} - var hidden []Message - if err := storeQueries.InTx(owner, func(txq dbstore.Queries) error { - var persistErr error - hidden, persistErr = service.PersistAgentReplacementStepTx(owner, txq, step) - return persistErr - }); err != nil { + hidden, err := service.PersistAgentReplacementStep(owner, step) + if err != nil { t.Fatalf("persist hidden replacement step: %v", err) } visible, err := service.ListBySession(ctx, sessionID.String()) @@ -312,9 +307,7 @@ func TestPostgresRuntimeFenceAgentReplacementStepsFinalizeVisibility(t *testing. OldTurnID: oldTurn.ID, ReplacementTurnID: replacementTurnID, ReplacementTurnPosition: &replacementPosition, RequestMessageID: user.ID, Reason: "retry", } - if err := storeQueries.InTx(owner, func(txq dbstore.Queries) error { - return service.FinalizeAgentReplacementTx(owner, txq, sessionID.String(), replacement, user.ID, hidden[0].ID) - }); err != nil { + if err := service.FinalizeAgentReplacement(owner, sessionID.String(), replacement, user.ID, hidden[0].ID); err != nil { t.Fatalf("finalize replacement history: %v", err) } visible, err = service.ListBySession(ctx, sessionID.String()) diff --git a/internal/chat/message/service.go b/internal/chat/message/service.go index 2b052cb6dc..8c4fa8b958 100644 --- a/internal/chat/message/service.go +++ b/internal/chat/message/service.go @@ -941,16 +941,10 @@ func resolveRuntimeSnapshotWithQueries(ctx context.Context, queries dbstore.Quer return sessionMode, runtimeType } +// Database rows already carry the backfilled, constrained descriptor. Legacy +// request/import normalization belongs at those input boundaries. func sessionSnapshotFromRow(row sqlc.BotSession) (string, string) { - sessionMode := normalizeSessionMode(row.SessionMode) - if sessionMode == "" { - sessionMode = legacySessionMode(row.Type) - } - runtimeType := normalizeRuntimeType(row.RuntimeType) - if runtimeType == "" { - runtimeType = legacyRuntimeType(row.Type) - } - return sessionMode, runtimeType + return row.SessionMode, row.RuntimeType } func normalizeSessionMode(mode string) string { @@ -974,24 +968,6 @@ func normalizeRuntimeType(runtimeType string) string { return string(kind) } -func legacySessionMode(typ string) string { - switch strings.TrimSpace(typ) { - case "acp_agent": - return "chat" - case "discuss", "schedule", "subagent": - return strings.TrimSpace(typ) - default: - return "chat" - } -} - -func legacyRuntimeType(typ string) string { - if strings.TrimSpace(typ) == "acp_agent" { - return "acp_agent" - } - return "model" -} - // List returns all messages for a bot. func (s *DBService) List(ctx context.Context, botID string) ([]Message, error) { pgBotID, err := dbpkg.ParseUUID(botID) diff --git a/internal/chat/message/step_commit.go b/internal/chat/message/step_commit.go index 73326a55bc..7ae3374092 100644 --- a/internal/chat/message/step_commit.go +++ b/internal/chat/message/step_commit.go @@ -25,45 +25,35 @@ type agentStepQueries interface { // Complete steps precede abort intent; interrupted checkpoints remain writable // until terminal finalization for cancellation paths without recorded intent. func (s *DBService) PersistAgentStep(ctx context.Context, step AgentStep) ([]Message, error) { - botID, sessionID, err := validateAgentStep(ctx, s, step) + return s.persistAgentStep(ctx, step, false) +} + +// PersistAgentReplacementStep keeps retry/edit output hidden until the true +// final boundary. Both step kinds use the same fenced persistence transaction. +func (s *DBService) PersistAgentReplacementStep(ctx context.Context, step AgentStep) ([]Message, error) { + return s.persistAgentStep(ctx, step, true) +} + +func (s *DBService) persistAgentStep(ctx context.Context, step AgentStep, replacement bool) ([]Message, error) { + botID, sessionID, err := validateAgentStepMode(ctx, s, step, replacement) if err != nil { return nil, err } var persisted []Message err = runtimefence.InTransaction(ctx, s.queries, botID, sessionID, func(queries dbstore.Queries) error { var txErr error - persisted, txErr = s.PersistAgentStepTx(ctx, queries, step) + persisted, txErr = s.persistAgentStepTx(ctx, queries, step, replacement) return txErr }) if err != nil { return nil, err } - s.PublishAgentStep(persisted) - return persisted, nil -} - -// PublishAgentStep emits the post-commit notifications normally owned by -// PersistAgentStep. Coordinator-owned transactions call this after their outer -// commit; keeping it here preserves one publication path for all message rows. -func (s *DBService) PublishAgentStep(messages []Message) { - for _, message := range messages { - s.publishMessageCreated(message) + if !replacement { + for _, message := range persisted { + s.publishMessageCreated(message) + } } -} - -// PersistAgentStepTx persists an agent step inside the caller's transaction. -// Publishing remains the outer operation's responsibility and happens only -// after that transaction commits. -func (s *DBService) PersistAgentStepTx(ctx context.Context, queries dbstore.Queries, step AgentStep) ([]Message, error) { - return s.persistAgentStepTx(ctx, queries, step, false) -} - -// PersistAgentReplacementStepTx appends a retry/edit step without projecting -// it into visible history. The queue coordinator owns the surrounding -// transaction, so a step and any queue claim transition succeed or roll back -// together. -func (s *DBService) PersistAgentReplacementStepTx(ctx context.Context, queries dbstore.Queries, step AgentStep) ([]Message, error) { - return s.persistAgentStepTx(ctx, queries, step, true) + return persisted, nil } func (s *DBService) persistAgentStepTx(ctx context.Context, queries dbstore.Queries, step AgentStep, replacement bool) ([]Message, error) { @@ -124,41 +114,31 @@ func (s *DBService) persistAgentStepTx(ctx context.Context, queries dbstore.Quer return persisted, nil } -// FinalizeAgentReplacementTx makes the accumulated hidden retry/edit output -// the canonical visible turn. It deliberately does not open a transaction; -// the caller must run it inside the same coordinator transaction that -// terminalizes R0 and assigns a follow-up continuation. -func (s *DBService) FinalizeAgentReplacementTx( - ctx context.Context, - queries dbstore.Queries, - sessionID string, - replacement TurnReplacement, - requestMessageID string, - assistantMessageID string, -) error { - if s == nil || s.queries == nil || queries == nil { - return errors.New("replacement persistence transaction is not configured") +// FinalizeAgentReplacement atomically selects the completed replacement turn. +// Queue coordination is transient and does not own this database transaction. +func (s *DBService) FinalizeAgentReplacement(ctx context.Context, sessionID string, replacement TurnReplacement, requestMessageID, assistantMessageID string) error { + if s == nil || s.queries == nil { + return errors.New("replacement persistence is not configured") } - if _, ok := runtimefence.FromContext(ctx); !ok { + fence, ok := runtimefence.FromContext(ctx) + if !ok { return errors.New("agent replacement requires a runtime persistence fence") } - requestMessageID = strings.TrimSpace(requestMessageID) - assistantMessageID = strings.TrimSpace(assistantMessageID) - if requestMessageID == "" || assistantMessageID == "" { - return errors.New("agent replacement requires request and assistant message ids") - } - txService := *s - txService.queries = queries - txService.publisher = nil - replacement.RequestMessageID = requestMessageID - return txService.replacePersistedRound(ctx, strings.TrimSpace(sessionID), []Message{ - {ID: requestMessageID, Role: "user"}, - {ID: assistantMessageID, Role: "assistant"}, - }, replacement) -} - -func validateAgentStep(ctx context.Context, s *DBService, step AgentStep) (string, string, error) { - return validateAgentStepMode(ctx, s, step, false) + return runtimefence.InTransaction(ctx, s.queries, fence.BotID, sessionID, func(queries dbstore.Queries) error { + requestMessageID = strings.TrimSpace(requestMessageID) + assistantMessageID = strings.TrimSpace(assistantMessageID) + if requestMessageID == "" || assistantMessageID == "" { + return errors.New("agent replacement requires request and assistant message ids") + } + txService := *s + txService.queries = queries + txService.publisher = nil + replacement.RequestMessageID = requestMessageID + return txService.replacePersistedRound(ctx, strings.TrimSpace(sessionID), []Message{ + {ID: requestMessageID, Role: "user"}, + {ID: assistantMessageID, Role: "assistant"}, + }, replacement) + }) } func validateAgentStepMode(ctx context.Context, s *DBService, step AgentStep, replacement bool) (string, string, error) { diff --git a/internal/chat/message/types.go b/internal/chat/message/types.go index 2a44aa6b2c..b343a28609 100644 --- a/internal/chat/message/types.go +++ b/internal/chat/message/types.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "time" - - dbstore "github.com/felinics/memoh/internal/db/store" ) const ( @@ -208,36 +206,11 @@ type AgentStepPersister interface { PersistAgentStep(ctx context.Context, step AgentStep) ([]Message, error) } -// AgentStepTxPersister appends a step using a transaction already owned by a -// higher-level coordinator. Implementations must not open or commit a second -// transaction. This is intentionally separate from AgentStepPersister so -// legacy callers cannot accidentally bypass their normal transaction wrapper. -type AgentStepTxPersister interface { - PersistAgentStepTx(ctx context.Context, queries dbstore.Queries, step AgentStep) ([]Message, error) -} - -// AgentStepPublisher emits the normal post-commit message notifications for a -// step persisted inside a coordinator-owned transaction. It must only be -// called after that outer transaction has committed. -type AgentStepPublisher interface { - PublishAgentStep(messages []Message) -} - -// AgentReplacementTxPersister persists the hidden step deltas produced by a -// retry/edit run and publishes the replacement history inside a transaction -// owned by the session queue coordinator. Hidden deltas must not become the -// visible turn until FinalizeAgentReplacementTx is called at the true final -// boundary (after the coordinator has ruled out an eligible steer). -type AgentReplacementTxPersister interface { - PersistAgentReplacementStepTx(ctx context.Context, queries dbstore.Queries, step AgentStep) ([]Message, error) - FinalizeAgentReplacementTx( - ctx context.Context, - queries dbstore.Queries, - sessionID string, - replacement TurnReplacement, - requestMessageID string, - assistantMessageID string, - ) error +// AgentReplacementPersister owns the fenced database transactions for hidden +// retry/edit steps and their final visible-turn replacement. +type AgentReplacementPersister interface { + PersistAgentReplacementStep(context.Context, AgentStep) ([]Message, error) + FinalizeAgentReplacement(context.Context, string, TurnReplacement, string, string) error } // Service defines message read/write behavior. diff --git a/internal/contextview/steer_continuation_test.go b/internal/contextview/steer_continuation_test.go new file mode 100644 index 0000000000..67146d84b3 --- /dev/null +++ b/internal/contextview/steer_continuation_test.go @@ -0,0 +1,63 @@ +package contextview + +import ( + "context" + "strings" + "sync/atomic" + "testing" + + sdk "github.com/felinics/twilight/sdk" + + contextfrag "github.com/felinics/memoh/internal/agent/context/fragment" + native "github.com/felinics/memoh/internal/agent/runtime/native" +) + +func TestFinalSteerSurvivesProductionContextCompilationAndStepCapture(t *testing.T) { + var continueAfter atomic.Bool + var next []sdk.Message + var indexes []int + var captured [][]sdk.Message + provider := &envelopeProbeProvider{handler: func(call int, params sdk.GenerateParams) (*sdk.GenerateResult, error) { + if call == 2 { + var text strings.Builder + for _, message := range params.Messages { + if message.Role == sdk.MessageRoleUser { + text.Reset() + for _, part := range message.Content { + if p, ok := part.(sdk.TextPart); ok { + text.WriteString(p.Text) + } + } + } + } + if !strings.Contains(text.String(), "new direction") { + t.Errorf("typed context dropped the steer: %q", text.String()) + } + } + return &sdk.GenerateResult{Text: "answer", FinishReason: sdk.FinishReasonStop}, nil + }} + cfg := native.RunConfig{ + Model: &sdk.Model{ID: "model", Provider: provider}, Messages: []sdk.Message{sdk.UserMessage("original")}, + ContextSourceFrags: []contextfrag.ContextFrag{currentMessageFrag("message.000", "original")}, + ContextBudgetMaxTokens: 200000, ContextQueryMaterialized: true, + ContinueAfterFinal: &continueAfter, NextModelInputs: &next, + OnStepCommitted: func(_ context.Context, index int, step *sdk.StepResult) error { + indexes = append(indexes, index) + captured = append(captured, step.Messages) + if index == 0 { + next = []sdk.Message{sdk.UserMessage("new direction")} + continueAfter.Store(true) + } + return nil + }, + } + if _, err := native.New(native.Deps{ContextViewApplier: ProviderRunConfigApplier(nil)}).Generate(context.Background(), cfg); err != nil { + t.Fatal(err) + } + if provider.calls.Load() != 2 || len(indexes) != 2 || indexes[0] != 0 || indexes[1] != 1 { + t.Fatalf("calls=%d indexes=%v", provider.calls.Load(), indexes) + } + if len(captured[1]) < 2 || captured[1][0].Role != sdk.MessageRoleUser { + t.Fatalf("steer input missing at persistence barrier: %+v", captured[1]) + } +} diff --git a/internal/handlers/session_queue.go b/internal/handlers/session_queue.go index 44047230cc..c6a31989a1 100644 --- a/internal/handlers/session_queue.go +++ b/internal/handlers/session_queue.go @@ -13,7 +13,7 @@ import ( "github.com/felinics/memoh/internal/accounts" "github.com/felinics/memoh/internal/agent/application" - "github.com/felinics/memoh/internal/agent/runtime/session/queue" + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" "github.com/felinics/memoh/internal/apperror" "github.com/felinics/memoh/internal/auth" "github.com/felinics/memoh/internal/bots" @@ -58,18 +58,18 @@ type updateQueueRequest struct { Text string `json:"text" validate:"required"` } type steerQueueItemResponse struct { - ItemID queue.SteerItemID `json:"item_id"` - Status queue.Status `json:"status"` - Position int64 `json:"position"` - Text string `json:"text"` - TargetRunID string `json:"target_run_id"` + ItemID sessionruntime.SteerItemID `json:"item_id"` + Status sessionruntime.QueueStatus `json:"status"` + Position int64 `json:"position"` + Text string `json:"text"` + TargetRunID string `json:"target_run_id"` } type followUpQueueItemResponse struct { - ItemID queue.FollowUpItemID `json:"item_id"` - Status queue.Status `json:"status"` - Position int64 `json:"position"` - Text string `json:"text"` - EnqueuedDuringRunID string `json:"enqueued_during_run_id"` + ItemID sessionruntime.FollowUpItemID `json:"item_id"` + Status sessionruntime.QueueStatus `json:"status"` + Position int64 `json:"position"` + Text string `json:"text"` + EnqueuedDuringRunID string `json:"enqueued_during_run_id"` } type steerQueueResponse struct { Items []steerQueueItemResponse `json:"items"` @@ -78,16 +78,17 @@ type followUpQueueResponse struct { Items []followUpQueueItemResponse `json:"items"` } type sessionQueueResponse struct { - Steer []steerQueueItemResponse `json:"steer"` - FollowUp []followUpQueueItemResponse `json:"follow_up"` + SteerSupported bool `json:"steer_supported"` + Steer []steerQueueItemResponse `json:"steer"` + FollowUp []followUpQueueItemResponse `json:"follow_up"` } type steerQueueReorderRequest struct { - Item queue.SteerPendingRef `json:"item"` - Before queue.SteerPendingRef `json:"before"` + Item sessionruntime.SteerPendingRef `json:"item"` + Before sessionruntime.SteerPendingRef `json:"before"` } type followUpQueueReorderRequest struct { - Item queue.FollowUpPendingRef `json:"item"` - Before queue.FollowUpPendingRef `json:"before"` + Item sessionruntime.FollowUpPendingRef `json:"item"` + Before sessionruntime.FollowUpPendingRef `json:"before"` } func (h *SessionQueueHandler) authorize(c echo.Context) (string, string, error) { @@ -174,15 +175,17 @@ func queueAdmissionError(err error) error { switch { case err == nil: return nil - case errors.Is(err, queue.ErrNoActiveRun): + case errors.Is(err, sessionruntime.ErrQueueSteerUnsupported): + return apperror.New(apperror.CodeQueueSteerUnsupported, nil) + case errors.Is(err, sessionruntime.ErrQueueNoActiveRun): return apperror.New(apperror.CodeQueueNoActiveRun, nil) - case errors.Is(err, queue.ErrInvocationConflict): + case errors.Is(err, sessionruntime.ErrQueueInvocationConflict): return apperror.New(apperror.CodeSessionInvocationConflict, nil) - case errors.Is(err, queue.ErrAdmissionOverloaded): + case errors.Is(err, sessionruntime.ErrQueueAdmissionOverloaded): return apperror.New(apperror.CodeQueueAdmissionOverloaded, nil) - case errors.Is(err, queue.ErrCapacityExceeded): + case errors.Is(err, sessionruntime.ErrQueueCapacityExceeded): return apperror.New(apperror.CodeQueueCapacityExceeded, nil) - case errors.Is(err, queue.ErrInvalidReference): + case errors.Is(err, sessionruntime.ErrQueueInvalidReference): return apperror.New(apperror.CodeQueueRequestInvalid, nil) default: return err @@ -193,11 +196,13 @@ func queueMutationError(err error) error { switch { case err == nil: return nil - case errors.Is(err, queue.ErrNoActiveRun): + case errors.Is(err, sessionruntime.ErrQueueSteerUnsupported): + return apperror.New(apperror.CodeQueueSteerUnsupported, nil) + case errors.Is(err, sessionruntime.ErrQueueNoActiveRun): return apperror.New(apperror.CodeQueueNoActiveRun, nil) - case errors.Is(err, queue.ErrNotPending), errors.Is(err, queue.ErrInvalidReference): + case errors.Is(err, sessionruntime.ErrQueueNotPending), errors.Is(err, sessionruntime.ErrQueueInvalidReference): return apperror.New(apperror.CodeQueueItemNotPending, nil) - case errors.Is(err, queue.ErrCapacityExceeded): + case errors.Is(err, sessionruntime.ErrQueueCapacityExceeded): return apperror.New(apperror.CodeQueueCapacityExceeded, nil) default: return err @@ -374,8 +379,9 @@ func (h *SessionQueueHandler) ListSessionQueue(c echo.Context) error { return err } return c.JSON(http.StatusOK, sessionQueueResponse{ - Steer: mapQueueItems(queues.Steer, steerQueueItemResponseFrom), - FollowUp: mapQueueItems(queues.FollowUp, followUpQueueItemResponseFrom), + SteerSupported: queues.SteerSupported, + Steer: mapQueueItems(queues.Steer, steerQueueItemResponseFrom), + FollowUp: mapQueueItems(queues.FollowUp, followUpQueueItemResponseFrom), }) } @@ -577,19 +583,19 @@ func (h *SessionQueueHandler) PromoteFollowUpToSteer(c echo.Context) error { if err != nil { return err } - result, err := h.agentService.PromoteFollowUpToSteer(c.Request().Context(), botID, sid, queue.FollowUpPendingRef{ItemID: queue.FollowUpItemID(itemID)}) + result, err := h.agentService.PromoteFollowUpToSteer(c.Request().Context(), botID, sid, sessionruntime.FollowUpPendingRef{ItemID: sessionruntime.FollowUpItemID(itemID)}) if err = queueMutationError(err); err != nil { return err } return c.JSON(http.StatusAccepted, steerQueueItemResponseFrom(result.Steer)) } -func steerQueueItemResponseFrom(item queue.SteerItem) steerQueueItemResponse { - return steerQueueItemResponse{ItemID: item.ID, Status: item.Status, Position: item.Position, Text: queuePayloadText(item.Payload), TargetRunID: item.TargetRunID} +func steerQueueItemResponseFrom(item sessionruntime.SteerItem) steerQueueItemResponse { + return steerQueueItemResponse{ItemID: item.ID, Status: item.Status, Position: item.Position, Text: application.QueuePayloadText(item.Payload), TargetRunID: item.TargetRunID} } -func followUpQueueItemResponseFrom(item queue.FollowUpItem) followUpQueueItemResponse { - return followUpQueueItemResponse{ItemID: item.ID, Status: item.Status, Position: item.Position, Text: queuePayloadText(item.Payload), EnqueuedDuringRunID: item.EnqueuedDuringRunID} +func followUpQueueItemResponseFrom(item sessionruntime.FollowUpItem) followUpQueueItemResponse { + return followUpQueueItemResponse{ItemID: item.ID, Status: item.Status, Position: item.Position, Text: application.QueuePayloadText(item.Payload), EnqueuedDuringRunID: item.EnqueuedDuringRunID} } func mapQueueItems[T any, R any](items []T, mapItem func(T) R) []R { @@ -599,13 +605,3 @@ func mapQueueItems[T any, R any](items []T, mapItem func(T) R) []R { } return out } - -func queuePayloadText(payload []byte) string { - var body struct { - Text string `json:"text"` - } - if json.Unmarshal(payload, &body) == nil && strings.TrimSpace(body.Text) != "" { - return body.Text - } - return strings.TrimSpace(string(payload)) -} diff --git a/internal/handlers/session_queue_test.go b/internal/handlers/session_queue_test.go index e7c668cad9..1273619085 100644 --- a/internal/handlers/session_queue_test.go +++ b/internal/handlers/session_queue_test.go @@ -9,7 +9,7 @@ import ( "github.com/labstack/echo/v4" - "github.com/felinics/memoh/internal/agent/runtime/session/queue" + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" ) func TestSessionQueueHandlerRegistersSeparateQueueRoutes(t *testing.T) { @@ -46,24 +46,24 @@ func TestSessionQueueReorderRequestsDecodeTypedReferences(t *testing.T) { steerContext := e.NewContext(httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(body)), httptest.NewRecorder()) steerContext.Request().Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) steer, err := decodeSteerReorderRequest(steerContext) - if err != nil || steer.Item.ItemID != queue.SteerItemID(itemID) || steer.Before.ItemID != queue.SteerItemID(beforeID) { + if err != nil || steer.Item.ItemID != sessionruntime.SteerItemID(itemID) || steer.Before.ItemID != sessionruntime.SteerItemID(beforeID) { t.Fatalf("steer reorder request = %#v, %v", steer, err) } followContext := e.NewContext(httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(body)), httptest.NewRecorder()) followContext.Request().Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) follow, err := decodeFollowUpReorderRequest(followContext) - if err != nil || follow.Item.ItemID != queue.FollowUpItemID(itemID) || follow.Before.ItemID != queue.FollowUpItemID(beforeID) { + if err != nil || follow.Item.ItemID != sessionruntime.FollowUpItemID(itemID) || follow.Before.ItemID != sessionruntime.FollowUpItemID(beforeID) { t.Fatalf("follow-up reorder request = %#v, %v", follow, err) } } func TestSessionQueueResponsesDoNotUseMixedQueueKind(t *testing.T) { - steerJSON, err := json.Marshal(steerQueueItemResponseFrom(queue.SteerItem{ID: "steer", Status: queue.Accepted, Position: 1, Payload: []byte(`{"text":"s"}`), TargetRunID: "run-0"})) + steerJSON, err := json.Marshal(steerQueueItemResponseFrom(sessionruntime.SteerItem{ID: "steer", Status: sessionruntime.QueueAccepted, Position: 1, Payload: []byte(`{"text":"s"}`), TargetRunID: "run-0"})) if err != nil { t.Fatal(err) } - followJSON, err := json.Marshal(followUpQueueItemResponseFrom(queue.FollowUpItem{ID: "follow", Status: queue.Accepted, Position: 2, Payload: []byte(`{"text":"f"}`), EnqueuedDuringRunID: "run-0"})) + followJSON, err := json.Marshal(followUpQueueItemResponseFrom(sessionruntime.FollowUpItem{ID: "follow", Status: sessionruntime.QueueAccepted, Position: 2, Payload: []byte(`{"text":"f"}`), EnqueuedDuringRunID: "run-0"})) if err != nil { t.Fatal(err) } diff --git a/internal/testutil/sessionledger/ledger.go b/internal/testutil/sessionledger/ledger.go new file mode 100644 index 0000000000..7e7827c26c --- /dev/null +++ b/internal/testutil/sessionledger/ledger.go @@ -0,0 +1,374 @@ +// Package sessionledger supplies a shared in-memory ledger for runtime and +// application tests. No production composition root uses this fixture. +package sessionledger + +import ( + "context" + "sort" + "sync" + "time" + + "github.com/felinics/memoh/internal/agent/runtime/session/ledger" +) + +// Store is an in-memory ledger with the same guarantees the PostgreSQL +// adapter provides: one active run per session, fenced idempotent transitions, +// and a monotonic token sequence. It exists so admission ordering can be tested +// without a database; the adapter's own SQL is covered by its integration test. +type Store struct { + Mu sync.Mutex + Runs map[string]*ledger.Run + // bySession preserves insertion order so ActiveRun is deterministic. + Order []string + Token int64 + + AdmitErr error + ClaimErr error + TokenErr error + PrepareErr error + FinalizeErr error + ClaimHook func(runID string) + + Admits int + Claims int + Finalized []ledger.FinalizeParams +} + +func New() *Store { + return &Store{Runs: map[string]*ledger.Run{}} +} + +func (f *Store) Admit(_ context.Context, params ledger.AdmitParams) (ledger.Run, bool, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + f.Admits++ + if f.AdmitErr != nil { + return ledger.Run{}, false, f.AdmitErr + } + for _, id := range f.Order { + run := f.Runs[id] + if run.SessionID != params.SessionID { + continue + } + if run.InvocationID == params.InvocationID { + return *run, false, nil + } + if run.State.Active() { + return ledger.Run{}, false, ledger.ErrSessionBusy + } + } + position := int64(1) + for _, id := range f.Order { + if f.Runs[id].SessionID == params.SessionID { + position++ + } + } + run := &ledger.Run{ + RunID: params.RunID, + BotID: params.BotID, + SessionID: params.SessionID, + InvocationID: params.InvocationID, + TurnID: params.TurnID, + TurnPosition: position, + State: ledger.StateAccepted, + Input: params.Input, + InputFingerprint: params.InputFingerprint, + CreatedAt: time.Now(), + } + f.Runs[run.RunID] = run + f.Order = append(f.Order, run.RunID) + return *run, true, nil +} + +func (f *Store) Get(_ context.Context, runID string) (ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + run, ok := f.Runs[runID] + if !ok { + return ledger.Run{}, ledger.ErrRunNotFound + } + return *run, nil +} + +func (f *Store) GetByInvocation(_ context.Context, sessionID, invocationID string) (ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + for _, id := range f.Order { + if run := f.Runs[id]; run.SessionID == sessionID && run.InvocationID == invocationID { + return *run, nil + } + } + return ledger.Run{}, ledger.ErrRunNotFound +} + +func (f *Store) ActiveRun(_ context.Context, sessionID string) (ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + for _, id := range f.Order { + if run := f.Runs[id]; run.SessionID == sessionID && run.State.Active() { + return *run, nil + } + } + return ledger.Run{}, ledger.ErrRunNotFound +} + +func (f *Store) LatestRun(_ context.Context, sessionID string) (ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + for i := len(f.Order) - 1; i >= 0; i-- { + if run := f.Runs[f.Order[i]]; run.SessionID == sessionID { + return *run, nil + } + } + return ledger.Run{}, ledger.ErrRunNotFound +} + +func (f *Store) NextFencingToken(context.Context) (int64, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + if f.TokenErr != nil { + return 0, f.TokenErr + } + f.Token++ + return f.Token, nil +} + +func (f *Store) Claim(_ context.Context, params ledger.ClaimParams) (ledger.Run, bool, error) { + if f.ClaimHook != nil { + f.ClaimHook(params.RunID) + } + f.Mu.Lock() + defer f.Mu.Unlock() + f.Claims++ + if f.ClaimErr != nil { + return ledger.Run{}, false, f.ClaimErr + } + run, ok := f.Runs[params.RunID] + if !ok { + return ledger.Run{}, false, ledger.ErrRunNotFound + } + if run.State != ledger.StateAccepted || run.FencingToken >= params.FencingToken { + return ledger.Run{}, false, nil + } + run.State = ledger.StateRunning + run.OwnerID = params.OwnerID + run.FencingToken = params.FencingToken + run.LiveGeneration = params.LiveGeneration + run.OwnerSince = time.Now() + return *run, true, nil +} + +func (f *Store) SetWaitingDecision(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { + return f.transition(runID, token, ledger.StateWaitingDecision) +} + +func (f *Store) Resume(_ context.Context, runID string, token int64) (ledger.Run, bool, error) { + return f.transition(runID, token, ledger.StateRunning) +} + +func (f *Store) transition(runID string, token int64, state ledger.State) (ledger.Run, bool, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + run, ok := f.Runs[runID] + if !ok || run.FencingToken != token || run.State.Terminal() || run.State == ledger.StateFinishing { + return ledger.Run{}, false, nil + } + run.State = state + return *run, true, nil +} + +func (f *Store) PrepareFinish(_ context.Context, params ledger.PrepareFinishParams) (ledger.Run, bool, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + if f.PrepareErr != nil { + return ledger.Run{}, false, f.PrepareErr + } + run, ok := f.Runs[params.RunID] + if !ok || run.FencingToken != params.FencingToken || run.State.Terminal() || + (run.State == ledger.StateWaitingDecision && !params.AllowWaitingDecision) { + return ledger.Run{}, false, nil + } + if run.State != ledger.StateFinishing { + run.State = ledger.StateFinishing + run.ProposedState = params.State + run.ProposedErrorCode = params.ErrorCode + run.ProposedErrorMessage = params.ErrorMessage + run.FinishProposedAt = time.Now() + } + return *run, true, nil +} + +func (f *Store) Finalize(_ context.Context, params ledger.FinalizeParams) (ledger.Run, bool, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + if f.FinalizeErr != nil { + return ledger.Run{}, false, f.FinalizeErr + } + run, ok := f.Runs[params.RunID] + if !ok || run.FencingToken != params.FencingToken || run.State.Terminal() { + return ledger.Run{}, false, nil + } + state := params.State + errorCode := params.ErrorCode + errorMessage := params.ErrorMessage + if run.State == ledger.StateFinishing { + state = run.ProposedState + errorCode = run.ProposedErrorCode + errorMessage = run.ProposedErrorMessage + } else if state == ledger.StateLost && !run.AbortRequestedAt.IsZero() { + state = ledger.StateAborted + errorCode = "" + errorMessage = "" + } + run.State = state + run.ErrorCode = errorCode + run.ErrorMessage = errorMessage + f.Finalized = append(f.Finalized, params) + return *run, true, nil +} + +func (f *Store) RequestAbort(_ context.Context, runID string) (ledger.Run, bool, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + run, ok := f.Runs[runID] + if !ok || run.State.Terminal() || run.State == ledger.StateFinishing { + return ledger.Run{}, false, nil + } + run.AbortRequestedAt = time.Now() + return *run, true, nil +} + +// StaleGenerationRuns mirrors the adapter's keyset sweep: active rows that were +// claimed by an incarnation other than the current one, ordered so a cursor can +// page through them. +func (f *Store) StaleGenerationRuns(_ context.Context, query ledger.StaleGenerationQuery) ([]ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + var matched []ledger.Run + for _, id := range f.Order { + run := *f.Runs[id] + if !run.State.Active() || run.LiveGeneration == "" || run.LiveGeneration == query.CurrentGeneration { + continue + } + if run.LiveGeneration < query.After.LiveGeneration || + (run.LiveGeneration == query.After.LiveGeneration && run.RunID <= query.After.RunID) { + continue + } + matched = append(matched, run) + } + sort.Slice(matched, func(i, j int) bool { + if matched[i].LiveGeneration != matched[j].LiveGeneration { + return matched[i].LiveGeneration < matched[j].LiveGeneration + } + return matched[i].RunID < matched[j].RunID + }) + if query.Limit > 0 && len(matched) > int(query.Limit) { + matched = matched[:query.Limit] + } + return matched, nil +} + +func (f *Store) OrphanedRuns(_ context.Context, query ledger.OrphanQuery) ([]ledger.Run, error) { + f.Mu.Lock() + defer f.Mu.Unlock() + cutoff := time.Now().Add(-query.MinAge) + var matched []ledger.Run + for _, id := range f.Order { + run := *f.Runs[id] + if run.State != ledger.StateAccepted || run.OwnerID != "" || !run.CreatedAt.Before(cutoff) { + continue + } + matched = append(matched, run) + } + if query.Limit > 0 && len(matched) > int(query.Limit) { + matched = matched[:query.Limit] + } + return matched, nil +} + +// insertOrphan records an admission that committed under a process that died +// before it could claim anything. +func (f *Store) InsertOrphan(runID, sessionID, invocationID, fingerprint string) { + f.Mu.Lock() + defer f.Mu.Unlock() + run := &ledger.Run{ + RunID: runID, + BotID: "bot-runtime", + SessionID: sessionID, + InvocationID: invocationID, + TurnID: runID + "-turn", + TurnPosition: 1, + State: ledger.StateAccepted, + InputFingerprint: fingerprint, + CreatedAt: time.Now().Add(-time.Hour), + } + f.Runs[run.RunID] = run + f.Order = append(f.Order, run.RunID) +} + +// insertClaimed records a run that some owner took and never finished, which is +// what the reaper finds after that owner disappears. +func (f *Store) InsertClaimed(runID, sessionID string, token int64, generation string) { + f.Mu.Lock() + defer f.Mu.Unlock() + run := &ledger.Run{ + RunID: runID, + BotID: "bot-runtime", + SessionID: sessionID, + InvocationID: runID + "-inv", + TurnID: runID + "-turn", + TurnPosition: 1, + State: ledger.StateRunning, + OwnerID: "owner-gone", + FencingToken: token, + LiveGeneration: generation, + OwnerSince: time.Now().Add(-time.Minute), + CreatedAt: time.Now().Add(-time.Minute), + } + f.Runs[run.RunID] = run + f.Order = append(f.Order, run.RunID) +} + +func (f *Store) State(runID string) ledger.State { + f.Mu.Lock() + defer f.Mu.Unlock() + run, ok := f.Runs[runID] + if !ok { + return "" + } + return run.State +} + +func (f *Store) ErrorCode(runID string) string { + f.Mu.Lock() + defer f.Mu.Unlock() + run, ok := f.Runs[runID] + if !ok { + return "" + } + return run.ErrorCode +} + +func (f *Store) SetFinalizeErr(err error) { + f.Mu.Lock() + defer f.Mu.Unlock() + f.FinalizeErr = err +} + +func (f *Store) SetPrepareErr(err error) { + f.Mu.Lock() + defer f.Mu.Unlock() + f.PrepareErr = err +} + +func (f *Store) Counts() (admits, claims int) { + f.Mu.Lock() + defer f.Mu.Unlock() + return f.Admits, f.Claims +} + +func (f *Store) TerminalWrites() []ledger.FinalizeParams { + f.Mu.Lock() + defer f.Mu.Unlock() + return append([]ledger.FinalizeParams(nil), f.Finalized...) +} diff --git a/internal/testutil/sessionruntime/runtime.go b/internal/testutil/sessionruntime/runtime.go new file mode 100644 index 0000000000..2b02cd75fb --- /dev/null +++ b/internal/testutil/sessionruntime/runtime.go @@ -0,0 +1,50 @@ +// Package sessiontest creates admitted runtimes for tests outside the runtime +// package. It uses the public admission path, never a pre-ledger reservation. +package sessiontest + +import ( + "context" + + sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" + "github.com/felinics/memoh/internal/agent/runtime/session/ledger" + "github.com/felinics/memoh/internal/agent/turn" + "github.com/felinics/memoh/internal/testutil/sessionledger" +) + +type deterministicLedger struct{ *sessionledger.Store } + +// Fixture callers name their run through the invocation argument. Production +// IDs still come from Manager.Admit; deterministic test identities keep events +// and assertions readable without bypassing admission or fencing. +func (s deterministicLedger) Admit(ctx context.Context, params ledger.AdmitParams) (ledger.Run, bool, error) { + params.RunID = params.InvocationID + return s.Store.Admit(ctx, params) +} + +type fence struct{} + +func (fence) Activate(context.Context, string, string, int64) error { return nil } + +func New(backend sessionruntime.Backend, opts sessionruntime.Options) *sessionruntime.Manager { + if opts.Ledger == nil { + opts.Ledger = deterministicLedger{sessionledger.New()} + } + if opts.Fence == nil { + opts.Fence = fence{} + } + return sessionruntime.NewManager(backend, opts) +} + +func Start(ctx context.Context, manager *sessionruntime.Manager, botID, sessionID, invocationID string, abortCh chan<- struct{}, cancel context.CancelFunc, injectCh chan<- turn.InjectMessage) (sessionruntime.RunHandle, error) { + admission, err := manager.Admit(ctx, sessionruntime.AdmitInput{ + BotID: botID, SessionID: sessionID, InvocationID: invocationID, + Payload: []byte(`{"text":"runtime fixture"}`), + Execution: sessionruntime.Execution{ + Admission: func(context.Context, sessionruntime.RunHandle) (sessionruntime.RunAdmissionView, error) { + return sessionruntime.RunAdmissionView{}, nil + }, + AbortCh: abortCh, Cancel: cancel, InjectCh: injectCh, + }, + }) + return admission.Handle, err +} diff --git a/mise.toml b/mise.toml index a5bba70488..933f458348 100644 --- a/mise.toml +++ b/mise.toml @@ -202,14 +202,14 @@ go test \ """ [tasks."test:session-runtime:acceptance:cluster:destructive"] -description = "Run the isolated owner-restart decision black-box serially against the cluster topology" +description = "Run isolated owner, decision, steer, and terminal-proposal crash acceptance serially" env = { MEMOH_SESSION_RUNTIME_ACCEPTANCE = "1", MEMOH_SESSION_RUNTIME_ACCEPTANCE_REQUIRED = "1", MEMOH_SESSION_RUNTIME_ACCEPTANCE_MODE = "cluster", MEMOH_SESSION_RUNTIME_ACCEPTANCE_CRASH = "1" } run = """ go test \ -tags=integration \ -count=1 \ -timeout=5m \ - -run '^TestSRDEC001DecisionPersistsRunAndTurnAcrossRestart$' \ + -run '^Test(SRDEC001DecisionPersistsRunAndTurnAcrossRestart|QueueSteerDecisionSurvivesOwnerRestart|SRDUR001AcceptedInputSurvivesOwnerCrashAsLedgerFact|SRDUR002PreparedFinishSurvivesProcessCrash)$' \ ./internal/agent/runtime/session/acceptance """ diff --git a/packages/sdk/src/@pinia/colada.gen.ts b/packages/sdk/src/@pinia/colada.gen.ts index cda7c83f2f..dc96696dd3 100644 --- a/packages/sdk/src/@pinia/colada.gen.ts +++ b/packages/sdk/src/@pinia/colada.gen.ts @@ -4,8 +4,8 @@ import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@p import { serializeQueryKeyValue } from '../client'; import { client } from '../client.gen'; -import { deleteBotsByBotIdAclRulesByRuleId, deleteBotsByBotIdAcpRuntimesByRuntimeId, deleteBotsByBotIdAgentsById, deleteBotsByBotIdAgentsByIdCredential, deleteBotsByBotIdChannelManagersByChannelIdentityId, deleteBotsByBotIdCompactionLogs, deleteBotsByBotIdConnectorsByConnectionId, deleteBotsByBotIdContainer, deleteBotsByBotIdContainerBrowserSessionsBySessionId, deleteBotsByBotIdContainerDisplaySessionsBySessionId, deleteBotsByBotIdContainerSkills, deleteBotsByBotIdEmailBindingsById, deleteBotsByBotIdMcpById, deleteBotsByBotIdMcpByIdOauthToken, deleteBotsByBotIdMemory, deleteBotsByBotIdMemoryById, deleteBotsByBotIdMessages, deleteBotsByBotIdScheduleById, deleteBotsByBotIdScheduleLogs, deleteBotsByBotIdSessionsBySessionId, deleteBotsByBotIdSettings, deleteBotsByBotIdSupermarketPackagesByInstallationId, deleteBotsByBotIdUserAccessByGrantId, deleteBotsByBotIdWorkdirsByWorkdirId, deleteBotsByBotIdWorkspaceTargetsByTargetId, deleteBotsById, deleteBotsByIdChannelByPlatform, deleteEmailProvidersById, deleteEmailProvidersByIdOauthToken, deleteFetchProvidersById, deleteMemoryProvidersById, deleteModelsById, deleteModelsModelByModelId, deleteProvidersById, deleteProvidersByIdOauthToken, deleteSearchProvidersById, deleteUsersById, deleteUsersMeChannelIdentitiesByChannelIdentityId, deleteUsersMeRuntimesById, getAcpProfiles, getBots, getBotsByBotIdAclChannelIdentities, getBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversations, getBotsByBotIdAclChannelTypesByChannelTypeConversations, getBotsByBotIdAclDefaultEffect, getBotsByBotIdAclRules, getBotsByBotIdAcpRuntimesByRuntimeId, getBotsByBotIdAgents, getBotsByBotIdAgentsById, getBotsByBotIdAgentsByIdCredential, getBotsByBotIdAgentsByIdModels, getBotsByBotIdBackupSummary, getBotsByBotIdChannelManagers, getBotsByBotIdCompactionLogs, getBotsByBotIdConnectors, getBotsByBotIdConnectorsByConnectionId, getBotsByBotIdContainer, getBotsByBotIdContainerDisplay, getBotsByBotIdContainerDisplaySessions, getBotsByBotIdContainerFs, getBotsByBotIdContainerFsDownload, getBotsByBotIdContainerFsList, getBotsByBotIdContainerFsRead, getBotsByBotIdContainerMetrics, getBotsByBotIdContainerSkills, getBotsByBotIdContainerSnapshots, getBotsByBotIdContainerTerminal, getBotsByBotIdContainerTerminalWs, getBotsByBotIdEmailBindings, getBotsByBotIdEmailOutbox, getBotsByBotIdEmailOutboxById, getBotsByBotIdHooksEvents, getBotsByBotIdMcp, getBotsByBotIdMcpById, getBotsByBotIdMcpByIdOauthStatus, getBotsByBotIdMcpOpsExport, getBotsByBotIdMemory, getBotsByBotIdMemoryGraph, getBotsByBotIdMemoryStatus, getBotsByBotIdMemoryUsage, getBotsByBotIdMessages, getBotsByBotIdMessagesLocate, getBotsByBotIdSchedule, getBotsByBotIdScheduleById, getBotsByBotIdScheduleByIdLogs, getBotsByBotIdScheduleLogs, getBotsByBotIdSessions, getBotsByBotIdSessionsBySessionId, getBotsByBotIdSessionsBySessionIdAcpRuntime, getBotsByBotIdSessionsBySessionIdContextLifecycle, getBotsByBotIdSessionsBySessionIdStatus, getBotsByBotIdSessionsModelPreferenceSeed, getBotsByBotIdSettings, getBotsByBotIdSkillsCatalog, getBotsByBotIdSupermarketPackages, getBotsByBotIdTokenUsage, getBotsByBotIdTokenUsageRecords, getBotsByBotIdUserAccess, getBotsByBotIdUserAccessCandidates, getBotsByBotIdWebWs, getBotsByBotIdWorkdirs, getBotsByBotIdWorkspaceTargets, getBotsById, getBotsByIdChannelByPlatform, getBotsByIdChecks, getBotsNameAvailability, getBotsUserAccessCandidates, getChannels, getChannelsByPlatform, getConnectorsCatalog, getEmailOauthCallback, getEmailProviders, getEmailProvidersById, getEmailProvidersByIdOauthAuthorize, getEmailProvidersByIdOauthStatus, getEmailProvidersMeta, getFetchProviders, getFetchProvidersById, getFetchProvidersMeta, getMemoryProviders, getMemoryProvidersById, getMemoryProvidersByIdStatus, getMemoryProvidersMeta, getModels, getModelsById, getModelsCount, getModelsModelByModelId, getOauthMcpCallback, getPing, getProviders, getProvidersById, getProvidersByIdModels, getProvidersByIdOauthAuthorize, getProvidersByIdOauthStatus, getProvidersCount, getProvidersNameByName, getProvidersOauthCallback, getProviderTemplates, getProviderTemplatesById, getSearchProviders, getSearchProvidersById, getSearchProvidersMeta, getSpeechModels, getSpeechModelsById, getSpeechModelsByIdCapabilities, getSpeechProviders, getSpeechProvidersById, getSpeechProvidersByIdModels, getSpeechProvidersMeta, getSupermarketArtifactsIconByDigest, getSupermarketPackages, getSupermarketRegistries, getSupermarketRegistriesByRegistryIdCategories, getSupermarketRegistriesByRegistryIdPackages, getSupermarketRegistriesByRegistryIdPackagesByPackageId, getSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevision, getSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillId, getSupermarketSkills, getTranscriptionModels, getTranscriptionModelsById, getTranscriptionModelsByIdCapabilities, getTranscriptionProviders, getTranscriptionProvidersById, getTranscriptionProvidersByIdModels, getTranscriptionProvidersMeta, getUsers, getUsersById, getUsersMe, getUsersMeChannelIdentities, getUsersMeChannelsByPlatform, getUsersMeComputerAccess, getUsersMeRuntimes, getVideoModels, getVideoModelsById, getVideoProviders, getVideoProvidersById, getVideoProvidersByIdModels, getVideoProvidersMeta, getWebhookTunnelStatus, type Options, patchBotsByBotIdAcpRuntimesByRuntimeIdMode, patchBotsByBotIdAcpRuntimesByRuntimeIdModel, patchBotsByBotIdAcpRuntimesByRuntimeIdReasoning, patchBotsByBotIdAgentsById, patchBotsByBotIdConnectorsByConnectionId, patchBotsByBotIdSessionsBySessionId, patchBotsByBotIdSessionsBySessionIdAcpRuntimeMode, patchBotsByBotIdSessionsBySessionIdAcpRuntimeModel, patchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoning, patchBotsByBotIdWorkdirsByWorkdirId, patchBotsByIdChannelByPlatformStatus, postAuthLogin, postAuthRefresh, postBots, postBotsBackupImport, postBotsBackupImportPreview, postBotsByBotIdAclRules, postBotsByBotIdAcpRuntimes, postBotsByBotIdAgents, postBotsByBotIdAgentsByIdCodexLoginDeviceAuthorize, postBotsByBotIdAgentsByIdCodexLoginDeviceCancel, postBotsByBotIdAgentsByIdCodexLoginDevicePoll, postBotsByBotIdBackupExport, postBotsByBotIdChannelManagers, postBotsByBotIdConnectorsApiKey, postBotsByBotIdConnectorsByConnectionIdReauth, postBotsByBotIdConnectorsOauth, postBotsByBotIdContainer, postBotsByBotIdContainerBrowserSessions, postBotsByBotIdContainerBrowserSessionsBySessionIdKeepalive, postBotsByBotIdContainerDataRestore, postBotsByBotIdContainerDisplayWebrtcOffer, postBotsByBotIdContainerFsArchive, postBotsByBotIdContainerFsDelete, postBotsByBotIdContainerFsExtract, postBotsByBotIdContainerFsMkdir, postBotsByBotIdContainerFsRename, postBotsByBotIdContainerFsUpload, postBotsByBotIdContainerFsWrite, postBotsByBotIdContainerSkills, postBotsByBotIdContainerSkillsActions, postBotsByBotIdContainerSnapshots, postBotsByBotIdContainerSnapshotsRollback, postBotsByBotIdContainerStart, postBotsByBotIdContainerStop, postBotsByBotIdEmailBindings, postBotsByBotIdHooksTest, postBotsByBotIdMcp, postBotsByBotIdMcpByIdOauthAuthorize, postBotsByBotIdMcpByIdOauthDiscover, postBotsByBotIdMcpByIdOauthExchange, postBotsByBotIdMcpByIdProbe, postBotsByBotIdMcpOpsBatchDelete, postBotsByBotIdMcpStdio, postBotsByBotIdMcpStdioByConnectionId, postBotsByBotIdMemory, postBotsByBotIdMemoryCompact, postBotsByBotIdMemoryIngest, postBotsByBotIdMemoryRebuild, postBotsByBotIdMemorySearch, postBotsByBotIdQuickActionsExecute, postBotsByBotIdSchedule, postBotsByBotIdSessions, postBotsByBotIdSessionsBySessionIdAcpRuntime, postBotsByBotIdSessionsBySessionIdCompact, postBotsByBotIdSessionsBySessionIdFork, postBotsByBotIdSettings, postBotsByBotIdSupermarketInstallPackage, postBotsByBotIdToolApprovalsByApprovalIdApprove, postBotsByBotIdToolApprovalsByApprovalIdReject, postBotsByBotIdTools, postBotsByBotIdTtsSynthesize, postBotsByBotIdUserAccess, postBotsByBotIdWebMessages, postBotsByBotIdWorkdirs, postBotsByIdChannelByPlatformSend, postBotsByIdChannelByPlatformSendChat, postBotsByIdChannelByPlatformWebhookEndpoint, postEmailMailgunWebhookByConfigId, postEmailProviders, postFetchProviders, postMemoryProviders, postModels, postModelsByIdTest, postProviders, postProvidersByIdImportModels, postProvidersByIdOauthPoll, postProvidersByIdTest, postProvidersFromTemplate, postSearchProviders, postSpeechModelsByIdTest, postSpeechProvidersByIdImportModels, postTranscriptionModelsByIdTest, postTranscriptionProvidersByIdImportModels, postUsers, postUsersMeChannelLinks, postUsersMeRuntimes, postVideoProvidersByIdImportModels, putBotsByBotIdAclDefaultEffect, putBotsByBotIdAclRulesByRuleId, putBotsByBotIdAgentsByIdCredential, putBotsByBotIdContainerMetrics, putBotsByBotIdEmailBindingsById, putBotsByBotIdMcpById, putBotsByBotIdMcpOpsImport, putBotsByBotIdMemoryByMemoryId, putBotsByBotIdScheduleById, putBotsByBotIdSettings, putBotsByBotIdUserAccessByGrantId, putBotsByBotIdWorkspaceTargetsByTargetIdToolApproval, putBotsByBotIdWorkspaceTargetsPrimary, putBotsByBotIdWorkspaceTargetsRemotesByRuntimeId, putBotsById, putBotsByIdChannelByPlatform, putBotsByIdOwner, putEmailProvidersById, putFetchProvidersById, putMemoryProvidersById, putModelsById, putModelsModelByModelId, putProvidersById, putSearchProvidersById, putSpeechModelsById, putTranscriptionModelsById, putUsersById, putUsersMe, putUsersMeChannelsByPlatform, putUsersMePassword, putVideoModelsById } from '../sdk.gen'; -import type { DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdError, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdError, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialError, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdError, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdError, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsError, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdError, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdError, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdError, DeleteBotsByBotIdContainerError, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsError, DeleteBotsByBotIdContainerSkillsResponse, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdError, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdError, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenError, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdError, DeleteBotsByBotIdMemoryByIdResponse, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryError, DeleteBotsByBotIdMemoryResponse, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesError, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdError, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsError, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdError, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponse, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdError, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdError, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdError, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformError, DeleteBotsByIdData, DeleteBotsByIdError, DeleteBotsByIdResponse, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdError, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenError, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdError, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdError, DeleteModelsByIdData, DeleteModelsByIdError, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdError, DeleteProvidersByIdData, DeleteProvidersByIdError, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenError, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdError, DeleteUsersByIdData, DeleteUsersByIdError, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdError, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdError, GetAcpProfilesData, GetAcpProfilesResponse, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsError, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponse, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesError, GetBotsByBotIdAclChannelIdentitiesResponse, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsError, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponse, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectError, GetBotsByBotIdAclDefaultEffectResponse, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesError, GetBotsByBotIdAclRulesResponse, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdError, GetBotsByBotIdAcpRuntimesByRuntimeIdResponse, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialError, GetBotsByBotIdAgentsByIdCredentialResponse, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdError, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsError, GetBotsByBotIdAgentsByIdModelsResponse, GetBotsByBotIdAgentsByIdResponse, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsError, GetBotsByBotIdAgentsResponse, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryError, GetBotsByBotIdBackupSummaryResponse, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersError, GetBotsByBotIdChannelManagersResponse, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsError, GetBotsByBotIdCompactionLogsResponse, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdError, GetBotsByBotIdConnectorsByConnectionIdResponse, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsError, GetBotsByBotIdConnectorsResponse, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayError, GetBotsByBotIdContainerDisplayResponse, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsError, GetBotsByBotIdContainerDisplaySessionsResponse, GetBotsByBotIdContainerError, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadError, GetBotsByBotIdContainerFsError, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListError, GetBotsByBotIdContainerFsListResponse, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadError, GetBotsByBotIdContainerFsReadResponse, GetBotsByBotIdContainerFsResponse, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsError, GetBotsByBotIdContainerMetricsResponse, GetBotsByBotIdContainerResponse, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsError, GetBotsByBotIdContainerSkillsResponse, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsError, GetBotsByBotIdContainerSnapshotsResponse, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalError, GetBotsByBotIdContainerTerminalResponse, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsError, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsError, GetBotsByBotIdEmailBindingsResponse, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdError, GetBotsByBotIdEmailOutboxByIdResponse, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxError, GetBotsByBotIdEmailOutboxResponse, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsError, GetBotsByBotIdHooksEventsResponse, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdError, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusError, GetBotsByBotIdMcpByIdOauthStatusResponse, GetBotsByBotIdMcpByIdResponse, GetBotsByBotIdMcpData, GetBotsByBotIdMcpError, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportError, GetBotsByBotIdMcpOpsExportResponse, GetBotsByBotIdMcpResponse, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryError, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphError, GetBotsByBotIdMemoryGraphResponse, GetBotsByBotIdMemoryResponse, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusError, GetBotsByBotIdMemoryStatusResponse, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageError, GetBotsByBotIdMemoryUsageResponse, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesError, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateError, GetBotsByBotIdMessagesLocateResponse, GetBotsByBotIdMessagesResponse, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdError, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsError, GetBotsByBotIdScheduleByIdLogsResponse, GetBotsByBotIdScheduleByIdResponse, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleError, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsError, GetBotsByBotIdScheduleLogsResponse, GetBotsByBotIdScheduleResponse, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeError, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleError, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponse, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdError, GetBotsByBotIdSessionsBySessionIdResponse, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusError, GetBotsByBotIdSessionsBySessionIdStatusResponse, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsError, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedError, GetBotsByBotIdSessionsModelPreferenceSeedResponse, GetBotsByBotIdSessionsResponse, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsError, GetBotsByBotIdSettingsResponse, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogError, GetBotsByBotIdSkillsCatalogResponse, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesError, GetBotsByBotIdSupermarketPackagesResponse, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageError, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsError, GetBotsByBotIdTokenUsageRecordsResponse, GetBotsByBotIdTokenUsageResponse, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesError, GetBotsByBotIdUserAccessCandidatesResponse, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessError, GetBotsByBotIdUserAccessResponse, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsError, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsError, GetBotsByBotIdWorkdirsResponse, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsError, GetBotsByBotIdWorkspaceTargetsResponse, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformError, GetBotsByIdChannelByPlatformResponse, GetBotsByIdChecksData, GetBotsByIdChecksError, GetBotsByIdChecksResponse, GetBotsByIdData, GetBotsByIdError, GetBotsByIdResponse, GetBotsData, GetBotsError, GetBotsNameAvailabilityData, GetBotsNameAvailabilityError, GetBotsNameAvailabilityResponse, GetBotsResponse, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesError, GetBotsUserAccessCandidatesResponse, GetChannelsByPlatformData, GetChannelsByPlatformError, GetChannelsByPlatformResponse, GetChannelsData, GetChannelsError, GetChannelsResponse, GetConnectorsCatalogData, GetConnectorsCatalogError, GetConnectorsCatalogResponse, GetEmailOauthCallbackData, GetEmailOauthCallbackError, GetEmailOauthCallbackResponse, GetEmailProvidersByIdData, GetEmailProvidersByIdError, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeError, GetEmailProvidersByIdOauthAuthorizeResponse, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusError, GetEmailProvidersByIdOauthStatusResponse, GetEmailProvidersByIdResponse, GetEmailProvidersData, GetEmailProvidersError, GetEmailProvidersMetaData, GetEmailProvidersMetaResponse, GetEmailProvidersResponse, GetFetchProvidersByIdData, GetFetchProvidersByIdError, GetFetchProvidersByIdResponse, GetFetchProvidersData, GetFetchProvidersError, GetFetchProvidersMetaData, GetFetchProvidersMetaResponse, GetFetchProvidersResponse, GetMemoryProvidersByIdData, GetMemoryProvidersByIdError, GetMemoryProvidersByIdResponse, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusError, GetMemoryProvidersByIdStatusResponse, GetMemoryProvidersData, GetMemoryProvidersError, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponse, GetMemoryProvidersResponse, GetModelsByIdData, GetModelsByIdError, GetModelsByIdResponse, GetModelsCountData, GetModelsCountError, GetModelsCountResponse, GetModelsData, GetModelsError, GetModelsModelByModelIdData, GetModelsModelByModelIdError, GetModelsModelByModelIdResponse, GetModelsResponse, GetOauthMcpCallbackData, GetOauthMcpCallbackError, GetOauthMcpCallbackResponse, GetPingData, GetPingResponse, GetProvidersByIdData, GetProvidersByIdError, GetProvidersByIdModelsData, GetProvidersByIdModelsError, GetProvidersByIdModelsResponse, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeError, GetProvidersByIdOauthAuthorizeResponse, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusError, GetProvidersByIdOauthStatusResponse, GetProvidersByIdResponse, GetProvidersCountData, GetProvidersCountError, GetProvidersCountResponse, GetProvidersData, GetProvidersError, GetProvidersNameByNameData, GetProvidersNameByNameError, GetProvidersNameByNameResponse, GetProvidersOauthCallbackData, GetProvidersOauthCallbackError, GetProvidersOauthCallbackResponse, GetProvidersResponse, GetProviderTemplatesByIdData, GetProviderTemplatesByIdError, GetProviderTemplatesByIdResponse, GetProviderTemplatesData, GetProviderTemplatesError, GetProviderTemplatesResponse, GetSearchProvidersByIdData, GetSearchProvidersByIdError, GetSearchProvidersByIdResponse, GetSearchProvidersData, GetSearchProvidersError, GetSearchProvidersMetaData, GetSearchProvidersMetaResponse, GetSearchProvidersResponse, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesError, GetSpeechModelsByIdCapabilitiesResponse, GetSpeechModelsByIdData, GetSpeechModelsByIdError, GetSpeechModelsByIdResponse, GetSpeechModelsData, GetSpeechModelsError, GetSpeechModelsResponse, GetSpeechProvidersByIdData, GetSpeechProvidersByIdError, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsError, GetSpeechProvidersByIdModelsResponse, GetSpeechProvidersByIdResponse, GetSpeechProvidersData, GetSpeechProvidersError, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponse, GetSpeechProvidersResponse, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestError, GetSupermarketPackagesData, GetSupermarketPackagesError, GetSupermarketPackagesResponse, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesError, GetSupermarketRegistriesByRegistryIdCategoriesResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponse, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesError, GetSupermarketRegistriesByRegistryIdPackagesResponse, GetSupermarketRegistriesData, GetSupermarketRegistriesError, GetSupermarketRegistriesResponse, GetSupermarketSkillsData, GetSupermarketSkillsError, GetSupermarketSkillsResponse, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesError, GetTranscriptionModelsByIdCapabilitiesResponse, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdError, GetTranscriptionModelsByIdResponse, GetTranscriptionModelsData, GetTranscriptionModelsError, GetTranscriptionModelsResponse, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdError, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsError, GetTranscriptionProvidersByIdModelsResponse, GetTranscriptionProvidersByIdResponse, GetTranscriptionProvidersData, GetTranscriptionProvidersError, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponse, GetTranscriptionProvidersResponse, GetUsersByIdData, GetUsersByIdError, GetUsersByIdResponse, GetUsersData, GetUsersError, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesError, GetUsersMeChannelIdentitiesResponse, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformError, GetUsersMeChannelsByPlatformResponse, GetUsersMeComputerAccessData, GetUsersMeComputerAccessError, GetUsersMeComputerAccessResponse, GetUsersMeData, GetUsersMeError, GetUsersMeResponse, GetUsersMeRuntimesData, GetUsersMeRuntimesError, GetUsersMeRuntimesResponse, GetUsersResponse, GetVideoModelsByIdData, GetVideoModelsByIdError, GetVideoModelsByIdResponse, GetVideoModelsData, GetVideoModelsError, GetVideoModelsResponse, GetVideoProvidersByIdData, GetVideoProvidersByIdError, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsError, GetVideoProvidersByIdModelsResponse, GetVideoProvidersByIdResponse, GetVideoProvidersData, GetVideoProvidersError, GetVideoProvidersMetaData, GetVideoProvidersMetaResponse, GetVideoProvidersResponse, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningError, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponse, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdError, PatchBotsByBotIdAgentsByIdResponse, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponse, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdError, PatchBotsByBotIdSessionsBySessionIdResponse, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdError, PatchBotsByBotIdWorkdirsByWorkdirIdResponse, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusError, PatchBotsByIdChannelByPlatformStatusResponse, PostAuthLoginData, PostAuthLoginError, PostAuthLoginResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostBotsBackupImportData, PostBotsBackupImportError, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewError, PostBotsBackupImportPreviewResponse, PostBotsBackupImportResponse, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesError, PostBotsByBotIdAclRulesResponse, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesError, PostBotsByBotIdAcpRuntimesResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeError, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponse, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsError, PostBotsByBotIdAgentsResponse, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportError, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersError, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyError, PostBotsByBotIdConnectorsApiKeyResponse, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthError, PostBotsByBotIdConnectorsByConnectionIdReauthResponse, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthError, PostBotsByBotIdConnectorsOauthResponse, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveError, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponse, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsError, PostBotsByBotIdContainerBrowserSessionsResponse, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreError, PostBotsByBotIdContainerDataRestoreResponse, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferError, PostBotsByBotIdContainerDisplayWebrtcOfferResponse, PostBotsByBotIdContainerError, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveError, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteError, PostBotsByBotIdContainerFsDeleteResponse, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractError, PostBotsByBotIdContainerFsExtractResponse, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirError, PostBotsByBotIdContainerFsMkdirResponse, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameError, PostBotsByBotIdContainerFsRenameResponse, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadError, PostBotsByBotIdContainerFsUploadResponse, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteError, PostBotsByBotIdContainerFsWriteResponse, PostBotsByBotIdContainerResponse, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsError, PostBotsByBotIdContainerSkillsActionsResponse, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsError, PostBotsByBotIdContainerSkillsResponse, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsError, PostBotsByBotIdContainerSnapshotsResponse, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackError, PostBotsByBotIdContainerSnapshotsRollbackResponse, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartError, PostBotsByBotIdContainerStartResponse, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopError, PostBotsByBotIdContainerStopResponse, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsError, PostBotsByBotIdEmailBindingsResponse, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestError, PostBotsByBotIdHooksTestResponse, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeError, PostBotsByBotIdMcpByIdOauthAuthorizeResponse, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverError, PostBotsByBotIdMcpByIdOauthDiscoverResponse, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeError, PostBotsByBotIdMcpByIdOauthExchangeResponse, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeError, PostBotsByBotIdMcpByIdProbeResponse, PostBotsByBotIdMcpData, PostBotsByBotIdMcpError, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteError, PostBotsByBotIdMcpResponse, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdError, PostBotsByBotIdMcpStdioByConnectionIdResponse, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioError, PostBotsByBotIdMcpStdioResponse, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactError, PostBotsByBotIdMemoryCompactResponse, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryError, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestError, PostBotsByBotIdMemoryIngestResponse, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildError, PostBotsByBotIdMemoryRebuildResponse, PostBotsByBotIdMemoryResponse, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchError, PostBotsByBotIdMemorySearchResponse, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteError, PostBotsByBotIdQuickActionsExecuteResponse, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleError, PostBotsByBotIdScheduleResponse, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeError, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactError, PostBotsByBotIdSessionsBySessionIdCompactResponse, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkError, PostBotsByBotIdSessionsBySessionIdForkResponse, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsError, PostBotsByBotIdSessionsResponse, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsError, PostBotsByBotIdSettingsResponse, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageError, PostBotsByBotIdSupermarketInstallPackageResponse, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveError, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponse, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectError, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponse, PostBotsByBotIdToolsData, PostBotsByBotIdToolsError, PostBotsByBotIdToolsResponse, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeError, PostBotsByBotIdTtsSynthesizeResponse, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessError, PostBotsByBotIdUserAccessResponse, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesError, PostBotsByBotIdWebMessagesResponse, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsError, PostBotsByBotIdWorkdirsResponse, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatError, PostBotsByIdChannelByPlatformSendChatResponse, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendError, PostBotsByIdChannelByPlatformSendResponse, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointError, PostBotsByIdChannelByPlatformWebhookEndpointResponse, PostBotsData, PostBotsError, PostBotsResponse, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdError, PostEmailMailgunWebhookByConfigIdResponse, PostEmailProvidersData, PostEmailProvidersError, PostEmailProvidersResponse, PostFetchProvidersData, PostFetchProvidersError, PostFetchProvidersResponse, PostMemoryProvidersData, PostMemoryProvidersError, PostMemoryProvidersResponse, PostModelsByIdTestData, PostModelsByIdTestError, PostModelsByIdTestResponse, PostModelsData, PostModelsError, PostModelsResponse, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsError, PostProvidersByIdImportModelsResponse, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollError, PostProvidersByIdOauthPollResponse, PostProvidersByIdTestData, PostProvidersByIdTestError, PostProvidersByIdTestResponse, PostProvidersData, PostProvidersError, PostProvidersFromTemplateData, PostProvidersFromTemplateError, PostProvidersFromTemplateResponse, PostProvidersResponse, PostSearchProvidersData, PostSearchProvidersError, PostSearchProvidersResponse, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestError, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsError, PostSpeechProvidersByIdImportModelsResponse, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestError, PostTranscriptionModelsByIdTestResponse, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsError, PostTranscriptionProvidersByIdImportModelsResponse, PostUsersData, PostUsersError, PostUsersMeChannelLinksData, PostUsersMeChannelLinksError, PostUsersMeChannelLinksResponse, PostUsersMeRuntimesData, PostUsersMeRuntimesError, PostUsersMeRuntimesResponse, PostUsersResponse, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsError, PostVideoProvidersByIdImportModelsResponse, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectError, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdError, PutBotsByBotIdAclRulesByRuleIdResponse, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialError, PutBotsByBotIdAgentsByIdCredentialResponse, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsError, PutBotsByBotIdContainerMetricsResponse, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdError, PutBotsByBotIdEmailBindingsByIdResponse, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdError, PutBotsByBotIdMcpByIdResponse, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportError, PutBotsByBotIdMcpOpsImportResponse, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdError, PutBotsByBotIdMemoryByMemoryIdResponse, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdError, PutBotsByBotIdScheduleByIdResponse, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsError, PutBotsByBotIdSettingsResponse, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdError, PutBotsByBotIdUserAccessByGrantIdResponse, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalError, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponse, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformError, PutBotsByIdChannelByPlatformResponse, PutBotsByIdData, PutBotsByIdError, PutBotsByIdOwnerData, PutBotsByIdOwnerError, PutBotsByIdOwnerResponse, PutBotsByIdResponse, PutEmailProvidersByIdData, PutEmailProvidersByIdError, PutEmailProvidersByIdResponse, PutFetchProvidersByIdData, PutFetchProvidersByIdError, PutFetchProvidersByIdResponse, PutMemoryProvidersByIdData, PutMemoryProvidersByIdError, PutMemoryProvidersByIdResponse, PutModelsByIdData, PutModelsByIdError, PutModelsByIdResponse, PutModelsModelByModelIdData, PutModelsModelByModelIdError, PutModelsModelByModelIdResponse, PutProvidersByIdData, PutProvidersByIdError, PutProvidersByIdResponse, PutSearchProvidersByIdData, PutSearchProvidersByIdError, PutSearchProvidersByIdResponse, PutSpeechModelsByIdData, PutSpeechModelsByIdError, PutSpeechModelsByIdResponse, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdError, PutTranscriptionModelsByIdResponse, PutUsersByIdData, PutUsersByIdError, PutUsersByIdResponse, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformError, PutUsersMeChannelsByPlatformResponse, PutUsersMeData, PutUsersMeError, PutUsersMePasswordData, PutUsersMePasswordError, PutUsersMeResponse, PutVideoModelsByIdData, PutVideoModelsByIdError, PutVideoModelsByIdResponse } from '../types.gen'; +import { deleteBotsByBotIdAclRulesByRuleId, deleteBotsByBotIdAcpRuntimesByRuntimeId, deleteBotsByBotIdAgentsById, deleteBotsByBotIdAgentsByIdCredential, deleteBotsByBotIdChannelManagersByChannelIdentityId, deleteBotsByBotIdCompactionLogs, deleteBotsByBotIdConnectorsByConnectionId, deleteBotsByBotIdContainer, deleteBotsByBotIdContainerBrowserSessionsBySessionId, deleteBotsByBotIdContainerDisplaySessionsBySessionId, deleteBotsByBotIdContainerSkills, deleteBotsByBotIdEmailBindingsById, deleteBotsByBotIdMcpById, deleteBotsByBotIdMcpByIdOauthToken, deleteBotsByBotIdMemory, deleteBotsByBotIdMemoryById, deleteBotsByBotIdMessages, deleteBotsByBotIdScheduleById, deleteBotsByBotIdScheduleLogs, deleteBotsByBotIdSessionsBySessionId, deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId, deleteBotsByBotIdSettings, deleteBotsByBotIdSupermarketPackagesByInstallationId, deleteBotsByBotIdUserAccessByGrantId, deleteBotsByBotIdWorkdirsByWorkdirId, deleteBotsByBotIdWorkspaceTargetsByTargetId, deleteBotsById, deleteBotsByIdChannelByPlatform, deleteEmailProvidersById, deleteEmailProvidersByIdOauthToken, deleteFetchProvidersById, deleteMemoryProvidersById, deleteModelsById, deleteModelsModelByModelId, deleteProvidersById, deleteProvidersByIdOauthToken, deleteSearchProvidersById, deleteUsersById, deleteUsersMeChannelIdentitiesByChannelIdentityId, deleteUsersMeRuntimesById, getAcpProfiles, getBots, getBotsByBotIdAclChannelIdentities, getBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversations, getBotsByBotIdAclChannelTypesByChannelTypeConversations, getBotsByBotIdAclDefaultEffect, getBotsByBotIdAclRules, getBotsByBotIdAcpRuntimesByRuntimeId, getBotsByBotIdAgents, getBotsByBotIdAgentsById, getBotsByBotIdAgentsByIdCredential, getBotsByBotIdAgentsByIdModels, getBotsByBotIdBackupSummary, getBotsByBotIdChannelManagers, getBotsByBotIdCompactionLogs, getBotsByBotIdConnectors, getBotsByBotIdConnectorsByConnectionId, getBotsByBotIdContainer, getBotsByBotIdContainerDisplay, getBotsByBotIdContainerDisplaySessions, getBotsByBotIdContainerFs, getBotsByBotIdContainerFsDownload, getBotsByBotIdContainerFsList, getBotsByBotIdContainerFsRead, getBotsByBotIdContainerMetrics, getBotsByBotIdContainerSkills, getBotsByBotIdContainerSnapshots, getBotsByBotIdContainerTerminal, getBotsByBotIdContainerTerminalWs, getBotsByBotIdEmailBindings, getBotsByBotIdEmailOutbox, getBotsByBotIdEmailOutboxById, getBotsByBotIdHooksEvents, getBotsByBotIdMcp, getBotsByBotIdMcpById, getBotsByBotIdMcpByIdOauthStatus, getBotsByBotIdMcpOpsExport, getBotsByBotIdMemory, getBotsByBotIdMemoryGraph, getBotsByBotIdMemoryStatus, getBotsByBotIdMemoryUsage, getBotsByBotIdMessages, getBotsByBotIdMessagesLocate, getBotsByBotIdSchedule, getBotsByBotIdScheduleById, getBotsByBotIdScheduleByIdLogs, getBotsByBotIdScheduleLogs, getBotsByBotIdSessions, getBotsByBotIdSessionsBySessionId, getBotsByBotIdSessionsBySessionIdAcpRuntime, getBotsByBotIdSessionsBySessionIdContextLifecycle, getBotsByBotIdSessionsBySessionIdFollowUpQueue, getBotsByBotIdSessionsBySessionIdQueue, getBotsByBotIdSessionsBySessionIdStatus, getBotsByBotIdSessionsBySessionIdSteerQueue, getBotsByBotIdSessionsModelPreferenceSeed, getBotsByBotIdSettings, getBotsByBotIdSkillsCatalog, getBotsByBotIdSupermarketPackages, getBotsByBotIdTokenUsage, getBotsByBotIdTokenUsageRecords, getBotsByBotIdUserAccess, getBotsByBotIdUserAccessCandidates, getBotsByBotIdWebWs, getBotsByBotIdWorkdirs, getBotsByBotIdWorkspaceTargets, getBotsById, getBotsByIdChannelByPlatform, getBotsByIdChecks, getBotsNameAvailability, getBotsUserAccessCandidates, getChannels, getChannelsByPlatform, getConnectorsCatalog, getEmailOauthCallback, getEmailProviders, getEmailProvidersById, getEmailProvidersByIdOauthAuthorize, getEmailProvidersByIdOauthStatus, getEmailProvidersMeta, getFetchProviders, getFetchProvidersById, getFetchProvidersMeta, getMemoryProviders, getMemoryProvidersById, getMemoryProvidersByIdStatus, getMemoryProvidersMeta, getModels, getModelsById, getModelsCount, getModelsModelByModelId, getOauthMcpCallback, getPing, getProviders, getProvidersById, getProvidersByIdModels, getProvidersByIdOauthAuthorize, getProvidersByIdOauthStatus, getProvidersCount, getProvidersNameByName, getProvidersOauthCallback, getProviderTemplates, getProviderTemplatesById, getSearchProviders, getSearchProvidersById, getSearchProvidersMeta, getSpeechModels, getSpeechModelsById, getSpeechModelsByIdCapabilities, getSpeechProviders, getSpeechProvidersById, getSpeechProvidersByIdModels, getSpeechProvidersMeta, getSupermarketArtifactsIconByDigest, getSupermarketPackages, getSupermarketRegistries, getSupermarketRegistriesByRegistryIdCategories, getSupermarketRegistriesByRegistryIdPackages, getSupermarketRegistriesByRegistryIdPackagesByPackageId, getSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevision, getSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillId, getSupermarketSkills, getTranscriptionModels, getTranscriptionModelsById, getTranscriptionModelsByIdCapabilities, getTranscriptionProviders, getTranscriptionProvidersById, getTranscriptionProvidersByIdModels, getTranscriptionProvidersMeta, getUsers, getUsersById, getUsersMe, getUsersMeChannelIdentities, getUsersMeChannelsByPlatform, getUsersMeComputerAccess, getUsersMeRuntimes, getVideoModels, getVideoModelsById, getVideoProviders, getVideoProvidersById, getVideoProvidersByIdModels, getVideoProvidersMeta, getWebhookTunnelStatus, type Options, patchBotsByBotIdAcpRuntimesByRuntimeIdMode, patchBotsByBotIdAcpRuntimesByRuntimeIdModel, patchBotsByBotIdAcpRuntimesByRuntimeIdReasoning, patchBotsByBotIdAgentsById, patchBotsByBotIdConnectorsByConnectionId, patchBotsByBotIdSessionsBySessionId, patchBotsByBotIdSessionsBySessionIdAcpRuntimeMode, patchBotsByBotIdSessionsBySessionIdAcpRuntimeModel, patchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoning, patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId, patchBotsByBotIdWorkdirsByWorkdirId, patchBotsByIdChannelByPlatformStatus, postAuthLogin, postAuthRefresh, postBots, postBotsBackupImport, postBotsBackupImportPreview, postBotsByBotIdAclRules, postBotsByBotIdAcpRuntimes, postBotsByBotIdAgents, postBotsByBotIdAgentsByIdCodexLoginDeviceAuthorize, postBotsByBotIdAgentsByIdCodexLoginDeviceCancel, postBotsByBotIdAgentsByIdCodexLoginDevicePoll, postBotsByBotIdBackupExport, postBotsByBotIdChannelManagers, postBotsByBotIdConnectorsApiKey, postBotsByBotIdConnectorsByConnectionIdReauth, postBotsByBotIdConnectorsOauth, postBotsByBotIdContainer, postBotsByBotIdContainerBrowserSessions, postBotsByBotIdContainerBrowserSessionsBySessionIdKeepalive, postBotsByBotIdContainerDataRestore, postBotsByBotIdContainerDisplayWebrtcOffer, postBotsByBotIdContainerFsArchive, postBotsByBotIdContainerFsDelete, postBotsByBotIdContainerFsExtract, postBotsByBotIdContainerFsMkdir, postBotsByBotIdContainerFsRename, postBotsByBotIdContainerFsUpload, postBotsByBotIdContainerFsWrite, postBotsByBotIdContainerSkills, postBotsByBotIdContainerSkillsActions, postBotsByBotIdContainerSnapshots, postBotsByBotIdContainerSnapshotsRollback, postBotsByBotIdContainerStart, postBotsByBotIdContainerStop, postBotsByBotIdEmailBindings, postBotsByBotIdHooksTest, postBotsByBotIdMcp, postBotsByBotIdMcpByIdOauthAuthorize, postBotsByBotIdMcpByIdOauthDiscover, postBotsByBotIdMcpByIdOauthExchange, postBotsByBotIdMcpByIdProbe, postBotsByBotIdMcpOpsBatchDelete, postBotsByBotIdMcpStdio, postBotsByBotIdMcpStdioByConnectionId, postBotsByBotIdMemory, postBotsByBotIdMemoryCompact, postBotsByBotIdMemoryIngest, postBotsByBotIdMemoryRebuild, postBotsByBotIdMemorySearch, postBotsByBotIdQuickActionsExecute, postBotsByBotIdSchedule, postBotsByBotIdSessions, postBotsByBotIdSessionsBySessionIdAcpRuntime, postBotsByBotIdSessionsBySessionIdCompact, postBotsByBotIdSessionsBySessionIdFollowUpQueue, postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer, postBotsByBotIdSessionsBySessionIdFork, postBotsByBotIdSessionsBySessionIdSteerQueue, postBotsByBotIdSettings, postBotsByBotIdSupermarketInstallPackage, postBotsByBotIdToolApprovalsByApprovalIdApprove, postBotsByBotIdToolApprovalsByApprovalIdReject, postBotsByBotIdTools, postBotsByBotIdTtsSynthesize, postBotsByBotIdUserAccess, postBotsByBotIdWebMessages, postBotsByBotIdWorkdirs, postBotsByIdChannelByPlatformSend, postBotsByIdChannelByPlatformSendChat, postBotsByIdChannelByPlatformWebhookEndpoint, postEmailMailgunWebhookByConfigId, postEmailProviders, postFetchProviders, postMemoryProviders, postModels, postModelsByIdTest, postProviders, postProvidersByIdImportModels, postProvidersByIdOauthPoll, postProvidersByIdTest, postProvidersFromTemplate, postSearchProviders, postSpeechModelsByIdTest, postSpeechProvidersByIdImportModels, postTranscriptionModelsByIdTest, postTranscriptionProvidersByIdImportModels, postUsers, postUsersMeChannelLinks, postUsersMeRuntimes, postVideoProvidersByIdImportModels, putBotsByBotIdAclDefaultEffect, putBotsByBotIdAclRulesByRuleId, putBotsByBotIdAgentsByIdCredential, putBotsByBotIdContainerMetrics, putBotsByBotIdEmailBindingsById, putBotsByBotIdMcpById, putBotsByBotIdMcpOpsImport, putBotsByBotIdMemoryByMemoryId, putBotsByBotIdScheduleById, putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder, putBotsByBotIdSessionsBySessionIdSteerQueueReorder, putBotsByBotIdSettings, putBotsByBotIdUserAccessByGrantId, putBotsByBotIdWorkspaceTargetsByTargetIdToolApproval, putBotsByBotIdWorkspaceTargetsPrimary, putBotsByBotIdWorkspaceTargetsRemotesByRuntimeId, putBotsById, putBotsByIdChannelByPlatform, putBotsByIdOwner, putEmailProvidersById, putFetchProvidersById, putMemoryProvidersById, putModelsById, putModelsModelByModelId, putProvidersById, putSearchProvidersById, putSpeechModelsById, putTranscriptionModelsById, putUsersById, putUsersMe, putUsersMeChannelsByPlatform, putUsersMePassword, putVideoModelsById } from '../sdk.gen'; +import type { DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdError, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdError, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialError, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdError, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdError, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsError, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdError, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdError, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdError, DeleteBotsByBotIdContainerError, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsError, DeleteBotsByBotIdContainerSkillsResponse, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdError, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdError, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenError, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdError, DeleteBotsByBotIdMemoryByIdResponse, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryError, DeleteBotsByBotIdMemoryResponse, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesError, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdError, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsError, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdError, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponse, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdError, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdError, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdError, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformError, DeleteBotsByIdData, DeleteBotsByIdError, DeleteBotsByIdResponse, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdError, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenError, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdError, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdError, DeleteModelsByIdData, DeleteModelsByIdError, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdError, DeleteProvidersByIdData, DeleteProvidersByIdError, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenError, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdError, DeleteUsersByIdData, DeleteUsersByIdError, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdError, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdError, GetAcpProfilesData, GetAcpProfilesResponse, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsError, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponse, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesError, GetBotsByBotIdAclChannelIdentitiesResponse, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsError, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponse, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectError, GetBotsByBotIdAclDefaultEffectResponse, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesError, GetBotsByBotIdAclRulesResponse, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdError, GetBotsByBotIdAcpRuntimesByRuntimeIdResponse, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialError, GetBotsByBotIdAgentsByIdCredentialResponse, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdError, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsError, GetBotsByBotIdAgentsByIdModelsResponse, GetBotsByBotIdAgentsByIdResponse, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsError, GetBotsByBotIdAgentsResponse, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryError, GetBotsByBotIdBackupSummaryResponse, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersError, GetBotsByBotIdChannelManagersResponse, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsError, GetBotsByBotIdCompactionLogsResponse, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdError, GetBotsByBotIdConnectorsByConnectionIdResponse, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsError, GetBotsByBotIdConnectorsResponse, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayError, GetBotsByBotIdContainerDisplayResponse, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsError, GetBotsByBotIdContainerDisplaySessionsResponse, GetBotsByBotIdContainerError, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadError, GetBotsByBotIdContainerFsError, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListError, GetBotsByBotIdContainerFsListResponse, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadError, GetBotsByBotIdContainerFsReadResponse, GetBotsByBotIdContainerFsResponse, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsError, GetBotsByBotIdContainerMetricsResponse, GetBotsByBotIdContainerResponse, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsError, GetBotsByBotIdContainerSkillsResponse, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsError, GetBotsByBotIdContainerSnapshotsResponse, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalError, GetBotsByBotIdContainerTerminalResponse, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsError, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsError, GetBotsByBotIdEmailBindingsResponse, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdError, GetBotsByBotIdEmailOutboxByIdResponse, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxError, GetBotsByBotIdEmailOutboxResponse, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsError, GetBotsByBotIdHooksEventsResponse, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdError, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusError, GetBotsByBotIdMcpByIdOauthStatusResponse, GetBotsByBotIdMcpByIdResponse, GetBotsByBotIdMcpData, GetBotsByBotIdMcpError, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportError, GetBotsByBotIdMcpOpsExportResponse, GetBotsByBotIdMcpResponse, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryError, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphError, GetBotsByBotIdMemoryGraphResponse, GetBotsByBotIdMemoryResponse, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusError, GetBotsByBotIdMemoryStatusResponse, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageError, GetBotsByBotIdMemoryUsageResponse, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesError, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateError, GetBotsByBotIdMessagesLocateResponse, GetBotsByBotIdMessagesResponse, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdError, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsError, GetBotsByBotIdScheduleByIdLogsResponse, GetBotsByBotIdScheduleByIdResponse, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleError, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsError, GetBotsByBotIdScheduleLogsResponse, GetBotsByBotIdScheduleResponse, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeError, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleError, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponse, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdError, GetBotsByBotIdSessionsBySessionIdFollowUpQueueData, GetBotsByBotIdSessionsBySessionIdFollowUpQueueError, GetBotsByBotIdSessionsBySessionIdFollowUpQueueResponse, GetBotsByBotIdSessionsBySessionIdQueueData, GetBotsByBotIdSessionsBySessionIdQueueError, GetBotsByBotIdSessionsBySessionIdQueueResponse, GetBotsByBotIdSessionsBySessionIdResponse, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusError, GetBotsByBotIdSessionsBySessionIdStatusResponse, GetBotsByBotIdSessionsBySessionIdSteerQueueData, GetBotsByBotIdSessionsBySessionIdSteerQueueError, GetBotsByBotIdSessionsBySessionIdSteerQueueResponse, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsError, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedError, GetBotsByBotIdSessionsModelPreferenceSeedResponse, GetBotsByBotIdSessionsResponse, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsError, GetBotsByBotIdSettingsResponse, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogError, GetBotsByBotIdSkillsCatalogResponse, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesError, GetBotsByBotIdSupermarketPackagesResponse, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageError, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsError, GetBotsByBotIdTokenUsageRecordsResponse, GetBotsByBotIdTokenUsageResponse, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesError, GetBotsByBotIdUserAccessCandidatesResponse, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessError, GetBotsByBotIdUserAccessResponse, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsError, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsError, GetBotsByBotIdWorkdirsResponse, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsError, GetBotsByBotIdWorkspaceTargetsResponse, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformError, GetBotsByIdChannelByPlatformResponse, GetBotsByIdChecksData, GetBotsByIdChecksError, GetBotsByIdChecksResponse, GetBotsByIdData, GetBotsByIdError, GetBotsByIdResponse, GetBotsData, GetBotsError, GetBotsNameAvailabilityData, GetBotsNameAvailabilityError, GetBotsNameAvailabilityResponse, GetBotsResponse, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesError, GetBotsUserAccessCandidatesResponse, GetChannelsByPlatformData, GetChannelsByPlatformError, GetChannelsByPlatformResponse, GetChannelsData, GetChannelsError, GetChannelsResponse, GetConnectorsCatalogData, GetConnectorsCatalogError, GetConnectorsCatalogResponse, GetEmailOauthCallbackData, GetEmailOauthCallbackError, GetEmailOauthCallbackResponse, GetEmailProvidersByIdData, GetEmailProvidersByIdError, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeError, GetEmailProvidersByIdOauthAuthorizeResponse, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusError, GetEmailProvidersByIdOauthStatusResponse, GetEmailProvidersByIdResponse, GetEmailProvidersData, GetEmailProvidersError, GetEmailProvidersMetaData, GetEmailProvidersMetaResponse, GetEmailProvidersResponse, GetFetchProvidersByIdData, GetFetchProvidersByIdError, GetFetchProvidersByIdResponse, GetFetchProvidersData, GetFetchProvidersError, GetFetchProvidersMetaData, GetFetchProvidersMetaResponse, GetFetchProvidersResponse, GetMemoryProvidersByIdData, GetMemoryProvidersByIdError, GetMemoryProvidersByIdResponse, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusError, GetMemoryProvidersByIdStatusResponse, GetMemoryProvidersData, GetMemoryProvidersError, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponse, GetMemoryProvidersResponse, GetModelsByIdData, GetModelsByIdError, GetModelsByIdResponse, GetModelsCountData, GetModelsCountError, GetModelsCountResponse, GetModelsData, GetModelsError, GetModelsModelByModelIdData, GetModelsModelByModelIdError, GetModelsModelByModelIdResponse, GetModelsResponse, GetOauthMcpCallbackData, GetOauthMcpCallbackError, GetOauthMcpCallbackResponse, GetPingData, GetPingResponse, GetProvidersByIdData, GetProvidersByIdError, GetProvidersByIdModelsData, GetProvidersByIdModelsError, GetProvidersByIdModelsResponse, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeError, GetProvidersByIdOauthAuthorizeResponse, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusError, GetProvidersByIdOauthStatusResponse, GetProvidersByIdResponse, GetProvidersCountData, GetProvidersCountError, GetProvidersCountResponse, GetProvidersData, GetProvidersError, GetProvidersNameByNameData, GetProvidersNameByNameError, GetProvidersNameByNameResponse, GetProvidersOauthCallbackData, GetProvidersOauthCallbackError, GetProvidersOauthCallbackResponse, GetProvidersResponse, GetProviderTemplatesByIdData, GetProviderTemplatesByIdError, GetProviderTemplatesByIdResponse, GetProviderTemplatesData, GetProviderTemplatesError, GetProviderTemplatesResponse, GetSearchProvidersByIdData, GetSearchProvidersByIdError, GetSearchProvidersByIdResponse, GetSearchProvidersData, GetSearchProvidersError, GetSearchProvidersMetaData, GetSearchProvidersMetaResponse, GetSearchProvidersResponse, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesError, GetSpeechModelsByIdCapabilitiesResponse, GetSpeechModelsByIdData, GetSpeechModelsByIdError, GetSpeechModelsByIdResponse, GetSpeechModelsData, GetSpeechModelsError, GetSpeechModelsResponse, GetSpeechProvidersByIdData, GetSpeechProvidersByIdError, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsError, GetSpeechProvidersByIdModelsResponse, GetSpeechProvidersByIdResponse, GetSpeechProvidersData, GetSpeechProvidersError, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponse, GetSpeechProvidersResponse, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestError, GetSupermarketPackagesData, GetSupermarketPackagesError, GetSupermarketPackagesResponse, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesError, GetSupermarketRegistriesByRegistryIdCategoriesResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponse, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesError, GetSupermarketRegistriesByRegistryIdPackagesResponse, GetSupermarketRegistriesData, GetSupermarketRegistriesError, GetSupermarketRegistriesResponse, GetSupermarketSkillsData, GetSupermarketSkillsError, GetSupermarketSkillsResponse, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesError, GetTranscriptionModelsByIdCapabilitiesResponse, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdError, GetTranscriptionModelsByIdResponse, GetTranscriptionModelsData, GetTranscriptionModelsError, GetTranscriptionModelsResponse, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdError, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsError, GetTranscriptionProvidersByIdModelsResponse, GetTranscriptionProvidersByIdResponse, GetTranscriptionProvidersData, GetTranscriptionProvidersError, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponse, GetTranscriptionProvidersResponse, GetUsersByIdData, GetUsersByIdError, GetUsersByIdResponse, GetUsersData, GetUsersError, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesError, GetUsersMeChannelIdentitiesResponse, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformError, GetUsersMeChannelsByPlatformResponse, GetUsersMeComputerAccessData, GetUsersMeComputerAccessError, GetUsersMeComputerAccessResponse, GetUsersMeData, GetUsersMeError, GetUsersMeResponse, GetUsersMeRuntimesData, GetUsersMeRuntimesError, GetUsersMeRuntimesResponse, GetUsersResponse, GetVideoModelsByIdData, GetVideoModelsByIdError, GetVideoModelsByIdResponse, GetVideoModelsData, GetVideoModelsError, GetVideoModelsResponse, GetVideoProvidersByIdData, GetVideoProvidersByIdError, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsError, GetVideoProvidersByIdModelsResponse, GetVideoProvidersByIdResponse, GetVideoProvidersData, GetVideoProvidersError, GetVideoProvidersMetaData, GetVideoProvidersMetaResponse, GetVideoProvidersResponse, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningError, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponse, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdError, PatchBotsByBotIdAgentsByIdResponse, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponse, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdError, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponse, PatchBotsByBotIdSessionsBySessionIdResponse, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponse, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdError, PatchBotsByBotIdWorkdirsByWorkdirIdResponse, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusError, PatchBotsByIdChannelByPlatformStatusResponse, PostAuthLoginData, PostAuthLoginError, PostAuthLoginResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostBotsBackupImportData, PostBotsBackupImportError, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewError, PostBotsBackupImportPreviewResponse, PostBotsBackupImportResponse, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesError, PostBotsByBotIdAclRulesResponse, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesError, PostBotsByBotIdAcpRuntimesResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeError, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponse, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsError, PostBotsByBotIdAgentsResponse, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportError, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersError, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyError, PostBotsByBotIdConnectorsApiKeyResponse, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthError, PostBotsByBotIdConnectorsByConnectionIdReauthResponse, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthError, PostBotsByBotIdConnectorsOauthResponse, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveError, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponse, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsError, PostBotsByBotIdContainerBrowserSessionsResponse, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreError, PostBotsByBotIdContainerDataRestoreResponse, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferError, PostBotsByBotIdContainerDisplayWebrtcOfferResponse, PostBotsByBotIdContainerError, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveError, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteError, PostBotsByBotIdContainerFsDeleteResponse, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractError, PostBotsByBotIdContainerFsExtractResponse, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirError, PostBotsByBotIdContainerFsMkdirResponse, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameError, PostBotsByBotIdContainerFsRenameResponse, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadError, PostBotsByBotIdContainerFsUploadResponse, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteError, PostBotsByBotIdContainerFsWriteResponse, PostBotsByBotIdContainerResponse, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsError, PostBotsByBotIdContainerSkillsActionsResponse, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsError, PostBotsByBotIdContainerSkillsResponse, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsError, PostBotsByBotIdContainerSnapshotsResponse, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackError, PostBotsByBotIdContainerSnapshotsRollbackResponse, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartError, PostBotsByBotIdContainerStartResponse, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopError, PostBotsByBotIdContainerStopResponse, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsError, PostBotsByBotIdEmailBindingsResponse, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestError, PostBotsByBotIdHooksTestResponse, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeError, PostBotsByBotIdMcpByIdOauthAuthorizeResponse, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverError, PostBotsByBotIdMcpByIdOauthDiscoverResponse, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeError, PostBotsByBotIdMcpByIdOauthExchangeResponse, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeError, PostBotsByBotIdMcpByIdProbeResponse, PostBotsByBotIdMcpData, PostBotsByBotIdMcpError, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteError, PostBotsByBotIdMcpResponse, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdError, PostBotsByBotIdMcpStdioByConnectionIdResponse, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioError, PostBotsByBotIdMcpStdioResponse, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactError, PostBotsByBotIdMemoryCompactResponse, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryError, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestError, PostBotsByBotIdMemoryIngestResponse, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildError, PostBotsByBotIdMemoryRebuildResponse, PostBotsByBotIdMemoryResponse, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchError, PostBotsByBotIdMemorySearchResponse, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteError, PostBotsByBotIdQuickActionsExecuteResponse, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleError, PostBotsByBotIdScheduleResponse, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeError, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactError, PostBotsByBotIdSessionsBySessionIdCompactResponse, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerError, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerResponse, PostBotsByBotIdSessionsBySessionIdFollowUpQueueData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueError, PostBotsByBotIdSessionsBySessionIdFollowUpQueueResponse, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkError, PostBotsByBotIdSessionsBySessionIdForkResponse, PostBotsByBotIdSessionsBySessionIdSteerQueueData, PostBotsByBotIdSessionsBySessionIdSteerQueueError, PostBotsByBotIdSessionsBySessionIdSteerQueueResponse, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsError, PostBotsByBotIdSessionsResponse, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsError, PostBotsByBotIdSettingsResponse, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageError, PostBotsByBotIdSupermarketInstallPackageResponse, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveError, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponse, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectError, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponse, PostBotsByBotIdToolsData, PostBotsByBotIdToolsError, PostBotsByBotIdToolsResponse, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeError, PostBotsByBotIdTtsSynthesizeResponse, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessError, PostBotsByBotIdUserAccessResponse, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesError, PostBotsByBotIdWebMessagesResponse, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsError, PostBotsByBotIdWorkdirsResponse, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatError, PostBotsByIdChannelByPlatformSendChatResponse, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendError, PostBotsByIdChannelByPlatformSendResponse, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointError, PostBotsByIdChannelByPlatformWebhookEndpointResponse, PostBotsData, PostBotsError, PostBotsResponse, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdError, PostEmailMailgunWebhookByConfigIdResponse, PostEmailProvidersData, PostEmailProvidersError, PostEmailProvidersResponse, PostFetchProvidersData, PostFetchProvidersError, PostFetchProvidersResponse, PostMemoryProvidersData, PostMemoryProvidersError, PostMemoryProvidersResponse, PostModelsByIdTestData, PostModelsByIdTestError, PostModelsByIdTestResponse, PostModelsData, PostModelsError, PostModelsResponse, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsError, PostProvidersByIdImportModelsResponse, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollError, PostProvidersByIdOauthPollResponse, PostProvidersByIdTestData, PostProvidersByIdTestError, PostProvidersByIdTestResponse, PostProvidersData, PostProvidersError, PostProvidersFromTemplateData, PostProvidersFromTemplateError, PostProvidersFromTemplateResponse, PostProvidersResponse, PostSearchProvidersData, PostSearchProvidersError, PostSearchProvidersResponse, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestError, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsError, PostSpeechProvidersByIdImportModelsResponse, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestError, PostTranscriptionModelsByIdTestResponse, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsError, PostTranscriptionProvidersByIdImportModelsResponse, PostUsersData, PostUsersError, PostUsersMeChannelLinksData, PostUsersMeChannelLinksError, PostUsersMeChannelLinksResponse, PostUsersMeRuntimesData, PostUsersMeRuntimesError, PostUsersMeRuntimesResponse, PostUsersResponse, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsError, PostVideoProvidersByIdImportModelsResponse, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectError, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdError, PutBotsByBotIdAclRulesByRuleIdResponse, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialError, PutBotsByBotIdAgentsByIdCredentialResponse, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsError, PutBotsByBotIdContainerMetricsResponse, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdError, PutBotsByBotIdEmailBindingsByIdResponse, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdError, PutBotsByBotIdMcpByIdResponse, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportError, PutBotsByBotIdMcpOpsImportResponse, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdError, PutBotsByBotIdMemoryByMemoryIdResponse, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdError, PutBotsByBotIdScheduleByIdResponse, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderData, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderError, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderResponse, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderData, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderError, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderResponse, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsError, PutBotsByBotIdSettingsResponse, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdError, PutBotsByBotIdUserAccessByGrantIdResponse, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalError, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponse, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformError, PutBotsByIdChannelByPlatformResponse, PutBotsByIdData, PutBotsByIdError, PutBotsByIdOwnerData, PutBotsByIdOwnerError, PutBotsByIdOwnerResponse, PutBotsByIdResponse, PutEmailProvidersByIdData, PutEmailProvidersByIdError, PutEmailProvidersByIdResponse, PutFetchProvidersByIdData, PutFetchProvidersByIdError, PutFetchProvidersByIdResponse, PutMemoryProvidersByIdData, PutMemoryProvidersByIdError, PutMemoryProvidersByIdResponse, PutModelsByIdData, PutModelsByIdError, PutModelsByIdResponse, PutModelsModelByModelIdData, PutModelsModelByModelIdError, PutModelsModelByModelIdResponse, PutProvidersByIdData, PutProvidersByIdError, PutProvidersByIdResponse, PutSearchProvidersByIdData, PutSearchProvidersByIdError, PutSearchProvidersByIdResponse, PutSpeechModelsByIdData, PutSpeechModelsByIdError, PutSpeechModelsByIdResponse, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdError, PutTranscriptionModelsByIdResponse, PutUsersByIdData, PutUsersByIdError, PutUsersByIdResponse, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformError, PutUsersMeChannelsByPlatformResponse, PutUsersMeData, PutUsersMeError, PutUsersMePasswordData, PutUsersMePasswordError, PutUsersMeResponse, PutVideoModelsByIdData, PutVideoModelsByIdError, PutVideoModelsByIdResponse } from '../types.gen'; export type QueryKey = [ Pick & { @@ -2437,6 +2437,93 @@ export const getBotsByBotIdSessionsBySessionIdContextLifecycleQuery = defineQuer } })); +export const getBotsByBotIdSessionsBySessionIdFollowUpQueueQueryKey = (options: Options) => createQueryKey('getBotsByBotIdSessionsBySessionIdFollowUpQueue', options); + +/** + * List pending follow-up inputs + */ +export const getBotsByBotIdSessionsBySessionIdFollowUpQueueQuery = defineQueryOptions, GetBotsByBotIdSessionsBySessionIdFollowUpQueueResponse, GetBotsByBotIdSessionsBySessionIdFollowUpQueueError>((options: Options) => ({ + key: getBotsByBotIdSessionsBySessionIdFollowUpQueueQueryKey(options), + query: async (context) => { + const { data } = await getBotsByBotIdSessionsBySessionIdFollowUpQueue({ + ...options, + ...context, + throwOnError: true + }); + return data; + } +})); + +/** + * Enqueue follow-up input for the active session run + */ +export const postBotsByBotIdSessionsBySessionIdFollowUpQueueMutation = (options?: Partial>): UseMutationOptions, PostBotsByBotIdSessionsBySessionIdFollowUpQueueError> => ({ + mutation: async (vars) => { + const { data } = await postBotsByBotIdSessionsBySessionIdFollowUpQueue({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Reorder accepted follow-up inputs + */ +export const putBotsByBotIdSessionsBySessionIdFollowUpQueueReorderMutation = (options?: Partial>): UseMutationOptions, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderError> => ({ + mutation: async (vars) => { + const { data } = await putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Cancel an accepted follow-up input + */ +export const deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdMutation = (options?: Partial>): UseMutationOptions, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError> => ({ + mutation: async (vars) => { + const { data } = await deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Edit an accepted follow-up input + */ +export const patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdMutation = (options?: Partial>): UseMutationOptions, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError> => ({ + mutation: async (vars) => { + const { data } = await patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Promote an accepted follow-up input to steer the active run + */ +export const postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerMutation = (options?: Partial>): UseMutationOptions, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerError> => ({ + mutation: async (vars) => { + const { data } = await postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + /** * Fork a chat session from an assistant reply */ @@ -2451,6 +2538,23 @@ export const postBotsByBotIdSessionsBySessionIdForkMutation = (options?: Partial } }); +export const getBotsByBotIdSessionsBySessionIdQueueQueryKey = (options: Options) => createQueryKey('getBotsByBotIdSessionsBySessionIdQueue', options); + +/** + * List pending steer and follow-up inputs in one response + */ +export const getBotsByBotIdSessionsBySessionIdQueueQuery = defineQueryOptions, GetBotsByBotIdSessionsBySessionIdQueueResponse, GetBotsByBotIdSessionsBySessionIdQueueError>((options: Options) => ({ + key: getBotsByBotIdSessionsBySessionIdQueueQueryKey(options), + query: async (context) => { + const { data } = await getBotsByBotIdSessionsBySessionIdQueue({ + ...options, + ...context, + throwOnError: true + }); + return data; + } +})); + export const getBotsByBotIdSessionsBySessionIdStatusQueryKey = (options: Options) => createQueryKey('getBotsByBotIdSessionsBySessionIdStatus', options); /** @@ -2470,6 +2574,79 @@ export const getBotsByBotIdSessionsBySessionIdStatusQuery = defineQueryOptions) => createQueryKey('getBotsByBotIdSessionsBySessionIdSteerQueue', options); + +/** + * List pending steer inputs + */ +export const getBotsByBotIdSessionsBySessionIdSteerQueueQuery = defineQueryOptions, GetBotsByBotIdSessionsBySessionIdSteerQueueResponse, GetBotsByBotIdSessionsBySessionIdSteerQueueError>((options: Options) => ({ + key: getBotsByBotIdSessionsBySessionIdSteerQueueQueryKey(options), + query: async (context) => { + const { data } = await getBotsByBotIdSessionsBySessionIdSteerQueue({ + ...options, + ...context, + throwOnError: true + }); + return data; + } +})); + +/** + * Enqueue steer input for the active session run + */ +export const postBotsByBotIdSessionsBySessionIdSteerQueueMutation = (options?: Partial>): UseMutationOptions, PostBotsByBotIdSessionsBySessionIdSteerQueueError> => ({ + mutation: async (vars) => { + const { data } = await postBotsByBotIdSessionsBySessionIdSteerQueue({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Reorder accepted steer inputs + */ +export const putBotsByBotIdSessionsBySessionIdSteerQueueReorderMutation = (options?: Partial>): UseMutationOptions, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderError> => ({ + mutation: async (vars) => { + const { data } = await putBotsByBotIdSessionsBySessionIdSteerQueueReorder({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Cancel an accepted steer input + */ +export const deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdMutation = (options?: Partial>): UseMutationOptions, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError> => ({ + mutation: async (vars) => { + const { data } = await deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + +/** + * Edit an accepted steer input + */ +export const patchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdMutation = (options?: Partial>): UseMutationOptions, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError> => ({ + mutation: async (vars) => { + const { data } = await patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId({ + ...options, + ...vars, + throwOnError: true + }); + return data; + } +}); + /** * Delete user settings * diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index c01fa40ce4..2523e04b01 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { deleteBotsByBotIdAclRulesByRuleId, deleteBotsByBotIdAcpRuntimesByRuntimeId, deleteBotsByBotIdAgentsById, deleteBotsByBotIdAgentsByIdCredential, deleteBotsByBotIdChannelManagersByChannelIdentityId, deleteBotsByBotIdCompactionLogs, deleteBotsByBotIdConnectorsByConnectionId, deleteBotsByBotIdContainer, deleteBotsByBotIdContainerBrowserSessionsBySessionId, deleteBotsByBotIdContainerDisplaySessionsBySessionId, deleteBotsByBotIdContainerSkills, deleteBotsByBotIdEmailBindingsById, deleteBotsByBotIdMcpById, deleteBotsByBotIdMcpByIdOauthToken, deleteBotsByBotIdMemory, deleteBotsByBotIdMemoryById, deleteBotsByBotIdMessages, deleteBotsByBotIdScheduleById, deleteBotsByBotIdScheduleLogs, deleteBotsByBotIdSessionsBySessionId, deleteBotsByBotIdSettings, deleteBotsByBotIdSupermarketPackagesByInstallationId, deleteBotsByBotIdUserAccessByGrantId, deleteBotsByBotIdWorkdirsByWorkdirId, deleteBotsByBotIdWorkspaceTargetsByTargetId, deleteBotsById, deleteBotsByIdChannelByPlatform, deleteEmailProvidersById, deleteEmailProvidersByIdOauthToken, deleteFetchProvidersById, deleteMemoryProvidersById, deleteModelsById, deleteModelsModelByModelId, deleteProvidersById, deleteProvidersByIdOauthToken, deleteSearchProvidersById, deleteUsersById, deleteUsersMeChannelIdentitiesByChannelIdentityId, deleteUsersMeRuntimesById, getAcpProfiles, getBots, getBotsByBotIdAclChannelIdentities, getBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversations, getBotsByBotIdAclChannelTypesByChannelTypeConversations, getBotsByBotIdAclDefaultEffect, getBotsByBotIdAclRules, getBotsByBotIdAcpRuntimesByRuntimeId, getBotsByBotIdAgents, getBotsByBotIdAgentsById, getBotsByBotIdAgentsByIdCredential, getBotsByBotIdAgentsByIdModels, getBotsByBotIdBackupSummary, getBotsByBotIdChannelManagers, getBotsByBotIdCompactionLogs, getBotsByBotIdConnectors, getBotsByBotIdConnectorsByConnectionId, getBotsByBotIdContainer, getBotsByBotIdContainerDisplay, getBotsByBotIdContainerDisplaySessions, getBotsByBotIdContainerFs, getBotsByBotIdContainerFsDownload, getBotsByBotIdContainerFsList, getBotsByBotIdContainerFsRead, getBotsByBotIdContainerMetrics, getBotsByBotIdContainerSkills, getBotsByBotIdContainerSnapshots, getBotsByBotIdContainerTerminal, getBotsByBotIdContainerTerminalWs, getBotsByBotIdEmailBindings, getBotsByBotIdEmailOutbox, getBotsByBotIdEmailOutboxById, getBotsByBotIdHooksEvents, getBotsByBotIdMcp, getBotsByBotIdMcpById, getBotsByBotIdMcpByIdOauthStatus, getBotsByBotIdMcpOpsExport, getBotsByBotIdMemory, getBotsByBotIdMemoryGraph, getBotsByBotIdMemoryStatus, getBotsByBotIdMemoryUsage, getBotsByBotIdMessages, getBotsByBotIdMessagesLocate, getBotsByBotIdSchedule, getBotsByBotIdScheduleById, getBotsByBotIdScheduleByIdLogs, getBotsByBotIdScheduleLogs, getBotsByBotIdSessions, getBotsByBotIdSessionsBySessionId, getBotsByBotIdSessionsBySessionIdAcpRuntime, getBotsByBotIdSessionsBySessionIdContextLifecycle, getBotsByBotIdSessionsBySessionIdStatus, getBotsByBotIdSessionsEvents, getBotsByBotIdSessionsModelPreferenceSeed, getBotsByBotIdSettings, getBotsByBotIdSkillsCatalog, getBotsByBotIdSupermarketPackages, getBotsByBotIdTokenUsage, getBotsByBotIdTokenUsageRecords, getBotsByBotIdUserAccess, getBotsByBotIdUserAccessCandidates, getBotsByBotIdWebStream, getBotsByBotIdWebWs, getBotsByBotIdWorkdirs, getBotsByBotIdWorkspaceTargets, getBotsById, getBotsByIdChannelByPlatform, getBotsByIdChecks, getBotsNameAvailability, getBotsUserAccessCandidates, getChannels, getChannelsByPlatform, getConnectorsCatalog, getEmailOauthCallback, getEmailProviders, getEmailProvidersById, getEmailProvidersByIdOauthAuthorize, getEmailProvidersByIdOauthStatus, getEmailProvidersMeta, getFetchProviders, getFetchProvidersById, getFetchProvidersMeta, getMemoryProviders, getMemoryProvidersById, getMemoryProvidersByIdStatus, getMemoryProvidersMeta, getModels, getModelsById, getModelsCount, getModelsModelByModelId, getOauthMcpCallback, getPing, getProviders, getProvidersById, getProvidersByIdModels, getProvidersByIdOauthAuthorize, getProvidersByIdOauthStatus, getProvidersCount, getProvidersNameByName, getProvidersOauthCallback, getProviderTemplates, getProviderTemplatesById, getSearchProviders, getSearchProvidersById, getSearchProvidersMeta, getSpeechModels, getSpeechModelsById, getSpeechModelsByIdCapabilities, getSpeechProviders, getSpeechProvidersById, getSpeechProvidersByIdModels, getSpeechProvidersMeta, getSupermarketArtifactsIconByDigest, getSupermarketPackages, getSupermarketRegistries, getSupermarketRegistriesByRegistryIdCategories, getSupermarketRegistriesByRegistryIdPackages, getSupermarketRegistriesByRegistryIdPackagesByPackageId, getSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevision, getSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillId, getSupermarketSkills, getTranscriptionModels, getTranscriptionModelsById, getTranscriptionModelsByIdCapabilities, getTranscriptionProviders, getTranscriptionProvidersById, getTranscriptionProvidersByIdModels, getTranscriptionProvidersMeta, getUsers, getUsersById, getUsersMe, getUsersMeChannelIdentities, getUsersMeChannelsByPlatform, getUsersMeComputerAccess, getUsersMeRuntimes, getVideoModels, getVideoModelsById, getVideoProviders, getVideoProvidersById, getVideoProvidersByIdModels, getVideoProvidersMeta, getWebhookTunnelStatus, type Options, patchBotsByBotIdAcpRuntimesByRuntimeIdMode, patchBotsByBotIdAcpRuntimesByRuntimeIdModel, patchBotsByBotIdAcpRuntimesByRuntimeIdReasoning, patchBotsByBotIdAgentsById, patchBotsByBotIdConnectorsByConnectionId, patchBotsByBotIdSessionsBySessionId, patchBotsByBotIdSessionsBySessionIdAcpRuntimeMode, patchBotsByBotIdSessionsBySessionIdAcpRuntimeModel, patchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoning, patchBotsByBotIdWorkdirsByWorkdirId, patchBotsByIdChannelByPlatformStatus, postAuthLogin, postAuthRefresh, postBots, postBotsBackupImport, postBotsBackupImportPreview, postBotsByBotIdAclRules, postBotsByBotIdAcpRuntimes, postBotsByBotIdAgents, postBotsByBotIdAgentsByIdCodexLoginDeviceAuthorize, postBotsByBotIdAgentsByIdCodexLoginDeviceCancel, postBotsByBotIdAgentsByIdCodexLoginDevicePoll, postBotsByBotIdBackupExport, postBotsByBotIdChannelManagers, postBotsByBotIdConnectorsApiKey, postBotsByBotIdConnectorsByConnectionIdReauth, postBotsByBotIdConnectorsOauth, postBotsByBotIdContainer, postBotsByBotIdContainerBrowserSessions, postBotsByBotIdContainerBrowserSessionsBySessionIdKeepalive, postBotsByBotIdContainerDataRestore, postBotsByBotIdContainerDisplayPrepare, postBotsByBotIdContainerDisplayWebrtcOffer, postBotsByBotIdContainerFsArchive, postBotsByBotIdContainerFsDelete, postBotsByBotIdContainerFsExtract, postBotsByBotIdContainerFsMkdir, postBotsByBotIdContainerFsRename, postBotsByBotIdContainerFsUpload, postBotsByBotIdContainerFsWrite, postBotsByBotIdContainerSkills, postBotsByBotIdContainerSkillsActions, postBotsByBotIdContainerSnapshots, postBotsByBotIdContainerSnapshotsRollback, postBotsByBotIdContainerStart, postBotsByBotIdContainerStop, postBotsByBotIdEmailBindings, postBotsByBotIdHooksTest, postBotsByBotIdMcp, postBotsByBotIdMcpByIdOauthAuthorize, postBotsByBotIdMcpByIdOauthDiscover, postBotsByBotIdMcpByIdOauthExchange, postBotsByBotIdMcpByIdProbe, postBotsByBotIdMcpOpsBatchDelete, postBotsByBotIdMcpStdio, postBotsByBotIdMcpStdioByConnectionId, postBotsByBotIdMemory, postBotsByBotIdMemoryCompact, postBotsByBotIdMemoryIngest, postBotsByBotIdMemoryRebuild, postBotsByBotIdMemorySearch, postBotsByBotIdQuickActionsExecute, postBotsByBotIdSchedule, postBotsByBotIdSessions, postBotsByBotIdSessionsBySessionIdAcpRuntime, postBotsByBotIdSessionsBySessionIdCompact, postBotsByBotIdSessionsBySessionIdFork, postBotsByBotIdSettings, postBotsByBotIdSupermarketInstallPackage, postBotsByBotIdToolApprovalsByApprovalIdApprove, postBotsByBotIdToolApprovalsByApprovalIdReject, postBotsByBotIdTools, postBotsByBotIdTtsSynthesize, postBotsByBotIdUserAccess, postBotsByBotIdWebMessages, postBotsByBotIdWorkdirs, postBotsByIdChannelByPlatformSend, postBotsByIdChannelByPlatformSendChat, postBotsByIdChannelByPlatformWebhookEndpoint, postEmailMailgunWebhookByConfigId, postEmailProviders, postFetchProviders, postMemoryProviders, postModels, postModelsByIdTest, postProviders, postProvidersByIdImportModels, postProvidersByIdOauthPoll, postProvidersByIdTest, postProvidersFromTemplate, postSearchProviders, postSpeechModelsByIdTest, postSpeechProvidersByIdImportModels, postTranscriptionModelsByIdTest, postTranscriptionProvidersByIdImportModels, postUsers, postUsersMeChannelLinks, postUsersMeRuntimes, postVideoProvidersByIdImportModels, putBotsByBotIdAclDefaultEffect, putBotsByBotIdAclRulesByRuleId, putBotsByBotIdAgentsByIdCredential, putBotsByBotIdContainerMetrics, putBotsByBotIdEmailBindingsById, putBotsByBotIdMcpById, putBotsByBotIdMcpOpsImport, putBotsByBotIdMemoryByMemoryId, putBotsByBotIdScheduleById, putBotsByBotIdSettings, putBotsByBotIdUserAccessByGrantId, putBotsByBotIdWorkspaceTargetsByTargetIdToolApproval, putBotsByBotIdWorkspaceTargetsPrimary, putBotsByBotIdWorkspaceTargetsRemotesByRuntimeId, putBotsById, putBotsByIdChannelByPlatform, putBotsByIdOwner, putEmailProvidersById, putFetchProvidersById, putMemoryProvidersById, putModelsById, putModelsModelByModelId, putProvidersById, putSearchProvidersById, putSpeechModelsById, putTranscriptionModelsById, putUsersById, putUsersMe, putUsersMeChannelsByPlatform, putUsersMePassword, putVideoModelsById } from './sdk.gen'; -export type { AccountsAccount, AccountsCreateAccountRequest, AccountsListAccountsResponse, AccountsUpdateAccountRequest, AccountsUpdatePasswordRequest, AccountsUpdateProfileMetadata, AccountsUpdateProfileRequest, AclChannelIdentityCandidate, AclChannelIdentityCandidateListResponse, AclCreateRuleRequest, AclDefaultEffectResponse, AclListRulesResponse, AclObservedConversationCandidate, AclObservedConversationCandidateListResponse, AclRule, AclSourceScope, AclUpdateRuleRequest, AcpagentRuntimeStatus, AcpclientAvailableCommandInfo, AcpclientModeInfo, AcpclientModelInfo, AcpclientModelState, AcpclientModeState, AcpclientReasoningEffortInfo, AcpclientReasoningState, AcpprofileManagedField, AcpprofileProfilesResponse, AcpprofilePublicProfile, AdaptersCompactResult, AdaptersDeleteResponse, AdaptersHealthStatus, AdaptersIngestResult, AdaptersMemoryCompactCapability, AdaptersMemoryItem, AdaptersMemoryStatusResponse, AdaptersMessage, AdaptersProviderCollectionStatus, AdaptersProviderConfigSchema, AdaptersProviderCreateRequest, AdaptersProviderFieldSchema, AdaptersProviderGetResponse, AdaptersProviderMeta, AdaptersProviderStatusResponse, AdaptersProviderType, AdaptersProviderUpdateRequest, AdaptersRebuildResult, AdaptersSearchResponse, AdaptersUsageResponse, AgentcredentialPublicCredential, ApperrorProblem, AudioConfigSchema, AudioFieldSchema, AudioImportModelsResponse, AudioModelCapabilities, AudioModelInfo, AudioParamConstraint, AudioProviderMetaResponse, AudioSpeechModelResponse, AudioSpeechProviderResponse, AudioTestSynthesizeRequest, AudioTestTranscriptionResponse, AudioTranscriptionModelResponse, AudioTranscriptionWord, AudioUpdateSpeechModelRequest, AudioVoiceInfo, BotagentsBotAgent, BotagentsCreateRequest, BotagentsListResponse, BotagentsUpdateRequest, BotbackupExportRequest, BotbackupImportMode, BotbackupImportResult, BotbackupManifest, BotbackupManifestEntry, BotbackupManifestOptions, BotbackupPreviewResult, BotbackupProfilePreview, BotbackupRestorePlan, BotbackupSection, BotbackupSectionSummary, BotbackupSummaryResult, BotsBot, BotsBotCheck, BotsCreateBotRequest, BotsCreateUserGrantRequest, BotsListBotsResponse, BotsListChecksResponse, BotsNameAvailability, BotsTransferBotRequest, BotsUpdateBotRequest, BotsUpdateUserGrantRequest, BotsUserGrant, ChannelaccessBinding, ChannelaccessIssueLinkCodeRequest, ChannelaccessLinkCode, ChannelaccessListBindingsResponse, ChannelaccessListManagersResponse, ChannelaccessManager, ChannelaccessSetManagerRequest, ChannelAction, ChannelAttachment, ChannelAttachmentType, ChannelChannelCapabilities, ChannelChannelConfig, ChannelChannelIdentityBinding, ChannelChannelType, ChannelConfigSchema, ChannelFieldSchema, ChannelFieldType, ChannelForwardRef, ChannelMessage, ChannelMessageFormat, ChannelMessagePart, ChannelMessagePartType, ChannelMessageTextStyle, ChannelReplyRef, ChannelSendRequest, ChannelSetWebhookEndpointRequest, ChannelSetWebhookEndpointResponse, ChannelTargetHint, ChannelTargetSpec, ChannelThreadRef, ChannelUpdateChannelStatusRequest, ChannelUpsertChannelIdentityConfigRequest, ChannelUpsertConfigRequest, ClientOptions, CompactionListLogsResponse, CompactionLog, ConnectitAuthMethod, ConnectitConnector, ConnectitCredentialField, ConnectitOAuthAuthorization, ConnectorsConnector, ConnectorsListResponse, ContextfragCacheClass, ContextfragCacheComparison, ContextfragCacheUsageRecord, ContextfragContentRange, ContextfragContextBudgetPlan, ContextfragContextRef, ContextfragKind, ContextfragKindBreakdown, ContextfragLifecycleSnapshot, ContextfragManifestCounts, ContextfragManifestView, ContextfragMemoryRecallQueryTrace, ContextfragMemoryRecallResultTrace, ContextfragMemoryRecallTrace, ContextfragMutationKind, ContextfragMutationRecord, ContextfragRefDurability, ContextfragRetentionTier, ContextfragSelectionDecision, ContextfragSelectionDecisionKind, ContextfragSelectionTrace, ContextfragSlot, ContextfragStepSnapshot, ContextfragToolDefAccounting, ContextfragTrustBreakdown, ContextfragTrustLevel, ConversationSkillActivation, ConversationSkillActivationSkill, ConversationUiAttachment, ConversationUiBackgroundTask, ConversationUiExecutionLocation, ConversationUiForwardRef, ConversationUiMessage, ConversationUiMessageType, ConversationUiReasoningTiming, ConversationUiReplyRef, ConversationUiToolApproval, ConversationUiToolApprovalOption, ConversationUiTurn, ConversationUiUserInput, DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdError, DeleteBotsByBotIdAclRulesByRuleIdErrors, DeleteBotsByBotIdAclRulesByRuleIdResponses, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdError, DeleteBotsByBotIdAcpRuntimesByRuntimeIdErrors, DeleteBotsByBotIdAcpRuntimesByRuntimeIdResponses, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialError, DeleteBotsByBotIdAgentsByIdCredentialErrors, DeleteBotsByBotIdAgentsByIdCredentialResponses, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdError, DeleteBotsByBotIdAgentsByIdErrors, DeleteBotsByBotIdAgentsByIdResponses, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdError, DeleteBotsByBotIdChannelManagersByChannelIdentityIdErrors, DeleteBotsByBotIdChannelManagersByChannelIdentityIdResponses, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsError, DeleteBotsByBotIdCompactionLogsErrors, DeleteBotsByBotIdCompactionLogsResponses, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdError, DeleteBotsByBotIdConnectorsByConnectionIdErrors, DeleteBotsByBotIdConnectorsByConnectionIdResponses, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdError, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdErrors, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdResponses, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdError, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdErrors, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdResponses, DeleteBotsByBotIdContainerError, DeleteBotsByBotIdContainerErrors, DeleteBotsByBotIdContainerResponses, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsError, DeleteBotsByBotIdContainerSkillsErrors, DeleteBotsByBotIdContainerSkillsResponse, DeleteBotsByBotIdContainerSkillsResponses, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdError, DeleteBotsByBotIdEmailBindingsByIdErrors, DeleteBotsByBotIdEmailBindingsByIdResponses, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdError, DeleteBotsByBotIdMcpByIdErrors, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenError, DeleteBotsByBotIdMcpByIdOauthTokenErrors, DeleteBotsByBotIdMcpByIdOauthTokenResponses, DeleteBotsByBotIdMcpByIdResponses, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdError, DeleteBotsByBotIdMemoryByIdErrors, DeleteBotsByBotIdMemoryByIdResponse, DeleteBotsByBotIdMemoryByIdResponses, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryError, DeleteBotsByBotIdMemoryErrors, DeleteBotsByBotIdMemoryResponse, DeleteBotsByBotIdMemoryResponses, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesError, DeleteBotsByBotIdMessagesErrors, DeleteBotsByBotIdMessagesResponses, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdError, DeleteBotsByBotIdScheduleByIdErrors, DeleteBotsByBotIdScheduleByIdResponses, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsError, DeleteBotsByBotIdScheduleLogsErrors, DeleteBotsByBotIdScheduleLogsResponses, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdError, DeleteBotsByBotIdSessionsBySessionIdErrors, DeleteBotsByBotIdSessionsBySessionIdResponses, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsError, DeleteBotsByBotIdSettingsErrors, DeleteBotsByBotIdSettingsResponses, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdErrors, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponse, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponses, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdError, DeleteBotsByBotIdUserAccessByGrantIdErrors, DeleteBotsByBotIdUserAccessByGrantIdResponses, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdError, DeleteBotsByBotIdWorkdirsByWorkdirIdErrors, DeleteBotsByBotIdWorkdirsByWorkdirIdResponses, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdError, DeleteBotsByBotIdWorkspaceTargetsByTargetIdErrors, DeleteBotsByBotIdWorkspaceTargetsByTargetIdResponses, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformError, DeleteBotsByIdChannelByPlatformErrors, DeleteBotsByIdChannelByPlatformResponses, DeleteBotsByIdData, DeleteBotsByIdError, DeleteBotsByIdErrors, DeleteBotsByIdResponse, DeleteBotsByIdResponses, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdError, DeleteEmailProvidersByIdErrors, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenError, DeleteEmailProvidersByIdOauthTokenErrors, DeleteEmailProvidersByIdOauthTokenResponses, DeleteEmailProvidersByIdResponses, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdError, DeleteFetchProvidersByIdErrors, DeleteFetchProvidersByIdResponses, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdError, DeleteMemoryProvidersByIdErrors, DeleteMemoryProvidersByIdResponses, DeleteModelsByIdData, DeleteModelsByIdError, DeleteModelsByIdErrors, DeleteModelsByIdResponses, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdError, DeleteModelsModelByModelIdErrors, DeleteModelsModelByModelIdResponses, DeleteProvidersByIdData, DeleteProvidersByIdError, DeleteProvidersByIdErrors, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenError, DeleteProvidersByIdOauthTokenErrors, DeleteProvidersByIdOauthTokenResponses, DeleteProvidersByIdResponses, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdError, DeleteSearchProvidersByIdErrors, DeleteSearchProvidersByIdResponses, DeleteUsersByIdData, DeleteUsersByIdError, DeleteUsersByIdErrors, DeleteUsersByIdResponses, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdError, DeleteUsersMeChannelIdentitiesByChannelIdentityIdErrors, DeleteUsersMeChannelIdentitiesByChannelIdentityIdResponses, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdError, DeleteUsersMeRuntimesByIdErrors, DeleteUsersMeRuntimesByIdResponses, DisplaySessionInfo, EmailBindingResponse, EmailConfigSchema, EmailCreateBindingRequest, EmailCreateProviderRequest, EmailFieldSchema, EmailOutboxItemResponse, EmailProviderMeta, EmailProviderResponse, EmailUpdateBindingRequest, EmailUpdateProviderRequest, ExternalagentCodexDeviceLoginAuthorizeResponse, ExternalagentCodexDeviceLoginPollRequest, ExternalagentCodexDeviceLoginPollResponse, ExternalModelCatalog, ExternalModelOption, ExternalReasoningEffortOption, FetchprovidersCreateRequest, FetchprovidersGetResponse, FetchprovidersProviderConfigSchema, FetchprovidersProviderFieldSchema, FetchprovidersProviderMeta, FetchprovidersProviderName, FetchprovidersUpdateRequest, GetAcpProfilesData, GetAcpProfilesResponse, GetAcpProfilesResponses, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsError, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsErrors, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponse, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponses, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesError, GetBotsByBotIdAclChannelIdentitiesErrors, GetBotsByBotIdAclChannelIdentitiesResponse, GetBotsByBotIdAclChannelIdentitiesResponses, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsError, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsErrors, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponse, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponses, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectError, GetBotsByBotIdAclDefaultEffectErrors, GetBotsByBotIdAclDefaultEffectResponse, GetBotsByBotIdAclDefaultEffectResponses, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesError, GetBotsByBotIdAclRulesErrors, GetBotsByBotIdAclRulesResponse, GetBotsByBotIdAclRulesResponses, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdError, GetBotsByBotIdAcpRuntimesByRuntimeIdErrors, GetBotsByBotIdAcpRuntimesByRuntimeIdResponse, GetBotsByBotIdAcpRuntimesByRuntimeIdResponses, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialError, GetBotsByBotIdAgentsByIdCredentialErrors, GetBotsByBotIdAgentsByIdCredentialResponse, GetBotsByBotIdAgentsByIdCredentialResponses, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdError, GetBotsByBotIdAgentsByIdErrors, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsError, GetBotsByBotIdAgentsByIdModelsErrors, GetBotsByBotIdAgentsByIdModelsResponse, GetBotsByBotIdAgentsByIdModelsResponses, GetBotsByBotIdAgentsByIdResponse, GetBotsByBotIdAgentsByIdResponses, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsError, GetBotsByBotIdAgentsErrors, GetBotsByBotIdAgentsResponse, GetBotsByBotIdAgentsResponses, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryError, GetBotsByBotIdBackupSummaryErrors, GetBotsByBotIdBackupSummaryResponse, GetBotsByBotIdBackupSummaryResponses, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersError, GetBotsByBotIdChannelManagersErrors, GetBotsByBotIdChannelManagersResponse, GetBotsByBotIdChannelManagersResponses, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsError, GetBotsByBotIdCompactionLogsErrors, GetBotsByBotIdCompactionLogsResponse, GetBotsByBotIdCompactionLogsResponses, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdError, GetBotsByBotIdConnectorsByConnectionIdErrors, GetBotsByBotIdConnectorsByConnectionIdResponse, GetBotsByBotIdConnectorsByConnectionIdResponses, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsError, GetBotsByBotIdConnectorsErrors, GetBotsByBotIdConnectorsResponse, GetBotsByBotIdConnectorsResponses, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayError, GetBotsByBotIdContainerDisplayErrors, GetBotsByBotIdContainerDisplayResponse, GetBotsByBotIdContainerDisplayResponses, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsError, GetBotsByBotIdContainerDisplaySessionsErrors, GetBotsByBotIdContainerDisplaySessionsResponse, GetBotsByBotIdContainerDisplaySessionsResponses, GetBotsByBotIdContainerError, GetBotsByBotIdContainerErrors, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadError, GetBotsByBotIdContainerFsDownloadErrors, GetBotsByBotIdContainerFsDownloadResponses, GetBotsByBotIdContainerFsError, GetBotsByBotIdContainerFsErrors, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListError, GetBotsByBotIdContainerFsListErrors, GetBotsByBotIdContainerFsListResponse, GetBotsByBotIdContainerFsListResponses, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadError, GetBotsByBotIdContainerFsReadErrors, GetBotsByBotIdContainerFsReadResponse, GetBotsByBotIdContainerFsReadResponses, GetBotsByBotIdContainerFsResponse, GetBotsByBotIdContainerFsResponses, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsError, GetBotsByBotIdContainerMetricsErrors, GetBotsByBotIdContainerMetricsResponse, GetBotsByBotIdContainerMetricsResponses, GetBotsByBotIdContainerResponse, GetBotsByBotIdContainerResponses, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsError, GetBotsByBotIdContainerSkillsErrors, GetBotsByBotIdContainerSkillsResponse, GetBotsByBotIdContainerSkillsResponses, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsError, GetBotsByBotIdContainerSnapshotsErrors, GetBotsByBotIdContainerSnapshotsResponse, GetBotsByBotIdContainerSnapshotsResponses, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalError, GetBotsByBotIdContainerTerminalErrors, GetBotsByBotIdContainerTerminalResponse, GetBotsByBotIdContainerTerminalResponses, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsError, GetBotsByBotIdContainerTerminalWsErrors, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsError, GetBotsByBotIdEmailBindingsErrors, GetBotsByBotIdEmailBindingsResponse, GetBotsByBotIdEmailBindingsResponses, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdError, GetBotsByBotIdEmailOutboxByIdErrors, GetBotsByBotIdEmailOutboxByIdResponse, GetBotsByBotIdEmailOutboxByIdResponses, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxError, GetBotsByBotIdEmailOutboxErrors, GetBotsByBotIdEmailOutboxResponse, GetBotsByBotIdEmailOutboxResponses, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsError, GetBotsByBotIdHooksEventsErrors, GetBotsByBotIdHooksEventsResponse, GetBotsByBotIdHooksEventsResponses, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdError, GetBotsByBotIdMcpByIdErrors, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusError, GetBotsByBotIdMcpByIdOauthStatusErrors, GetBotsByBotIdMcpByIdOauthStatusResponse, GetBotsByBotIdMcpByIdOauthStatusResponses, GetBotsByBotIdMcpByIdResponse, GetBotsByBotIdMcpByIdResponses, GetBotsByBotIdMcpData, GetBotsByBotIdMcpError, GetBotsByBotIdMcpErrors, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportError, GetBotsByBotIdMcpOpsExportErrors, GetBotsByBotIdMcpOpsExportResponse, GetBotsByBotIdMcpOpsExportResponses, GetBotsByBotIdMcpResponse, GetBotsByBotIdMcpResponses, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryError, GetBotsByBotIdMemoryErrors, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphError, GetBotsByBotIdMemoryGraphErrors, GetBotsByBotIdMemoryGraphResponse, GetBotsByBotIdMemoryGraphResponses, GetBotsByBotIdMemoryResponse, GetBotsByBotIdMemoryResponses, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusError, GetBotsByBotIdMemoryStatusErrors, GetBotsByBotIdMemoryStatusResponse, GetBotsByBotIdMemoryStatusResponses, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageError, GetBotsByBotIdMemoryUsageErrors, GetBotsByBotIdMemoryUsageResponse, GetBotsByBotIdMemoryUsageResponses, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesError, GetBotsByBotIdMessagesErrors, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateError, GetBotsByBotIdMessagesLocateErrors, GetBotsByBotIdMessagesLocateResponse, GetBotsByBotIdMessagesLocateResponses, GetBotsByBotIdMessagesResponse, GetBotsByBotIdMessagesResponses, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdError, GetBotsByBotIdScheduleByIdErrors, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsError, GetBotsByBotIdScheduleByIdLogsErrors, GetBotsByBotIdScheduleByIdLogsResponse, GetBotsByBotIdScheduleByIdLogsResponses, GetBotsByBotIdScheduleByIdResponse, GetBotsByBotIdScheduleByIdResponses, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleError, GetBotsByBotIdScheduleErrors, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsError, GetBotsByBotIdScheduleLogsErrors, GetBotsByBotIdScheduleLogsResponse, GetBotsByBotIdScheduleLogsResponses, GetBotsByBotIdScheduleResponse, GetBotsByBotIdScheduleResponses, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeError, GetBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleError, GetBotsByBotIdSessionsBySessionIdContextLifecycleErrors, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponse, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponses, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdError, GetBotsByBotIdSessionsBySessionIdErrors, GetBotsByBotIdSessionsBySessionIdResponse, GetBotsByBotIdSessionsBySessionIdResponses, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusError, GetBotsByBotIdSessionsBySessionIdStatusErrors, GetBotsByBotIdSessionsBySessionIdStatusResponse, GetBotsByBotIdSessionsBySessionIdStatusResponses, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsError, GetBotsByBotIdSessionsErrors, GetBotsByBotIdSessionsEventsData, GetBotsByBotIdSessionsEventsError, GetBotsByBotIdSessionsEventsErrors, GetBotsByBotIdSessionsEventsResponse, GetBotsByBotIdSessionsEventsResponses, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedError, GetBotsByBotIdSessionsModelPreferenceSeedErrors, GetBotsByBotIdSessionsModelPreferenceSeedResponse, GetBotsByBotIdSessionsModelPreferenceSeedResponses, GetBotsByBotIdSessionsResponse, GetBotsByBotIdSessionsResponses, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsError, GetBotsByBotIdSettingsErrors, GetBotsByBotIdSettingsResponse, GetBotsByBotIdSettingsResponses, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogError, GetBotsByBotIdSkillsCatalogErrors, GetBotsByBotIdSkillsCatalogResponse, GetBotsByBotIdSkillsCatalogResponses, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesError, GetBotsByBotIdSupermarketPackagesErrors, GetBotsByBotIdSupermarketPackagesResponse, GetBotsByBotIdSupermarketPackagesResponses, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageError, GetBotsByBotIdTokenUsageErrors, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsError, GetBotsByBotIdTokenUsageRecordsErrors, GetBotsByBotIdTokenUsageRecordsResponse, GetBotsByBotIdTokenUsageRecordsResponses, GetBotsByBotIdTokenUsageResponse, GetBotsByBotIdTokenUsageResponses, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesError, GetBotsByBotIdUserAccessCandidatesErrors, GetBotsByBotIdUserAccessCandidatesResponse, GetBotsByBotIdUserAccessCandidatesResponses, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessError, GetBotsByBotIdUserAccessErrors, GetBotsByBotIdUserAccessResponse, GetBotsByBotIdUserAccessResponses, GetBotsByBotIdWebStreamData, GetBotsByBotIdWebStreamError, GetBotsByBotIdWebStreamErrors, GetBotsByBotIdWebStreamResponse, GetBotsByBotIdWebStreamResponses, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsError, GetBotsByBotIdWebWsErrors, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsError, GetBotsByBotIdWorkdirsErrors, GetBotsByBotIdWorkdirsResponse, GetBotsByBotIdWorkdirsResponses, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsError, GetBotsByBotIdWorkspaceTargetsErrors, GetBotsByBotIdWorkspaceTargetsResponse, GetBotsByBotIdWorkspaceTargetsResponses, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformError, GetBotsByIdChannelByPlatformErrors, GetBotsByIdChannelByPlatformResponse, GetBotsByIdChannelByPlatformResponses, GetBotsByIdChecksData, GetBotsByIdChecksError, GetBotsByIdChecksErrors, GetBotsByIdChecksResponse, GetBotsByIdChecksResponses, GetBotsByIdData, GetBotsByIdError, GetBotsByIdErrors, GetBotsByIdResponse, GetBotsByIdResponses, GetBotsData, GetBotsError, GetBotsErrors, GetBotsNameAvailabilityData, GetBotsNameAvailabilityError, GetBotsNameAvailabilityErrors, GetBotsNameAvailabilityResponse, GetBotsNameAvailabilityResponses, GetBotsResponse, GetBotsResponses, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesError, GetBotsUserAccessCandidatesErrors, GetBotsUserAccessCandidatesResponse, GetBotsUserAccessCandidatesResponses, GetChannelsByPlatformData, GetChannelsByPlatformError, GetChannelsByPlatformErrors, GetChannelsByPlatformResponse, GetChannelsByPlatformResponses, GetChannelsData, GetChannelsError, GetChannelsErrors, GetChannelsResponse, GetChannelsResponses, GetConnectorsCatalogData, GetConnectorsCatalogError, GetConnectorsCatalogErrors, GetConnectorsCatalogResponse, GetConnectorsCatalogResponses, GetEmailOauthCallbackData, GetEmailOauthCallbackError, GetEmailOauthCallbackErrors, GetEmailOauthCallbackResponse, GetEmailOauthCallbackResponses, GetEmailProvidersByIdData, GetEmailProvidersByIdError, GetEmailProvidersByIdErrors, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeError, GetEmailProvidersByIdOauthAuthorizeErrors, GetEmailProvidersByIdOauthAuthorizeResponse, GetEmailProvidersByIdOauthAuthorizeResponses, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusError, GetEmailProvidersByIdOauthStatusErrors, GetEmailProvidersByIdOauthStatusResponse, GetEmailProvidersByIdOauthStatusResponses, GetEmailProvidersByIdResponse, GetEmailProvidersByIdResponses, GetEmailProvidersData, GetEmailProvidersError, GetEmailProvidersErrors, GetEmailProvidersMetaData, GetEmailProvidersMetaResponse, GetEmailProvidersMetaResponses, GetEmailProvidersResponse, GetEmailProvidersResponses, GetFetchProvidersByIdData, GetFetchProvidersByIdError, GetFetchProvidersByIdErrors, GetFetchProvidersByIdResponse, GetFetchProvidersByIdResponses, GetFetchProvidersData, GetFetchProvidersError, GetFetchProvidersErrors, GetFetchProvidersMetaData, GetFetchProvidersMetaResponse, GetFetchProvidersMetaResponses, GetFetchProvidersResponse, GetFetchProvidersResponses, GetMemoryProvidersByIdData, GetMemoryProvidersByIdError, GetMemoryProvidersByIdErrors, GetMemoryProvidersByIdResponse, GetMemoryProvidersByIdResponses, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusError, GetMemoryProvidersByIdStatusErrors, GetMemoryProvidersByIdStatusResponse, GetMemoryProvidersByIdStatusResponses, GetMemoryProvidersData, GetMemoryProvidersError, GetMemoryProvidersErrors, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponse, GetMemoryProvidersMetaResponses, GetMemoryProvidersResponse, GetMemoryProvidersResponses, GetModelsByIdData, GetModelsByIdError, GetModelsByIdErrors, GetModelsByIdResponse, GetModelsByIdResponses, GetModelsCountData, GetModelsCountError, GetModelsCountErrors, GetModelsCountResponse, GetModelsCountResponses, GetModelsData, GetModelsError, GetModelsErrors, GetModelsModelByModelIdData, GetModelsModelByModelIdError, GetModelsModelByModelIdErrors, GetModelsModelByModelIdResponse, GetModelsModelByModelIdResponses, GetModelsResponse, GetModelsResponses, GetOauthMcpCallbackData, GetOauthMcpCallbackError, GetOauthMcpCallbackErrors, GetOauthMcpCallbackResponse, GetOauthMcpCallbackResponses, GetPingData, GetPingResponse, GetPingResponses, GetProvidersByIdData, GetProvidersByIdError, GetProvidersByIdErrors, GetProvidersByIdModelsData, GetProvidersByIdModelsError, GetProvidersByIdModelsErrors, GetProvidersByIdModelsResponse, GetProvidersByIdModelsResponses, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeError, GetProvidersByIdOauthAuthorizeErrors, GetProvidersByIdOauthAuthorizeResponse, GetProvidersByIdOauthAuthorizeResponses, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusError, GetProvidersByIdOauthStatusErrors, GetProvidersByIdOauthStatusResponse, GetProvidersByIdOauthStatusResponses, GetProvidersByIdResponse, GetProvidersByIdResponses, GetProvidersCountData, GetProvidersCountError, GetProvidersCountErrors, GetProvidersCountResponse, GetProvidersCountResponses, GetProvidersData, GetProvidersError, GetProvidersErrors, GetProvidersNameByNameData, GetProvidersNameByNameError, GetProvidersNameByNameErrors, GetProvidersNameByNameResponse, GetProvidersNameByNameResponses, GetProvidersOauthCallbackData, GetProvidersOauthCallbackError, GetProvidersOauthCallbackErrors, GetProvidersOauthCallbackResponse, GetProvidersOauthCallbackResponses, GetProvidersResponse, GetProvidersResponses, GetProviderTemplatesByIdData, GetProviderTemplatesByIdError, GetProviderTemplatesByIdErrors, GetProviderTemplatesByIdResponse, GetProviderTemplatesByIdResponses, GetProviderTemplatesData, GetProviderTemplatesError, GetProviderTemplatesErrors, GetProviderTemplatesResponse, GetProviderTemplatesResponses, GetSearchProvidersByIdData, GetSearchProvidersByIdError, GetSearchProvidersByIdErrors, GetSearchProvidersByIdResponse, GetSearchProvidersByIdResponses, GetSearchProvidersData, GetSearchProvidersError, GetSearchProvidersErrors, GetSearchProvidersMetaData, GetSearchProvidersMetaResponse, GetSearchProvidersMetaResponses, GetSearchProvidersResponse, GetSearchProvidersResponses, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesError, GetSpeechModelsByIdCapabilitiesErrors, GetSpeechModelsByIdCapabilitiesResponse, GetSpeechModelsByIdCapabilitiesResponses, GetSpeechModelsByIdData, GetSpeechModelsByIdError, GetSpeechModelsByIdErrors, GetSpeechModelsByIdResponse, GetSpeechModelsByIdResponses, GetSpeechModelsData, GetSpeechModelsError, GetSpeechModelsErrors, GetSpeechModelsResponse, GetSpeechModelsResponses, GetSpeechProvidersByIdData, GetSpeechProvidersByIdError, GetSpeechProvidersByIdErrors, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsError, GetSpeechProvidersByIdModelsErrors, GetSpeechProvidersByIdModelsResponse, GetSpeechProvidersByIdModelsResponses, GetSpeechProvidersByIdResponse, GetSpeechProvidersByIdResponses, GetSpeechProvidersData, GetSpeechProvidersError, GetSpeechProvidersErrors, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponse, GetSpeechProvidersMetaResponses, GetSpeechProvidersResponse, GetSpeechProvidersResponses, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestError, GetSupermarketArtifactsIconByDigestErrors, GetSupermarketArtifactsIconByDigestResponses, GetSupermarketPackagesData, GetSupermarketPackagesError, GetSupermarketPackagesErrors, GetSupermarketPackagesResponse, GetSupermarketPackagesResponses, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesError, GetSupermarketRegistriesByRegistryIdCategoriesErrors, GetSupermarketRegistriesByRegistryIdCategoriesResponse, GetSupermarketRegistriesByRegistryIdCategoriesResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponses, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesError, GetSupermarketRegistriesByRegistryIdPackagesErrors, GetSupermarketRegistriesByRegistryIdPackagesResponse, GetSupermarketRegistriesByRegistryIdPackagesResponses, GetSupermarketRegistriesData, GetSupermarketRegistriesError, GetSupermarketRegistriesErrors, GetSupermarketRegistriesResponse, GetSupermarketRegistriesResponses, GetSupermarketSkillsData, GetSupermarketSkillsError, GetSupermarketSkillsErrors, GetSupermarketSkillsResponse, GetSupermarketSkillsResponses, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesError, GetTranscriptionModelsByIdCapabilitiesErrors, GetTranscriptionModelsByIdCapabilitiesResponse, GetTranscriptionModelsByIdCapabilitiesResponses, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdError, GetTranscriptionModelsByIdErrors, GetTranscriptionModelsByIdResponse, GetTranscriptionModelsByIdResponses, GetTranscriptionModelsData, GetTranscriptionModelsError, GetTranscriptionModelsErrors, GetTranscriptionModelsResponse, GetTranscriptionModelsResponses, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdError, GetTranscriptionProvidersByIdErrors, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsError, GetTranscriptionProvidersByIdModelsErrors, GetTranscriptionProvidersByIdModelsResponse, GetTranscriptionProvidersByIdModelsResponses, GetTranscriptionProvidersByIdResponse, GetTranscriptionProvidersByIdResponses, GetTranscriptionProvidersData, GetTranscriptionProvidersError, GetTranscriptionProvidersErrors, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponse, GetTranscriptionProvidersMetaResponses, GetTranscriptionProvidersResponse, GetTranscriptionProvidersResponses, GetUsersByIdData, GetUsersByIdError, GetUsersByIdErrors, GetUsersByIdResponse, GetUsersByIdResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesError, GetUsersMeChannelIdentitiesErrors, GetUsersMeChannelIdentitiesResponse, GetUsersMeChannelIdentitiesResponses, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformError, GetUsersMeChannelsByPlatformErrors, GetUsersMeChannelsByPlatformResponse, GetUsersMeChannelsByPlatformResponses, GetUsersMeComputerAccessData, GetUsersMeComputerAccessError, GetUsersMeComputerAccessErrors, GetUsersMeComputerAccessResponse, GetUsersMeComputerAccessResponses, GetUsersMeData, GetUsersMeError, GetUsersMeErrors, GetUsersMeResponse, GetUsersMeResponses, GetUsersMeRuntimesData, GetUsersMeRuntimesError, GetUsersMeRuntimesErrors, GetUsersMeRuntimesResponse, GetUsersMeRuntimesResponses, GetUsersResponse, GetUsersResponses, GetVideoModelsByIdData, GetVideoModelsByIdError, GetVideoModelsByIdErrors, GetVideoModelsByIdResponse, GetVideoModelsByIdResponses, GetVideoModelsData, GetVideoModelsError, GetVideoModelsErrors, GetVideoModelsResponse, GetVideoModelsResponses, GetVideoProvidersByIdData, GetVideoProvidersByIdError, GetVideoProvidersByIdErrors, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsError, GetVideoProvidersByIdModelsErrors, GetVideoProvidersByIdModelsResponse, GetVideoProvidersByIdModelsResponses, GetVideoProvidersByIdResponse, GetVideoProvidersByIdResponses, GetVideoProvidersData, GetVideoProvidersError, GetVideoProvidersErrors, GetVideoProvidersMetaData, GetVideoProvidersMetaResponse, GetVideoProvidersMetaResponses, GetVideoProvidersResponse, GetVideoProvidersResponses, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponse, GetWebhookTunnelStatusResponses, GithubComFelinicsMemohInternalMcpConnection, HandlersAcpRuntimeCreateRequest, HandlersAcpRuntimeModelRequest, HandlersAcpRuntimeModeRequest, HandlersAcpRuntimeReasoningRequest, HandlersAgentCredentialPutRequest, HandlersBatchDeleteRequest, HandlersBotUserCandidate, HandlersBotUserCandidateListResponse, HandlersBotUserGrantListResponse, HandlersBrowserSessionCreateRequest, HandlersBrowserSessionCreateResponse, HandlersBrowserSessionKeepAliveResponse, HandlersCacheStats, HandlersChannelMeta, HandlersCommandActionError, HandlersCommandActionListItem, HandlersCommandActionResult, HandlersCommandEventResponse, HandlersCompactionInfo, HandlersConnectorCredentialRequest, HandlersConnectorEnabledRequest, HandlersConnectorOAuthRequest, HandlersContainerCpuMetricsResponse, HandlersContainerGpuRequest, HandlersContainerMemoryMetricsResponse, HandlersContainerMetricsPayloadResponse, HandlersContainerMetricsStatusResponse, HandlersContainerResourceLimitCapabilitiesResponse, HandlersContainerResourceLimitCapabilityResponse, HandlersContainerResourceLimitObservedResponse, HandlersContainerResourceLimitValuesResponse, HandlersContainerStorageMetricsResponse, HandlersContextLifecycleAggregates, HandlersContextLifecycleResponse, HandlersContextLifecycleTurn, HandlersContextUsage, HandlersCreateContainerRequest, HandlersCreateContainerResponse, HandlersCreateSessionRequest, HandlersCreateSnapshotRequest, HandlersCreateSnapshotResponse, HandlersDailyTokenUsage, HandlersDisplayInfoResponse, HandlersDisplaySessionListResponse, HandlersDisplayWebRtcOfferRequest, HandlersDisplayWebRtcOfferResponse, HandlersEmailOAuthStatusResponse, HandlersErrorResponse, HandlersForkSessionRequest, HandlersFsArchiveRequest, HandlersFsDeleteRequest, HandlersFsExtractRequest, HandlersFsExtractResponse, HandlersFsFileInfo, HandlersFsListResponse, HandlersFsMkdirRequest, HandlersFsOpResponse, HandlersFsReadResponse, HandlersFsRenameRequest, HandlersFsUploadResponse, HandlersFsWriteRequest, HandlersGetContainerMetricsResponse, HandlersGetContainerResourceLimitsResponse, HandlersGetContainerResponse, HandlersGraphEdge, HandlersGraphNode, HandlersGraphResponse, HandlersHookEventInfo, HandlersHooksEventsResponse, HandlersHookTestRequest, HandlersHookTestResponse, HandlersInstallPackageRequest, HandlersInstallRegistryPackageResponse, HandlersInstallRegistrySkillResponse, HandlersListSessionsResponse, HandlersListSnapshotsResponse, HandlersLocalChannelMessageRequest, HandlersLoginRequest, HandlersLoginResponse, HandlersMcpStdioRequest, HandlersMcpStdioResponse, HandlersMemoryAddPayload, HandlersMemoryCompactPayload, HandlersMemoryDeletePayload, HandlersMemorySearchPayload, HandlersMemoryUpdatePayload, HandlersModelPreferenceSeedResponse, HandlersModelTokenUsage, HandlersOauthAuthorizeRequest, HandlersOauthDiscoverRequest, HandlersOauthExchangeRequest, HandlersPingResponse, HandlersProbeResponse, HandlersQuickActionExecuteRequest, HandlersRefreshResponse, HandlersRollbackRequest, HandlersSafeSkillsResponse, HandlersSessionInfoResponse, HandlersSkillItem, HandlersSkillsActionRequest, HandlersSkillsDeleteRequest, HandlersSkillsOpResponse, HandlersSkillsResponse, HandlersSkillsUpsertRequest, HandlersSnapshotInfo, HandlersSupermarketAuthor, HandlersSupermarketCatalogSkill, HandlersSupermarketCatalogSkillListResponse, HandlersSupermarketRegistry, HandlersSupermarketRegistryListResponse, HandlersSupermarketSkillArtifact, HandlersSupermarketSkillCategory, HandlersSupermarketSkillCategoryListResponse, HandlersSupermarketSkillCategoryRegistry, HandlersSupermarketSkillIcon, HandlersSupermarketSkillIconAsset, HandlersSupermarketSkillPackageCategory, HandlersSupermarketSkillPackageDescriptor, HandlersSupermarketSkillPackageListResponse, HandlersSupermarketSkillPackageSummary, HandlersSupermarketSkillSource, HandlersSynthesizeRequest, HandlersSynthesizeResponse, HandlersTerminalInfoResponse, HandlersTokenUsageRecord, HandlersTokenUsageRecordsResponse, HandlersTokenUsageResponse, HandlersToolApprovalDecisionRequest, HandlersToolDefBucket, HandlersTriggerCompactResponse, HandlersUiLocateMessageResponse, HandlersUiMessageListResponse, HandlersUpdateContainerMetricsRequest, HandlersUpdateContainerResourceLimitsRequest, HandlersUpdateSessionRequest, HooksActionResult, HooksOutputWarning, HooksResult, HooksSystemSectionCache, HooksSystemSectionOutput, HooksSystemSectionRetention, HooksToolPayload, McpAuthorizeResult, McpDiscoveryResult, McpExportResponse, McpImportRequest, McpListResponse, McpMcpServerEntry, McpOAuthStatus, McpToolDescriptor, McpUpsertRequest, ModelsAddRequest, ModelsAddResponse, ModelsCountResponse, ModelsGetResponse, ModelsModelConfig, ModelsModelType, ModelsTestResponse, ModelsTestStatus, ModelsUpdateRequest, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningError, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponses, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdError, PatchBotsByBotIdAgentsByIdErrors, PatchBotsByBotIdAgentsByIdResponse, PatchBotsByBotIdAgentsByIdResponses, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdError, PatchBotsByBotIdConnectorsByConnectionIdErrors, PatchBotsByBotIdConnectorsByConnectionIdResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponses, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdError, PatchBotsByBotIdSessionsBySessionIdErrors, PatchBotsByBotIdSessionsBySessionIdResponse, PatchBotsByBotIdSessionsBySessionIdResponses, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdError, PatchBotsByBotIdWorkdirsByWorkdirIdErrors, PatchBotsByBotIdWorkdirsByWorkdirIdResponse, PatchBotsByBotIdWorkdirsByWorkdirIdResponses, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusError, PatchBotsByIdChannelByPlatformStatusErrors, PatchBotsByIdChannelByPlatformStatusResponse, PatchBotsByIdChannelByPlatformStatusResponses, PostAuthLoginData, PostAuthLoginError, PostAuthLoginErrors, PostAuthLoginResponse, PostAuthLoginResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostBotsBackupImportData, PostBotsBackupImportError, PostBotsBackupImportErrors, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewError, PostBotsBackupImportPreviewErrors, PostBotsBackupImportPreviewResponse, PostBotsBackupImportPreviewResponses, PostBotsBackupImportResponse, PostBotsBackupImportResponses, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesError, PostBotsByBotIdAclRulesErrors, PostBotsByBotIdAclRulesResponse, PostBotsByBotIdAclRulesResponses, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesError, PostBotsByBotIdAcpRuntimesErrors, PostBotsByBotIdAcpRuntimesResponse, PostBotsByBotIdAcpRuntimesResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeError, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelError, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelResponses, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollErrors, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponse, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponses, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsError, PostBotsByBotIdAgentsErrors, PostBotsByBotIdAgentsResponse, PostBotsByBotIdAgentsResponses, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportError, PostBotsByBotIdBackupExportErrors, PostBotsByBotIdBackupExportResponses, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersError, PostBotsByBotIdChannelManagersErrors, PostBotsByBotIdChannelManagersResponses, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyError, PostBotsByBotIdConnectorsApiKeyErrors, PostBotsByBotIdConnectorsApiKeyResponse, PostBotsByBotIdConnectorsApiKeyResponses, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthError, PostBotsByBotIdConnectorsByConnectionIdReauthErrors, PostBotsByBotIdConnectorsByConnectionIdReauthResponse, PostBotsByBotIdConnectorsByConnectionIdReauthResponses, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthError, PostBotsByBotIdConnectorsOauthErrors, PostBotsByBotIdConnectorsOauthResponse, PostBotsByBotIdConnectorsOauthResponses, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveError, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveErrors, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponse, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponses, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsError, PostBotsByBotIdContainerBrowserSessionsErrors, PostBotsByBotIdContainerBrowserSessionsResponse, PostBotsByBotIdContainerBrowserSessionsResponses, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreError, PostBotsByBotIdContainerDataRestoreErrors, PostBotsByBotIdContainerDataRestoreResponse, PostBotsByBotIdContainerDataRestoreResponses, PostBotsByBotIdContainerDisplayPrepareData, PostBotsByBotIdContainerDisplayPrepareError, PostBotsByBotIdContainerDisplayPrepareErrors, PostBotsByBotIdContainerDisplayPrepareResponse, PostBotsByBotIdContainerDisplayPrepareResponses, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferError, PostBotsByBotIdContainerDisplayWebrtcOfferErrors, PostBotsByBotIdContainerDisplayWebrtcOfferResponse, PostBotsByBotIdContainerDisplayWebrtcOfferResponses, PostBotsByBotIdContainerError, PostBotsByBotIdContainerErrors, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveError, PostBotsByBotIdContainerFsArchiveErrors, PostBotsByBotIdContainerFsArchiveResponses, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteError, PostBotsByBotIdContainerFsDeleteErrors, PostBotsByBotIdContainerFsDeleteResponse, PostBotsByBotIdContainerFsDeleteResponses, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractError, PostBotsByBotIdContainerFsExtractErrors, PostBotsByBotIdContainerFsExtractResponse, PostBotsByBotIdContainerFsExtractResponses, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirError, PostBotsByBotIdContainerFsMkdirErrors, PostBotsByBotIdContainerFsMkdirResponse, PostBotsByBotIdContainerFsMkdirResponses, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameError, PostBotsByBotIdContainerFsRenameErrors, PostBotsByBotIdContainerFsRenameResponse, PostBotsByBotIdContainerFsRenameResponses, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadError, PostBotsByBotIdContainerFsUploadErrors, PostBotsByBotIdContainerFsUploadResponse, PostBotsByBotIdContainerFsUploadResponses, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteError, PostBotsByBotIdContainerFsWriteErrors, PostBotsByBotIdContainerFsWriteResponse, PostBotsByBotIdContainerFsWriteResponses, PostBotsByBotIdContainerResponse, PostBotsByBotIdContainerResponses, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsError, PostBotsByBotIdContainerSkillsActionsErrors, PostBotsByBotIdContainerSkillsActionsResponse, PostBotsByBotIdContainerSkillsActionsResponses, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsError, PostBotsByBotIdContainerSkillsErrors, PostBotsByBotIdContainerSkillsResponse, PostBotsByBotIdContainerSkillsResponses, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsError, PostBotsByBotIdContainerSnapshotsErrors, PostBotsByBotIdContainerSnapshotsResponse, PostBotsByBotIdContainerSnapshotsResponses, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackError, PostBotsByBotIdContainerSnapshotsRollbackErrors, PostBotsByBotIdContainerSnapshotsRollbackResponse, PostBotsByBotIdContainerSnapshotsRollbackResponses, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartError, PostBotsByBotIdContainerStartErrors, PostBotsByBotIdContainerStartResponse, PostBotsByBotIdContainerStartResponses, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopError, PostBotsByBotIdContainerStopErrors, PostBotsByBotIdContainerStopResponse, PostBotsByBotIdContainerStopResponses, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsError, PostBotsByBotIdEmailBindingsErrors, PostBotsByBotIdEmailBindingsResponse, PostBotsByBotIdEmailBindingsResponses, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestError, PostBotsByBotIdHooksTestErrors, PostBotsByBotIdHooksTestResponse, PostBotsByBotIdHooksTestResponses, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeError, PostBotsByBotIdMcpByIdOauthAuthorizeErrors, PostBotsByBotIdMcpByIdOauthAuthorizeResponse, PostBotsByBotIdMcpByIdOauthAuthorizeResponses, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverError, PostBotsByBotIdMcpByIdOauthDiscoverErrors, PostBotsByBotIdMcpByIdOauthDiscoverResponse, PostBotsByBotIdMcpByIdOauthDiscoverResponses, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeError, PostBotsByBotIdMcpByIdOauthExchangeErrors, PostBotsByBotIdMcpByIdOauthExchangeResponse, PostBotsByBotIdMcpByIdOauthExchangeResponses, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeError, PostBotsByBotIdMcpByIdProbeErrors, PostBotsByBotIdMcpByIdProbeResponse, PostBotsByBotIdMcpByIdProbeResponses, PostBotsByBotIdMcpData, PostBotsByBotIdMcpError, PostBotsByBotIdMcpErrors, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteError, PostBotsByBotIdMcpOpsBatchDeleteErrors, PostBotsByBotIdMcpOpsBatchDeleteResponses, PostBotsByBotIdMcpResponse, PostBotsByBotIdMcpResponses, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdError, PostBotsByBotIdMcpStdioByConnectionIdErrors, PostBotsByBotIdMcpStdioByConnectionIdResponse, PostBotsByBotIdMcpStdioByConnectionIdResponses, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioError, PostBotsByBotIdMcpStdioErrors, PostBotsByBotIdMcpStdioResponse, PostBotsByBotIdMcpStdioResponses, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactError, PostBotsByBotIdMemoryCompactErrors, PostBotsByBotIdMemoryCompactResponse, PostBotsByBotIdMemoryCompactResponses, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryError, PostBotsByBotIdMemoryErrors, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestError, PostBotsByBotIdMemoryIngestErrors, PostBotsByBotIdMemoryIngestResponse, PostBotsByBotIdMemoryIngestResponses, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildError, PostBotsByBotIdMemoryRebuildErrors, PostBotsByBotIdMemoryRebuildResponse, PostBotsByBotIdMemoryRebuildResponses, PostBotsByBotIdMemoryResponse, PostBotsByBotIdMemoryResponses, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchError, PostBotsByBotIdMemorySearchErrors, PostBotsByBotIdMemorySearchResponse, PostBotsByBotIdMemorySearchResponses, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteError, PostBotsByBotIdQuickActionsExecuteErrors, PostBotsByBotIdQuickActionsExecuteResponse, PostBotsByBotIdQuickActionsExecuteResponses, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleError, PostBotsByBotIdScheduleErrors, PostBotsByBotIdScheduleResponse, PostBotsByBotIdScheduleResponses, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeError, PostBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactError, PostBotsByBotIdSessionsBySessionIdCompactErrors, PostBotsByBotIdSessionsBySessionIdCompactResponse, PostBotsByBotIdSessionsBySessionIdCompactResponses, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkError, PostBotsByBotIdSessionsBySessionIdForkErrors, PostBotsByBotIdSessionsBySessionIdForkResponse, PostBotsByBotIdSessionsBySessionIdForkResponses, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsError, PostBotsByBotIdSessionsErrors, PostBotsByBotIdSessionsResponse, PostBotsByBotIdSessionsResponses, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsError, PostBotsByBotIdSettingsErrors, PostBotsByBotIdSettingsResponse, PostBotsByBotIdSettingsResponses, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageError, PostBotsByBotIdSupermarketInstallPackageErrors, PostBotsByBotIdSupermarketInstallPackageResponse, PostBotsByBotIdSupermarketInstallPackageResponses, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveError, PostBotsByBotIdToolApprovalsByApprovalIdApproveErrors, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponse, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponses, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectError, PostBotsByBotIdToolApprovalsByApprovalIdRejectErrors, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponse, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponses, PostBotsByBotIdToolsData, PostBotsByBotIdToolsError, PostBotsByBotIdToolsErrors, PostBotsByBotIdToolsResponse, PostBotsByBotIdToolsResponses, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeError, PostBotsByBotIdTtsSynthesizeErrors, PostBotsByBotIdTtsSynthesizeResponse, PostBotsByBotIdTtsSynthesizeResponses, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessError, PostBotsByBotIdUserAccessErrors, PostBotsByBotIdUserAccessResponse, PostBotsByBotIdUserAccessResponses, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesError, PostBotsByBotIdWebMessagesErrors, PostBotsByBotIdWebMessagesResponse, PostBotsByBotIdWebMessagesResponses, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsError, PostBotsByBotIdWorkdirsErrors, PostBotsByBotIdWorkdirsResponse, PostBotsByBotIdWorkdirsResponses, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatError, PostBotsByIdChannelByPlatformSendChatErrors, PostBotsByIdChannelByPlatformSendChatResponse, PostBotsByIdChannelByPlatformSendChatResponses, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendError, PostBotsByIdChannelByPlatformSendErrors, PostBotsByIdChannelByPlatformSendResponse, PostBotsByIdChannelByPlatformSendResponses, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointError, PostBotsByIdChannelByPlatformWebhookEndpointErrors, PostBotsByIdChannelByPlatformWebhookEndpointResponse, PostBotsByIdChannelByPlatformWebhookEndpointResponses, PostBotsData, PostBotsError, PostBotsErrors, PostBotsResponse, PostBotsResponses, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdError, PostEmailMailgunWebhookByConfigIdErrors, PostEmailMailgunWebhookByConfigIdResponse, PostEmailMailgunWebhookByConfigIdResponses, PostEmailProvidersData, PostEmailProvidersError, PostEmailProvidersErrors, PostEmailProvidersResponse, PostEmailProvidersResponses, PostFetchProvidersData, PostFetchProvidersError, PostFetchProvidersErrors, PostFetchProvidersResponse, PostFetchProvidersResponses, PostMemoryProvidersData, PostMemoryProvidersError, PostMemoryProvidersErrors, PostMemoryProvidersResponse, PostMemoryProvidersResponses, PostModelsByIdTestData, PostModelsByIdTestError, PostModelsByIdTestErrors, PostModelsByIdTestResponse, PostModelsByIdTestResponses, PostModelsData, PostModelsError, PostModelsErrors, PostModelsResponse, PostModelsResponses, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsError, PostProvidersByIdImportModelsErrors, PostProvidersByIdImportModelsResponse, PostProvidersByIdImportModelsResponses, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollError, PostProvidersByIdOauthPollErrors, PostProvidersByIdOauthPollResponse, PostProvidersByIdOauthPollResponses, PostProvidersByIdTestData, PostProvidersByIdTestError, PostProvidersByIdTestErrors, PostProvidersByIdTestResponse, PostProvidersByIdTestResponses, PostProvidersData, PostProvidersError, PostProvidersErrors, PostProvidersFromTemplateData, PostProvidersFromTemplateError, PostProvidersFromTemplateErrors, PostProvidersFromTemplateResponse, PostProvidersFromTemplateResponses, PostProvidersResponse, PostProvidersResponses, PostSearchProvidersData, PostSearchProvidersError, PostSearchProvidersErrors, PostSearchProvidersResponse, PostSearchProvidersResponses, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestError, PostSpeechModelsByIdTestErrors, PostSpeechModelsByIdTestResponses, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsError, PostSpeechProvidersByIdImportModelsErrors, PostSpeechProvidersByIdImportModelsResponse, PostSpeechProvidersByIdImportModelsResponses, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestError, PostTranscriptionModelsByIdTestErrors, PostTranscriptionModelsByIdTestResponse, PostTranscriptionModelsByIdTestResponses, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsError, PostTranscriptionProvidersByIdImportModelsErrors, PostTranscriptionProvidersByIdImportModelsResponse, PostTranscriptionProvidersByIdImportModelsResponses, PostUsersData, PostUsersError, PostUsersErrors, PostUsersMeChannelLinksData, PostUsersMeChannelLinksError, PostUsersMeChannelLinksErrors, PostUsersMeChannelLinksResponse, PostUsersMeChannelLinksResponses, PostUsersMeRuntimesData, PostUsersMeRuntimesError, PostUsersMeRuntimesErrors, PostUsersMeRuntimesResponse, PostUsersMeRuntimesResponses, PostUsersResponse, PostUsersResponses, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsError, PostVideoProvidersByIdImportModelsErrors, PostVideoProvidersByIdImportModelsResponse, PostVideoProvidersByIdImportModelsResponses, ProvidersCountResponse, ProvidersCreateFromTemplateRequest, ProvidersCreateRequest, ProvidersGetResponse, ProvidersImportModelsRequest, ProvidersImportModelsResponse, ProvidersOAuthAccount, ProvidersOAuthAuthorizeResponse, ProvidersOAuthDeviceStatus, ProvidersOAuthStatus, ProvidersTestResponse, ProvidersTestStatus, ProvidersUpdateRequest, ProvidertemplatesGetResponse, ProvidertemplatesModelResponse, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectError, PutBotsByBotIdAclDefaultEffectErrors, PutBotsByBotIdAclDefaultEffectResponses, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdError, PutBotsByBotIdAclRulesByRuleIdErrors, PutBotsByBotIdAclRulesByRuleIdResponse, PutBotsByBotIdAclRulesByRuleIdResponses, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialError, PutBotsByBotIdAgentsByIdCredentialErrors, PutBotsByBotIdAgentsByIdCredentialResponse, PutBotsByBotIdAgentsByIdCredentialResponses, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsError, PutBotsByBotIdContainerMetricsErrors, PutBotsByBotIdContainerMetricsResponse, PutBotsByBotIdContainerMetricsResponses, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdError, PutBotsByBotIdEmailBindingsByIdErrors, PutBotsByBotIdEmailBindingsByIdResponse, PutBotsByBotIdEmailBindingsByIdResponses, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdError, PutBotsByBotIdMcpByIdErrors, PutBotsByBotIdMcpByIdResponse, PutBotsByBotIdMcpByIdResponses, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportError, PutBotsByBotIdMcpOpsImportErrors, PutBotsByBotIdMcpOpsImportResponse, PutBotsByBotIdMcpOpsImportResponses, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdError, PutBotsByBotIdMemoryByMemoryIdErrors, PutBotsByBotIdMemoryByMemoryIdResponse, PutBotsByBotIdMemoryByMemoryIdResponses, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdError, PutBotsByBotIdScheduleByIdErrors, PutBotsByBotIdScheduleByIdResponse, PutBotsByBotIdScheduleByIdResponses, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsError, PutBotsByBotIdSettingsErrors, PutBotsByBotIdSettingsResponse, PutBotsByBotIdSettingsResponses, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdError, PutBotsByBotIdUserAccessByGrantIdErrors, PutBotsByBotIdUserAccessByGrantIdResponse, PutBotsByBotIdUserAccessByGrantIdResponses, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalError, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalErrors, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalResponses, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryError, PutBotsByBotIdWorkspaceTargetsPrimaryErrors, PutBotsByBotIdWorkspaceTargetsPrimaryResponses, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdErrors, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponse, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponses, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformError, PutBotsByIdChannelByPlatformErrors, PutBotsByIdChannelByPlatformResponse, PutBotsByIdChannelByPlatformResponses, PutBotsByIdData, PutBotsByIdError, PutBotsByIdErrors, PutBotsByIdOwnerData, PutBotsByIdOwnerError, PutBotsByIdOwnerErrors, PutBotsByIdOwnerResponse, PutBotsByIdOwnerResponses, PutBotsByIdResponse, PutBotsByIdResponses, PutEmailProvidersByIdData, PutEmailProvidersByIdError, PutEmailProvidersByIdErrors, PutEmailProvidersByIdResponse, PutEmailProvidersByIdResponses, PutFetchProvidersByIdData, PutFetchProvidersByIdError, PutFetchProvidersByIdErrors, PutFetchProvidersByIdResponse, PutFetchProvidersByIdResponses, PutMemoryProvidersByIdData, PutMemoryProvidersByIdError, PutMemoryProvidersByIdErrors, PutMemoryProvidersByIdResponse, PutMemoryProvidersByIdResponses, PutModelsByIdData, PutModelsByIdError, PutModelsByIdErrors, PutModelsByIdResponse, PutModelsByIdResponses, PutModelsModelByModelIdData, PutModelsModelByModelIdError, PutModelsModelByModelIdErrors, PutModelsModelByModelIdResponse, PutModelsModelByModelIdResponses, PutProvidersByIdData, PutProvidersByIdError, PutProvidersByIdErrors, PutProvidersByIdResponse, PutProvidersByIdResponses, PutSearchProvidersByIdData, PutSearchProvidersByIdError, PutSearchProvidersByIdErrors, PutSearchProvidersByIdResponse, PutSearchProvidersByIdResponses, PutSpeechModelsByIdData, PutSpeechModelsByIdError, PutSpeechModelsByIdErrors, PutSpeechModelsByIdResponse, PutSpeechModelsByIdResponses, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdError, PutTranscriptionModelsByIdErrors, PutTranscriptionModelsByIdResponse, PutTranscriptionModelsByIdResponses, PutUsersByIdData, PutUsersByIdError, PutUsersByIdErrors, PutUsersByIdResponse, PutUsersByIdResponses, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformError, PutUsersMeChannelsByPlatformErrors, PutUsersMeChannelsByPlatformResponse, PutUsersMeChannelsByPlatformResponses, PutUsersMeData, PutUsersMeError, PutUsersMeErrors, PutUsersMePasswordData, PutUsersMePasswordError, PutUsersMePasswordErrors, PutUsersMePasswordResponses, PutUsersMeResponse, PutUsersMeResponses, PutVideoModelsByIdData, PutVideoModelsByIdError, PutVideoModelsByIdErrors, PutVideoModelsByIdResponse, PutVideoModelsByIdResponses, ReasoningOptions, ScheduleCreateRequest, ScheduleExecutionConfig, ScheduleListLogsResponse, ScheduleListResponse, ScheduleLog, ScheduleNullableInt, ScheduleSchedule, ScheduleUpdateRequest, SearchprovidersCreateRequest, SearchprovidersGetResponse, SearchprovidersProviderConfigSchema, SearchprovidersProviderFieldSchema, SearchprovidersProviderMeta, SearchprovidersProviderName, SearchprovidersUpdateRequest, SessionSession, SettingsSettings, SettingsToolApprovalConfig, SettingsToolApprovalExecPolicy, SettingsToolApprovalFilePolicy, SettingsToolApprovalMode, SettingsUpsertRequest, SkillpackagesInstallation, SkillsSafeCatalogItem, SupermarketUninstallPackageResponse, UserinputUiAnswer, UserinputUiOption, UserinputUiQuestion, UserruntimeCreateRuntimeRequest, UserruntimeRuntime, VideoConfigSchema, VideoFieldSchema, VideoImportModelsResponse, VideoModelInfo, VideoModelResponse, VideoProviderMetaResponse, VideoProviderResponse, VideoUpdateModelRequest, WebhooktunnelStatus, WorkdirCreateRequest, WorkdirUpdateRequest, WorkdirWorkdir, WorkdirWorkdirsResponse, WorkspaceSetPrimaryWorkspaceTargetRequest, WorkspaceUpdateWorkspaceTargetToolApprovalRequest, WorkspaceWorkspaceTarget, WorkspaceWorkspaceTargetGrant, WorkspaceWorkspaceTargetGrantsResponse, WorkspaceWorkspaceTargetsResponse, WorkspaceWorkspaceTargetToolApproval } from './types.gen'; +export { deleteBotsByBotIdAclRulesByRuleId, deleteBotsByBotIdAcpRuntimesByRuntimeId, deleteBotsByBotIdAgentsById, deleteBotsByBotIdAgentsByIdCredential, deleteBotsByBotIdChannelManagersByChannelIdentityId, deleteBotsByBotIdCompactionLogs, deleteBotsByBotIdConnectorsByConnectionId, deleteBotsByBotIdContainer, deleteBotsByBotIdContainerBrowserSessionsBySessionId, deleteBotsByBotIdContainerDisplaySessionsBySessionId, deleteBotsByBotIdContainerSkills, deleteBotsByBotIdEmailBindingsById, deleteBotsByBotIdMcpById, deleteBotsByBotIdMcpByIdOauthToken, deleteBotsByBotIdMemory, deleteBotsByBotIdMemoryById, deleteBotsByBotIdMessages, deleteBotsByBotIdScheduleById, deleteBotsByBotIdScheduleLogs, deleteBotsByBotIdSessionsBySessionId, deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId, deleteBotsByBotIdSettings, deleteBotsByBotIdSupermarketPackagesByInstallationId, deleteBotsByBotIdUserAccessByGrantId, deleteBotsByBotIdWorkdirsByWorkdirId, deleteBotsByBotIdWorkspaceTargetsByTargetId, deleteBotsById, deleteBotsByIdChannelByPlatform, deleteEmailProvidersById, deleteEmailProvidersByIdOauthToken, deleteFetchProvidersById, deleteMemoryProvidersById, deleteModelsById, deleteModelsModelByModelId, deleteProvidersById, deleteProvidersByIdOauthToken, deleteSearchProvidersById, deleteUsersById, deleteUsersMeChannelIdentitiesByChannelIdentityId, deleteUsersMeRuntimesById, getAcpProfiles, getBots, getBotsByBotIdAclChannelIdentities, getBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversations, getBotsByBotIdAclChannelTypesByChannelTypeConversations, getBotsByBotIdAclDefaultEffect, getBotsByBotIdAclRules, getBotsByBotIdAcpRuntimesByRuntimeId, getBotsByBotIdAgents, getBotsByBotIdAgentsById, getBotsByBotIdAgentsByIdCredential, getBotsByBotIdAgentsByIdModels, getBotsByBotIdBackupSummary, getBotsByBotIdChannelManagers, getBotsByBotIdCompactionLogs, getBotsByBotIdConnectors, getBotsByBotIdConnectorsByConnectionId, getBotsByBotIdContainer, getBotsByBotIdContainerDisplay, getBotsByBotIdContainerDisplaySessions, getBotsByBotIdContainerFs, getBotsByBotIdContainerFsDownload, getBotsByBotIdContainerFsList, getBotsByBotIdContainerFsRead, getBotsByBotIdContainerMetrics, getBotsByBotIdContainerSkills, getBotsByBotIdContainerSnapshots, getBotsByBotIdContainerTerminal, getBotsByBotIdContainerTerminalWs, getBotsByBotIdEmailBindings, getBotsByBotIdEmailOutbox, getBotsByBotIdEmailOutboxById, getBotsByBotIdHooksEvents, getBotsByBotIdMcp, getBotsByBotIdMcpById, getBotsByBotIdMcpByIdOauthStatus, getBotsByBotIdMcpOpsExport, getBotsByBotIdMemory, getBotsByBotIdMemoryGraph, getBotsByBotIdMemoryStatus, getBotsByBotIdMemoryUsage, getBotsByBotIdMessages, getBotsByBotIdMessagesLocate, getBotsByBotIdSchedule, getBotsByBotIdScheduleById, getBotsByBotIdScheduleByIdLogs, getBotsByBotIdScheduleLogs, getBotsByBotIdSessions, getBotsByBotIdSessionsBySessionId, getBotsByBotIdSessionsBySessionIdAcpRuntime, getBotsByBotIdSessionsBySessionIdContextLifecycle, getBotsByBotIdSessionsBySessionIdFollowUpQueue, getBotsByBotIdSessionsBySessionIdQueue, getBotsByBotIdSessionsBySessionIdStatus, getBotsByBotIdSessionsBySessionIdSteerQueue, getBotsByBotIdSessionsEvents, getBotsByBotIdSessionsModelPreferenceSeed, getBotsByBotIdSettings, getBotsByBotIdSkillsCatalog, getBotsByBotIdSupermarketPackages, getBotsByBotIdTokenUsage, getBotsByBotIdTokenUsageRecords, getBotsByBotIdUserAccess, getBotsByBotIdUserAccessCandidates, getBotsByBotIdWebStream, getBotsByBotIdWebWs, getBotsByBotIdWorkdirs, getBotsByBotIdWorkspaceTargets, getBotsById, getBotsByIdChannelByPlatform, getBotsByIdChecks, getBotsNameAvailability, getBotsUserAccessCandidates, getChannels, getChannelsByPlatform, getConnectorsCatalog, getEmailOauthCallback, getEmailProviders, getEmailProvidersById, getEmailProvidersByIdOauthAuthorize, getEmailProvidersByIdOauthStatus, getEmailProvidersMeta, getFetchProviders, getFetchProvidersById, getFetchProvidersMeta, getMemoryProviders, getMemoryProvidersById, getMemoryProvidersByIdStatus, getMemoryProvidersMeta, getModels, getModelsById, getModelsCount, getModelsModelByModelId, getOauthMcpCallback, getPing, getProviders, getProvidersById, getProvidersByIdModels, getProvidersByIdOauthAuthorize, getProvidersByIdOauthStatus, getProvidersCount, getProvidersNameByName, getProvidersOauthCallback, getProviderTemplates, getProviderTemplatesById, getSearchProviders, getSearchProvidersById, getSearchProvidersMeta, getSpeechModels, getSpeechModelsById, getSpeechModelsByIdCapabilities, getSpeechProviders, getSpeechProvidersById, getSpeechProvidersByIdModels, getSpeechProvidersMeta, getSupermarketArtifactsIconByDigest, getSupermarketPackages, getSupermarketRegistries, getSupermarketRegistriesByRegistryIdCategories, getSupermarketRegistriesByRegistryIdPackages, getSupermarketRegistriesByRegistryIdPackagesByPackageId, getSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevision, getSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillId, getSupermarketSkills, getTranscriptionModels, getTranscriptionModelsById, getTranscriptionModelsByIdCapabilities, getTranscriptionProviders, getTranscriptionProvidersById, getTranscriptionProvidersByIdModels, getTranscriptionProvidersMeta, getUsers, getUsersById, getUsersMe, getUsersMeChannelIdentities, getUsersMeChannelsByPlatform, getUsersMeComputerAccess, getUsersMeRuntimes, getVideoModels, getVideoModelsById, getVideoProviders, getVideoProvidersById, getVideoProvidersByIdModels, getVideoProvidersMeta, getWebhookTunnelStatus, type Options, patchBotsByBotIdAcpRuntimesByRuntimeIdMode, patchBotsByBotIdAcpRuntimesByRuntimeIdModel, patchBotsByBotIdAcpRuntimesByRuntimeIdReasoning, patchBotsByBotIdAgentsById, patchBotsByBotIdConnectorsByConnectionId, patchBotsByBotIdSessionsBySessionId, patchBotsByBotIdSessionsBySessionIdAcpRuntimeMode, patchBotsByBotIdSessionsBySessionIdAcpRuntimeModel, patchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoning, patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId, patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId, patchBotsByBotIdWorkdirsByWorkdirId, patchBotsByIdChannelByPlatformStatus, postAuthLogin, postAuthRefresh, postBots, postBotsBackupImport, postBotsBackupImportPreview, postBotsByBotIdAclRules, postBotsByBotIdAcpRuntimes, postBotsByBotIdAgents, postBotsByBotIdAgentsByIdCodexLoginDeviceAuthorize, postBotsByBotIdAgentsByIdCodexLoginDeviceCancel, postBotsByBotIdAgentsByIdCodexLoginDevicePoll, postBotsByBotIdBackupExport, postBotsByBotIdChannelManagers, postBotsByBotIdConnectorsApiKey, postBotsByBotIdConnectorsByConnectionIdReauth, postBotsByBotIdConnectorsOauth, postBotsByBotIdContainer, postBotsByBotIdContainerBrowserSessions, postBotsByBotIdContainerBrowserSessionsBySessionIdKeepalive, postBotsByBotIdContainerDataRestore, postBotsByBotIdContainerDisplayPrepare, postBotsByBotIdContainerDisplayWebrtcOffer, postBotsByBotIdContainerFsArchive, postBotsByBotIdContainerFsDelete, postBotsByBotIdContainerFsExtract, postBotsByBotIdContainerFsMkdir, postBotsByBotIdContainerFsRename, postBotsByBotIdContainerFsUpload, postBotsByBotIdContainerFsWrite, postBotsByBotIdContainerSkills, postBotsByBotIdContainerSkillsActions, postBotsByBotIdContainerSnapshots, postBotsByBotIdContainerSnapshotsRollback, postBotsByBotIdContainerStart, postBotsByBotIdContainerStop, postBotsByBotIdEmailBindings, postBotsByBotIdHooksTest, postBotsByBotIdMcp, postBotsByBotIdMcpByIdOauthAuthorize, postBotsByBotIdMcpByIdOauthDiscover, postBotsByBotIdMcpByIdOauthExchange, postBotsByBotIdMcpByIdProbe, postBotsByBotIdMcpOpsBatchDelete, postBotsByBotIdMcpStdio, postBotsByBotIdMcpStdioByConnectionId, postBotsByBotIdMemory, postBotsByBotIdMemoryCompact, postBotsByBotIdMemoryIngest, postBotsByBotIdMemoryRebuild, postBotsByBotIdMemorySearch, postBotsByBotIdQuickActionsExecute, postBotsByBotIdSchedule, postBotsByBotIdSessions, postBotsByBotIdSessionsBySessionIdAcpRuntime, postBotsByBotIdSessionsBySessionIdCompact, postBotsByBotIdSessionsBySessionIdFollowUpQueue, postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer, postBotsByBotIdSessionsBySessionIdFork, postBotsByBotIdSessionsBySessionIdSteerQueue, postBotsByBotIdSettings, postBotsByBotIdSupermarketInstallPackage, postBotsByBotIdToolApprovalsByApprovalIdApprove, postBotsByBotIdToolApprovalsByApprovalIdReject, postBotsByBotIdTools, postBotsByBotIdTtsSynthesize, postBotsByBotIdUserAccess, postBotsByBotIdWebMessages, postBotsByBotIdWorkdirs, postBotsByIdChannelByPlatformSend, postBotsByIdChannelByPlatformSendChat, postBotsByIdChannelByPlatformWebhookEndpoint, postEmailMailgunWebhookByConfigId, postEmailProviders, postFetchProviders, postMemoryProviders, postModels, postModelsByIdTest, postProviders, postProvidersByIdImportModels, postProvidersByIdOauthPoll, postProvidersByIdTest, postProvidersFromTemplate, postSearchProviders, postSpeechModelsByIdTest, postSpeechProvidersByIdImportModels, postTranscriptionModelsByIdTest, postTranscriptionProvidersByIdImportModels, postUsers, postUsersMeChannelLinks, postUsersMeRuntimes, postVideoProvidersByIdImportModels, putBotsByBotIdAclDefaultEffect, putBotsByBotIdAclRulesByRuleId, putBotsByBotIdAgentsByIdCredential, putBotsByBotIdContainerMetrics, putBotsByBotIdEmailBindingsById, putBotsByBotIdMcpById, putBotsByBotIdMcpOpsImport, putBotsByBotIdMemoryByMemoryId, putBotsByBotIdScheduleById, putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder, putBotsByBotIdSessionsBySessionIdSteerQueueReorder, putBotsByBotIdSettings, putBotsByBotIdUserAccessByGrantId, putBotsByBotIdWorkspaceTargetsByTargetIdToolApproval, putBotsByBotIdWorkspaceTargetsPrimary, putBotsByBotIdWorkspaceTargetsRemotesByRuntimeId, putBotsById, putBotsByIdChannelByPlatform, putBotsByIdOwner, putEmailProvidersById, putFetchProvidersById, putMemoryProvidersById, putModelsById, putModelsModelByModelId, putProvidersById, putSearchProvidersById, putSpeechModelsById, putTranscriptionModelsById, putUsersById, putUsersMe, putUsersMeChannelsByPlatform, putUsersMePassword, putVideoModelsById } from './sdk.gen'; +export type { AccountsAccount, AccountsCreateAccountRequest, AccountsListAccountsResponse, AccountsUpdateAccountRequest, AccountsUpdatePasswordRequest, AccountsUpdateProfileMetadata, AccountsUpdateProfileRequest, AclChannelIdentityCandidate, AclChannelIdentityCandidateListResponse, AclCreateRuleRequest, AclDefaultEffectResponse, AclListRulesResponse, AclObservedConversationCandidate, AclObservedConversationCandidateListResponse, AclRule, AclSourceScope, AclUpdateRuleRequest, AcpagentRuntimeStatus, AcpclientAvailableCommandInfo, AcpclientModeInfo, AcpclientModelInfo, AcpclientModelState, AcpclientModeState, AcpclientReasoningEffortInfo, AcpclientReasoningState, AcpprofileManagedField, AcpprofileProfilesResponse, AcpprofilePublicProfile, AdaptersCompactResult, AdaptersDeleteResponse, AdaptersHealthStatus, AdaptersIngestResult, AdaptersMemoryCompactCapability, AdaptersMemoryItem, AdaptersMemoryStatusResponse, AdaptersMessage, AdaptersProviderCollectionStatus, AdaptersProviderConfigSchema, AdaptersProviderCreateRequest, AdaptersProviderFieldSchema, AdaptersProviderGetResponse, AdaptersProviderMeta, AdaptersProviderStatusResponse, AdaptersProviderType, AdaptersProviderUpdateRequest, AdaptersRebuildResult, AdaptersSearchResponse, AdaptersUsageResponse, AgentcredentialPublicCredential, ApperrorProblem, AudioConfigSchema, AudioFieldSchema, AudioImportModelsResponse, AudioModelCapabilities, AudioModelInfo, AudioParamConstraint, AudioProviderMetaResponse, AudioSpeechModelResponse, AudioSpeechProviderResponse, AudioTestSynthesizeRequest, AudioTestTranscriptionResponse, AudioTranscriptionModelResponse, AudioTranscriptionWord, AudioUpdateSpeechModelRequest, AudioVoiceInfo, BotagentsBotAgent, BotagentsCreateRequest, BotagentsListResponse, BotagentsUpdateRequest, BotbackupExportRequest, BotbackupImportMode, BotbackupImportResult, BotbackupManifest, BotbackupManifestEntry, BotbackupManifestOptions, BotbackupPreviewResult, BotbackupProfilePreview, BotbackupRestorePlan, BotbackupSection, BotbackupSectionSummary, BotbackupSummaryResult, BotsBot, BotsBotCheck, BotsCreateBotRequest, BotsCreateUserGrantRequest, BotsListBotsResponse, BotsListChecksResponse, BotsNameAvailability, BotsTransferBotRequest, BotsUpdateBotRequest, BotsUpdateUserGrantRequest, BotsUserGrant, ChannelaccessBinding, ChannelaccessIssueLinkCodeRequest, ChannelaccessLinkCode, ChannelaccessListBindingsResponse, ChannelaccessListManagersResponse, ChannelaccessManager, ChannelaccessSetManagerRequest, ChannelAction, ChannelAttachment, ChannelAttachmentType, ChannelChannelCapabilities, ChannelChannelConfig, ChannelChannelIdentityBinding, ChannelChannelType, ChannelConfigSchema, ChannelFieldSchema, ChannelFieldType, ChannelForwardRef, ChannelMessage, ChannelMessageFormat, ChannelMessagePart, ChannelMessagePartType, ChannelMessageTextStyle, ChannelReplyRef, ChannelSendRequest, ChannelSetWebhookEndpointRequest, ChannelSetWebhookEndpointResponse, ChannelTargetHint, ChannelTargetSpec, ChannelThreadRef, ChannelUpdateChannelStatusRequest, ChannelUpsertChannelIdentityConfigRequest, ChannelUpsertConfigRequest, ClientOptions, CompactionListLogsResponse, CompactionLog, ConnectitAuthMethod, ConnectitConnector, ConnectitCredentialField, ConnectitOAuthAuthorization, ConnectorsConnector, ConnectorsListResponse, ContextfragCacheClass, ContextfragCacheComparison, ContextfragCacheUsageRecord, ContextfragContentRange, ContextfragContextBudgetPlan, ContextfragContextRef, ContextfragKind, ContextfragKindBreakdown, ContextfragLifecycleSnapshot, ContextfragManifestCounts, ContextfragManifestView, ContextfragMemoryRecallQueryTrace, ContextfragMemoryRecallResultTrace, ContextfragMemoryRecallTrace, ContextfragMutationKind, ContextfragMutationRecord, ContextfragRefDurability, ContextfragRetentionTier, ContextfragSelectionDecision, ContextfragSelectionDecisionKind, ContextfragSelectionTrace, ContextfragSlot, ContextfragStepSnapshot, ContextfragToolDefAccounting, ContextfragTrustBreakdown, ContextfragTrustLevel, ConversationSkillActivation, ConversationSkillActivationSkill, ConversationUiAttachment, ConversationUiBackgroundTask, ConversationUiExecutionLocation, ConversationUiForwardRef, ConversationUiMessage, ConversationUiMessageType, ConversationUiReasoningTiming, ConversationUiReplyRef, ConversationUiToolApproval, ConversationUiToolApprovalOption, ConversationUiTurn, ConversationUiUserInput, DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdError, DeleteBotsByBotIdAclRulesByRuleIdErrors, DeleteBotsByBotIdAclRulesByRuleIdResponses, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdError, DeleteBotsByBotIdAcpRuntimesByRuntimeIdErrors, DeleteBotsByBotIdAcpRuntimesByRuntimeIdResponses, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialError, DeleteBotsByBotIdAgentsByIdCredentialErrors, DeleteBotsByBotIdAgentsByIdCredentialResponses, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdError, DeleteBotsByBotIdAgentsByIdErrors, DeleteBotsByBotIdAgentsByIdResponses, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdError, DeleteBotsByBotIdChannelManagersByChannelIdentityIdErrors, DeleteBotsByBotIdChannelManagersByChannelIdentityIdResponses, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsError, DeleteBotsByBotIdCompactionLogsErrors, DeleteBotsByBotIdCompactionLogsResponses, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdError, DeleteBotsByBotIdConnectorsByConnectionIdErrors, DeleteBotsByBotIdConnectorsByConnectionIdResponses, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdError, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdErrors, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdResponses, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdError, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdErrors, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdResponses, DeleteBotsByBotIdContainerError, DeleteBotsByBotIdContainerErrors, DeleteBotsByBotIdContainerResponses, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsError, DeleteBotsByBotIdContainerSkillsErrors, DeleteBotsByBotIdContainerSkillsResponse, DeleteBotsByBotIdContainerSkillsResponses, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdError, DeleteBotsByBotIdEmailBindingsByIdErrors, DeleteBotsByBotIdEmailBindingsByIdResponses, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdError, DeleteBotsByBotIdMcpByIdErrors, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenError, DeleteBotsByBotIdMcpByIdOauthTokenErrors, DeleteBotsByBotIdMcpByIdOauthTokenResponses, DeleteBotsByBotIdMcpByIdResponses, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdError, DeleteBotsByBotIdMemoryByIdErrors, DeleteBotsByBotIdMemoryByIdResponse, DeleteBotsByBotIdMemoryByIdResponses, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryError, DeleteBotsByBotIdMemoryErrors, DeleteBotsByBotIdMemoryResponse, DeleteBotsByBotIdMemoryResponses, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesError, DeleteBotsByBotIdMessagesErrors, DeleteBotsByBotIdMessagesResponses, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdError, DeleteBotsByBotIdScheduleByIdErrors, DeleteBotsByBotIdScheduleByIdResponses, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsError, DeleteBotsByBotIdScheduleLogsErrors, DeleteBotsByBotIdScheduleLogsResponses, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdError, DeleteBotsByBotIdSessionsBySessionIdErrors, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdErrors, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponses, DeleteBotsByBotIdSessionsBySessionIdResponses, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdErrors, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponses, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsError, DeleteBotsByBotIdSettingsErrors, DeleteBotsByBotIdSettingsResponses, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdError, DeleteBotsByBotIdSupermarketPackagesByInstallationIdErrors, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponse, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponses, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdError, DeleteBotsByBotIdUserAccessByGrantIdErrors, DeleteBotsByBotIdUserAccessByGrantIdResponses, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdError, DeleteBotsByBotIdWorkdirsByWorkdirIdErrors, DeleteBotsByBotIdWorkdirsByWorkdirIdResponses, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdError, DeleteBotsByBotIdWorkspaceTargetsByTargetIdErrors, DeleteBotsByBotIdWorkspaceTargetsByTargetIdResponses, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformError, DeleteBotsByIdChannelByPlatformErrors, DeleteBotsByIdChannelByPlatformResponses, DeleteBotsByIdData, DeleteBotsByIdError, DeleteBotsByIdErrors, DeleteBotsByIdResponse, DeleteBotsByIdResponses, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdError, DeleteEmailProvidersByIdErrors, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenError, DeleteEmailProvidersByIdOauthTokenErrors, DeleteEmailProvidersByIdOauthTokenResponses, DeleteEmailProvidersByIdResponses, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdError, DeleteFetchProvidersByIdErrors, DeleteFetchProvidersByIdResponses, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdError, DeleteMemoryProvidersByIdErrors, DeleteMemoryProvidersByIdResponses, DeleteModelsByIdData, DeleteModelsByIdError, DeleteModelsByIdErrors, DeleteModelsByIdResponses, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdError, DeleteModelsModelByModelIdErrors, DeleteModelsModelByModelIdResponses, DeleteProvidersByIdData, DeleteProvidersByIdError, DeleteProvidersByIdErrors, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenError, DeleteProvidersByIdOauthTokenErrors, DeleteProvidersByIdOauthTokenResponses, DeleteProvidersByIdResponses, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdError, DeleteSearchProvidersByIdErrors, DeleteSearchProvidersByIdResponses, DeleteUsersByIdData, DeleteUsersByIdError, DeleteUsersByIdErrors, DeleteUsersByIdResponses, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdError, DeleteUsersMeChannelIdentitiesByChannelIdentityIdErrors, DeleteUsersMeChannelIdentitiesByChannelIdentityIdResponses, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdError, DeleteUsersMeRuntimesByIdErrors, DeleteUsersMeRuntimesByIdResponses, DisplaySessionInfo, EmailBindingResponse, EmailConfigSchema, EmailCreateBindingRequest, EmailCreateProviderRequest, EmailFieldSchema, EmailOutboxItemResponse, EmailProviderMeta, EmailProviderResponse, EmailUpdateBindingRequest, EmailUpdateProviderRequest, ExternalagentCodexDeviceLoginAuthorizeResponse, ExternalagentCodexDeviceLoginPollRequest, ExternalagentCodexDeviceLoginPollResponse, ExternalModelCatalog, ExternalModelOption, ExternalReasoningEffortOption, FetchprovidersCreateRequest, FetchprovidersGetResponse, FetchprovidersProviderConfigSchema, FetchprovidersProviderFieldSchema, FetchprovidersProviderMeta, FetchprovidersProviderName, FetchprovidersUpdateRequest, GetAcpProfilesData, GetAcpProfilesResponse, GetAcpProfilesResponses, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsError, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsErrors, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponse, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponses, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesError, GetBotsByBotIdAclChannelIdentitiesErrors, GetBotsByBotIdAclChannelIdentitiesResponse, GetBotsByBotIdAclChannelIdentitiesResponses, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsError, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsErrors, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponse, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponses, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectError, GetBotsByBotIdAclDefaultEffectErrors, GetBotsByBotIdAclDefaultEffectResponse, GetBotsByBotIdAclDefaultEffectResponses, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesError, GetBotsByBotIdAclRulesErrors, GetBotsByBotIdAclRulesResponse, GetBotsByBotIdAclRulesResponses, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdError, GetBotsByBotIdAcpRuntimesByRuntimeIdErrors, GetBotsByBotIdAcpRuntimesByRuntimeIdResponse, GetBotsByBotIdAcpRuntimesByRuntimeIdResponses, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialError, GetBotsByBotIdAgentsByIdCredentialErrors, GetBotsByBotIdAgentsByIdCredentialResponse, GetBotsByBotIdAgentsByIdCredentialResponses, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdError, GetBotsByBotIdAgentsByIdErrors, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsError, GetBotsByBotIdAgentsByIdModelsErrors, GetBotsByBotIdAgentsByIdModelsResponse, GetBotsByBotIdAgentsByIdModelsResponses, GetBotsByBotIdAgentsByIdResponse, GetBotsByBotIdAgentsByIdResponses, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsError, GetBotsByBotIdAgentsErrors, GetBotsByBotIdAgentsResponse, GetBotsByBotIdAgentsResponses, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryError, GetBotsByBotIdBackupSummaryErrors, GetBotsByBotIdBackupSummaryResponse, GetBotsByBotIdBackupSummaryResponses, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersError, GetBotsByBotIdChannelManagersErrors, GetBotsByBotIdChannelManagersResponse, GetBotsByBotIdChannelManagersResponses, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsError, GetBotsByBotIdCompactionLogsErrors, GetBotsByBotIdCompactionLogsResponse, GetBotsByBotIdCompactionLogsResponses, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdError, GetBotsByBotIdConnectorsByConnectionIdErrors, GetBotsByBotIdConnectorsByConnectionIdResponse, GetBotsByBotIdConnectorsByConnectionIdResponses, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsError, GetBotsByBotIdConnectorsErrors, GetBotsByBotIdConnectorsResponse, GetBotsByBotIdConnectorsResponses, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayError, GetBotsByBotIdContainerDisplayErrors, GetBotsByBotIdContainerDisplayResponse, GetBotsByBotIdContainerDisplayResponses, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsError, GetBotsByBotIdContainerDisplaySessionsErrors, GetBotsByBotIdContainerDisplaySessionsResponse, GetBotsByBotIdContainerDisplaySessionsResponses, GetBotsByBotIdContainerError, GetBotsByBotIdContainerErrors, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadError, GetBotsByBotIdContainerFsDownloadErrors, GetBotsByBotIdContainerFsDownloadResponses, GetBotsByBotIdContainerFsError, GetBotsByBotIdContainerFsErrors, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListError, GetBotsByBotIdContainerFsListErrors, GetBotsByBotIdContainerFsListResponse, GetBotsByBotIdContainerFsListResponses, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadError, GetBotsByBotIdContainerFsReadErrors, GetBotsByBotIdContainerFsReadResponse, GetBotsByBotIdContainerFsReadResponses, GetBotsByBotIdContainerFsResponse, GetBotsByBotIdContainerFsResponses, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsError, GetBotsByBotIdContainerMetricsErrors, GetBotsByBotIdContainerMetricsResponse, GetBotsByBotIdContainerMetricsResponses, GetBotsByBotIdContainerResponse, GetBotsByBotIdContainerResponses, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsError, GetBotsByBotIdContainerSkillsErrors, GetBotsByBotIdContainerSkillsResponse, GetBotsByBotIdContainerSkillsResponses, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsError, GetBotsByBotIdContainerSnapshotsErrors, GetBotsByBotIdContainerSnapshotsResponse, GetBotsByBotIdContainerSnapshotsResponses, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalError, GetBotsByBotIdContainerTerminalErrors, GetBotsByBotIdContainerTerminalResponse, GetBotsByBotIdContainerTerminalResponses, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsError, GetBotsByBotIdContainerTerminalWsErrors, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsError, GetBotsByBotIdEmailBindingsErrors, GetBotsByBotIdEmailBindingsResponse, GetBotsByBotIdEmailBindingsResponses, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdError, GetBotsByBotIdEmailOutboxByIdErrors, GetBotsByBotIdEmailOutboxByIdResponse, GetBotsByBotIdEmailOutboxByIdResponses, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxError, GetBotsByBotIdEmailOutboxErrors, GetBotsByBotIdEmailOutboxResponse, GetBotsByBotIdEmailOutboxResponses, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsError, GetBotsByBotIdHooksEventsErrors, GetBotsByBotIdHooksEventsResponse, GetBotsByBotIdHooksEventsResponses, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdError, GetBotsByBotIdMcpByIdErrors, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusError, GetBotsByBotIdMcpByIdOauthStatusErrors, GetBotsByBotIdMcpByIdOauthStatusResponse, GetBotsByBotIdMcpByIdOauthStatusResponses, GetBotsByBotIdMcpByIdResponse, GetBotsByBotIdMcpByIdResponses, GetBotsByBotIdMcpData, GetBotsByBotIdMcpError, GetBotsByBotIdMcpErrors, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportError, GetBotsByBotIdMcpOpsExportErrors, GetBotsByBotIdMcpOpsExportResponse, GetBotsByBotIdMcpOpsExportResponses, GetBotsByBotIdMcpResponse, GetBotsByBotIdMcpResponses, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryError, GetBotsByBotIdMemoryErrors, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphError, GetBotsByBotIdMemoryGraphErrors, GetBotsByBotIdMemoryGraphResponse, GetBotsByBotIdMemoryGraphResponses, GetBotsByBotIdMemoryResponse, GetBotsByBotIdMemoryResponses, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusError, GetBotsByBotIdMemoryStatusErrors, GetBotsByBotIdMemoryStatusResponse, GetBotsByBotIdMemoryStatusResponses, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageError, GetBotsByBotIdMemoryUsageErrors, GetBotsByBotIdMemoryUsageResponse, GetBotsByBotIdMemoryUsageResponses, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesError, GetBotsByBotIdMessagesErrors, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateError, GetBotsByBotIdMessagesLocateErrors, GetBotsByBotIdMessagesLocateResponse, GetBotsByBotIdMessagesLocateResponses, GetBotsByBotIdMessagesResponse, GetBotsByBotIdMessagesResponses, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdError, GetBotsByBotIdScheduleByIdErrors, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsError, GetBotsByBotIdScheduleByIdLogsErrors, GetBotsByBotIdScheduleByIdLogsResponse, GetBotsByBotIdScheduleByIdLogsResponses, GetBotsByBotIdScheduleByIdResponse, GetBotsByBotIdScheduleByIdResponses, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleError, GetBotsByBotIdScheduleErrors, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsError, GetBotsByBotIdScheduleLogsErrors, GetBotsByBotIdScheduleLogsResponse, GetBotsByBotIdScheduleLogsResponses, GetBotsByBotIdScheduleResponse, GetBotsByBotIdScheduleResponses, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeError, GetBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleError, GetBotsByBotIdSessionsBySessionIdContextLifecycleErrors, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponse, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponses, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdError, GetBotsByBotIdSessionsBySessionIdErrors, GetBotsByBotIdSessionsBySessionIdFollowUpQueueData, GetBotsByBotIdSessionsBySessionIdFollowUpQueueError, GetBotsByBotIdSessionsBySessionIdFollowUpQueueErrors, GetBotsByBotIdSessionsBySessionIdFollowUpQueueResponse, GetBotsByBotIdSessionsBySessionIdFollowUpQueueResponses, GetBotsByBotIdSessionsBySessionIdQueueData, GetBotsByBotIdSessionsBySessionIdQueueError, GetBotsByBotIdSessionsBySessionIdQueueErrors, GetBotsByBotIdSessionsBySessionIdQueueResponse, GetBotsByBotIdSessionsBySessionIdQueueResponses, GetBotsByBotIdSessionsBySessionIdResponse, GetBotsByBotIdSessionsBySessionIdResponses, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusError, GetBotsByBotIdSessionsBySessionIdStatusErrors, GetBotsByBotIdSessionsBySessionIdStatusResponse, GetBotsByBotIdSessionsBySessionIdStatusResponses, GetBotsByBotIdSessionsBySessionIdSteerQueueData, GetBotsByBotIdSessionsBySessionIdSteerQueueError, GetBotsByBotIdSessionsBySessionIdSteerQueueErrors, GetBotsByBotIdSessionsBySessionIdSteerQueueResponse, GetBotsByBotIdSessionsBySessionIdSteerQueueResponses, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsError, GetBotsByBotIdSessionsErrors, GetBotsByBotIdSessionsEventsData, GetBotsByBotIdSessionsEventsError, GetBotsByBotIdSessionsEventsErrors, GetBotsByBotIdSessionsEventsResponse, GetBotsByBotIdSessionsEventsResponses, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedError, GetBotsByBotIdSessionsModelPreferenceSeedErrors, GetBotsByBotIdSessionsModelPreferenceSeedResponse, GetBotsByBotIdSessionsModelPreferenceSeedResponses, GetBotsByBotIdSessionsResponse, GetBotsByBotIdSessionsResponses, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsError, GetBotsByBotIdSettingsErrors, GetBotsByBotIdSettingsResponse, GetBotsByBotIdSettingsResponses, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogError, GetBotsByBotIdSkillsCatalogErrors, GetBotsByBotIdSkillsCatalogResponse, GetBotsByBotIdSkillsCatalogResponses, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesError, GetBotsByBotIdSupermarketPackagesErrors, GetBotsByBotIdSupermarketPackagesResponse, GetBotsByBotIdSupermarketPackagesResponses, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageError, GetBotsByBotIdTokenUsageErrors, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsError, GetBotsByBotIdTokenUsageRecordsErrors, GetBotsByBotIdTokenUsageRecordsResponse, GetBotsByBotIdTokenUsageRecordsResponses, GetBotsByBotIdTokenUsageResponse, GetBotsByBotIdTokenUsageResponses, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesError, GetBotsByBotIdUserAccessCandidatesErrors, GetBotsByBotIdUserAccessCandidatesResponse, GetBotsByBotIdUserAccessCandidatesResponses, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessError, GetBotsByBotIdUserAccessErrors, GetBotsByBotIdUserAccessResponse, GetBotsByBotIdUserAccessResponses, GetBotsByBotIdWebStreamData, GetBotsByBotIdWebStreamError, GetBotsByBotIdWebStreamErrors, GetBotsByBotIdWebStreamResponse, GetBotsByBotIdWebStreamResponses, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsError, GetBotsByBotIdWebWsErrors, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsError, GetBotsByBotIdWorkdirsErrors, GetBotsByBotIdWorkdirsResponse, GetBotsByBotIdWorkdirsResponses, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsError, GetBotsByBotIdWorkspaceTargetsErrors, GetBotsByBotIdWorkspaceTargetsResponse, GetBotsByBotIdWorkspaceTargetsResponses, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformError, GetBotsByIdChannelByPlatformErrors, GetBotsByIdChannelByPlatformResponse, GetBotsByIdChannelByPlatformResponses, GetBotsByIdChecksData, GetBotsByIdChecksError, GetBotsByIdChecksErrors, GetBotsByIdChecksResponse, GetBotsByIdChecksResponses, GetBotsByIdData, GetBotsByIdError, GetBotsByIdErrors, GetBotsByIdResponse, GetBotsByIdResponses, GetBotsData, GetBotsError, GetBotsErrors, GetBotsNameAvailabilityData, GetBotsNameAvailabilityError, GetBotsNameAvailabilityErrors, GetBotsNameAvailabilityResponse, GetBotsNameAvailabilityResponses, GetBotsResponse, GetBotsResponses, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesError, GetBotsUserAccessCandidatesErrors, GetBotsUserAccessCandidatesResponse, GetBotsUserAccessCandidatesResponses, GetChannelsByPlatformData, GetChannelsByPlatformError, GetChannelsByPlatformErrors, GetChannelsByPlatformResponse, GetChannelsByPlatformResponses, GetChannelsData, GetChannelsError, GetChannelsErrors, GetChannelsResponse, GetChannelsResponses, GetConnectorsCatalogData, GetConnectorsCatalogError, GetConnectorsCatalogErrors, GetConnectorsCatalogResponse, GetConnectorsCatalogResponses, GetEmailOauthCallbackData, GetEmailOauthCallbackError, GetEmailOauthCallbackErrors, GetEmailOauthCallbackResponse, GetEmailOauthCallbackResponses, GetEmailProvidersByIdData, GetEmailProvidersByIdError, GetEmailProvidersByIdErrors, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeError, GetEmailProvidersByIdOauthAuthorizeErrors, GetEmailProvidersByIdOauthAuthorizeResponse, GetEmailProvidersByIdOauthAuthorizeResponses, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusError, GetEmailProvidersByIdOauthStatusErrors, GetEmailProvidersByIdOauthStatusResponse, GetEmailProvidersByIdOauthStatusResponses, GetEmailProvidersByIdResponse, GetEmailProvidersByIdResponses, GetEmailProvidersData, GetEmailProvidersError, GetEmailProvidersErrors, GetEmailProvidersMetaData, GetEmailProvidersMetaResponse, GetEmailProvidersMetaResponses, GetEmailProvidersResponse, GetEmailProvidersResponses, GetFetchProvidersByIdData, GetFetchProvidersByIdError, GetFetchProvidersByIdErrors, GetFetchProvidersByIdResponse, GetFetchProvidersByIdResponses, GetFetchProvidersData, GetFetchProvidersError, GetFetchProvidersErrors, GetFetchProvidersMetaData, GetFetchProvidersMetaResponse, GetFetchProvidersMetaResponses, GetFetchProvidersResponse, GetFetchProvidersResponses, GetMemoryProvidersByIdData, GetMemoryProvidersByIdError, GetMemoryProvidersByIdErrors, GetMemoryProvidersByIdResponse, GetMemoryProvidersByIdResponses, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusError, GetMemoryProvidersByIdStatusErrors, GetMemoryProvidersByIdStatusResponse, GetMemoryProvidersByIdStatusResponses, GetMemoryProvidersData, GetMemoryProvidersError, GetMemoryProvidersErrors, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponse, GetMemoryProvidersMetaResponses, GetMemoryProvidersResponse, GetMemoryProvidersResponses, GetModelsByIdData, GetModelsByIdError, GetModelsByIdErrors, GetModelsByIdResponse, GetModelsByIdResponses, GetModelsCountData, GetModelsCountError, GetModelsCountErrors, GetModelsCountResponse, GetModelsCountResponses, GetModelsData, GetModelsError, GetModelsErrors, GetModelsModelByModelIdData, GetModelsModelByModelIdError, GetModelsModelByModelIdErrors, GetModelsModelByModelIdResponse, GetModelsModelByModelIdResponses, GetModelsResponse, GetModelsResponses, GetOauthMcpCallbackData, GetOauthMcpCallbackError, GetOauthMcpCallbackErrors, GetOauthMcpCallbackResponse, GetOauthMcpCallbackResponses, GetPingData, GetPingResponse, GetPingResponses, GetProvidersByIdData, GetProvidersByIdError, GetProvidersByIdErrors, GetProvidersByIdModelsData, GetProvidersByIdModelsError, GetProvidersByIdModelsErrors, GetProvidersByIdModelsResponse, GetProvidersByIdModelsResponses, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeError, GetProvidersByIdOauthAuthorizeErrors, GetProvidersByIdOauthAuthorizeResponse, GetProvidersByIdOauthAuthorizeResponses, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusError, GetProvidersByIdOauthStatusErrors, GetProvidersByIdOauthStatusResponse, GetProvidersByIdOauthStatusResponses, GetProvidersByIdResponse, GetProvidersByIdResponses, GetProvidersCountData, GetProvidersCountError, GetProvidersCountErrors, GetProvidersCountResponse, GetProvidersCountResponses, GetProvidersData, GetProvidersError, GetProvidersErrors, GetProvidersNameByNameData, GetProvidersNameByNameError, GetProvidersNameByNameErrors, GetProvidersNameByNameResponse, GetProvidersNameByNameResponses, GetProvidersOauthCallbackData, GetProvidersOauthCallbackError, GetProvidersOauthCallbackErrors, GetProvidersOauthCallbackResponse, GetProvidersOauthCallbackResponses, GetProvidersResponse, GetProvidersResponses, GetProviderTemplatesByIdData, GetProviderTemplatesByIdError, GetProviderTemplatesByIdErrors, GetProviderTemplatesByIdResponse, GetProviderTemplatesByIdResponses, GetProviderTemplatesData, GetProviderTemplatesError, GetProviderTemplatesErrors, GetProviderTemplatesResponse, GetProviderTemplatesResponses, GetSearchProvidersByIdData, GetSearchProvidersByIdError, GetSearchProvidersByIdErrors, GetSearchProvidersByIdResponse, GetSearchProvidersByIdResponses, GetSearchProvidersData, GetSearchProvidersError, GetSearchProvidersErrors, GetSearchProvidersMetaData, GetSearchProvidersMetaResponse, GetSearchProvidersMetaResponses, GetSearchProvidersResponse, GetSearchProvidersResponses, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesError, GetSpeechModelsByIdCapabilitiesErrors, GetSpeechModelsByIdCapabilitiesResponse, GetSpeechModelsByIdCapabilitiesResponses, GetSpeechModelsByIdData, GetSpeechModelsByIdError, GetSpeechModelsByIdErrors, GetSpeechModelsByIdResponse, GetSpeechModelsByIdResponses, GetSpeechModelsData, GetSpeechModelsError, GetSpeechModelsErrors, GetSpeechModelsResponse, GetSpeechModelsResponses, GetSpeechProvidersByIdData, GetSpeechProvidersByIdError, GetSpeechProvidersByIdErrors, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsError, GetSpeechProvidersByIdModelsErrors, GetSpeechProvidersByIdModelsResponse, GetSpeechProvidersByIdModelsResponses, GetSpeechProvidersByIdResponse, GetSpeechProvidersByIdResponses, GetSpeechProvidersData, GetSpeechProvidersError, GetSpeechProvidersErrors, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponse, GetSpeechProvidersMetaResponses, GetSpeechProvidersResponse, GetSpeechProvidersResponses, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestError, GetSupermarketArtifactsIconByDigestErrors, GetSupermarketArtifactsIconByDigestResponses, GetSupermarketPackagesData, GetSupermarketPackagesError, GetSupermarketPackagesErrors, GetSupermarketPackagesResponse, GetSupermarketPackagesResponses, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesError, GetSupermarketRegistriesByRegistryIdCategoriesErrors, GetSupermarketRegistriesByRegistryIdCategoriesResponse, GetSupermarketRegistriesByRegistryIdCategoriesResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdError, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponse, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponses, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesError, GetSupermarketRegistriesByRegistryIdPackagesErrors, GetSupermarketRegistriesByRegistryIdPackagesResponse, GetSupermarketRegistriesByRegistryIdPackagesResponses, GetSupermarketRegistriesData, GetSupermarketRegistriesError, GetSupermarketRegistriesErrors, GetSupermarketRegistriesResponse, GetSupermarketRegistriesResponses, GetSupermarketSkillsData, GetSupermarketSkillsError, GetSupermarketSkillsErrors, GetSupermarketSkillsResponse, GetSupermarketSkillsResponses, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesError, GetTranscriptionModelsByIdCapabilitiesErrors, GetTranscriptionModelsByIdCapabilitiesResponse, GetTranscriptionModelsByIdCapabilitiesResponses, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdError, GetTranscriptionModelsByIdErrors, GetTranscriptionModelsByIdResponse, GetTranscriptionModelsByIdResponses, GetTranscriptionModelsData, GetTranscriptionModelsError, GetTranscriptionModelsErrors, GetTranscriptionModelsResponse, GetTranscriptionModelsResponses, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdError, GetTranscriptionProvidersByIdErrors, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsError, GetTranscriptionProvidersByIdModelsErrors, GetTranscriptionProvidersByIdModelsResponse, GetTranscriptionProvidersByIdModelsResponses, GetTranscriptionProvidersByIdResponse, GetTranscriptionProvidersByIdResponses, GetTranscriptionProvidersData, GetTranscriptionProvidersError, GetTranscriptionProvidersErrors, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponse, GetTranscriptionProvidersMetaResponses, GetTranscriptionProvidersResponse, GetTranscriptionProvidersResponses, GetUsersByIdData, GetUsersByIdError, GetUsersByIdErrors, GetUsersByIdResponse, GetUsersByIdResponses, GetUsersData, GetUsersError, GetUsersErrors, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesError, GetUsersMeChannelIdentitiesErrors, GetUsersMeChannelIdentitiesResponse, GetUsersMeChannelIdentitiesResponses, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformError, GetUsersMeChannelsByPlatformErrors, GetUsersMeChannelsByPlatformResponse, GetUsersMeChannelsByPlatformResponses, GetUsersMeComputerAccessData, GetUsersMeComputerAccessError, GetUsersMeComputerAccessErrors, GetUsersMeComputerAccessResponse, GetUsersMeComputerAccessResponses, GetUsersMeData, GetUsersMeError, GetUsersMeErrors, GetUsersMeResponse, GetUsersMeResponses, GetUsersMeRuntimesData, GetUsersMeRuntimesError, GetUsersMeRuntimesErrors, GetUsersMeRuntimesResponse, GetUsersMeRuntimesResponses, GetUsersResponse, GetUsersResponses, GetVideoModelsByIdData, GetVideoModelsByIdError, GetVideoModelsByIdErrors, GetVideoModelsByIdResponse, GetVideoModelsByIdResponses, GetVideoModelsData, GetVideoModelsError, GetVideoModelsErrors, GetVideoModelsResponse, GetVideoModelsResponses, GetVideoProvidersByIdData, GetVideoProvidersByIdError, GetVideoProvidersByIdErrors, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsError, GetVideoProvidersByIdModelsErrors, GetVideoProvidersByIdModelsResponse, GetVideoProvidersByIdModelsResponses, GetVideoProvidersByIdResponse, GetVideoProvidersByIdResponses, GetVideoProvidersData, GetVideoProvidersError, GetVideoProvidersErrors, GetVideoProvidersMetaData, GetVideoProvidersMetaResponse, GetVideoProvidersMetaResponses, GetVideoProvidersResponse, GetVideoProvidersResponses, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponse, GetWebhookTunnelStatusResponses, GithubComFelinicsMemohInternalMcpConnection, HandlersAcpRuntimeCreateRequest, HandlersAcpRuntimeModelRequest, HandlersAcpRuntimeModeRequest, HandlersAcpRuntimeReasoningRequest, HandlersAgentCredentialPutRequest, HandlersBatchDeleteRequest, HandlersBotUserCandidate, HandlersBotUserCandidateListResponse, HandlersBotUserGrantListResponse, HandlersBrowserSessionCreateRequest, HandlersBrowserSessionCreateResponse, HandlersBrowserSessionKeepAliveResponse, HandlersCacheStats, HandlersChannelMeta, HandlersCommandActionError, HandlersCommandActionListItem, HandlersCommandActionResult, HandlersCommandEventResponse, HandlersCompactionInfo, HandlersConnectorCredentialRequest, HandlersConnectorEnabledRequest, HandlersConnectorOAuthRequest, HandlersContainerCpuMetricsResponse, HandlersContainerGpuRequest, HandlersContainerMemoryMetricsResponse, HandlersContainerMetricsPayloadResponse, HandlersContainerMetricsStatusResponse, HandlersContainerResourceLimitCapabilitiesResponse, HandlersContainerResourceLimitCapabilityResponse, HandlersContainerResourceLimitObservedResponse, HandlersContainerResourceLimitValuesResponse, HandlersContainerStorageMetricsResponse, HandlersContextLifecycleAggregates, HandlersContextLifecycleResponse, HandlersContextLifecycleTurn, HandlersContextUsage, HandlersCreateContainerRequest, HandlersCreateContainerResponse, HandlersCreateSessionRequest, HandlersCreateSnapshotRequest, HandlersCreateSnapshotResponse, HandlersDailyTokenUsage, HandlersDisplayInfoResponse, HandlersDisplaySessionListResponse, HandlersDisplayWebRtcOfferRequest, HandlersDisplayWebRtcOfferResponse, HandlersEmailOAuthStatusResponse, HandlersEnqueueQueueRequest, HandlersErrorResponse, HandlersFollowUpQueueItemResponse, HandlersFollowUpQueueReorderRequest, HandlersFollowUpQueueResponse, HandlersForkSessionRequest, HandlersFsArchiveRequest, HandlersFsDeleteRequest, HandlersFsExtractRequest, HandlersFsExtractResponse, HandlersFsFileInfo, HandlersFsListResponse, HandlersFsMkdirRequest, HandlersFsOpResponse, HandlersFsReadResponse, HandlersFsRenameRequest, HandlersFsUploadResponse, HandlersFsWriteRequest, HandlersGetContainerMetricsResponse, HandlersGetContainerResourceLimitsResponse, HandlersGetContainerResponse, HandlersGraphEdge, HandlersGraphNode, HandlersGraphResponse, HandlersHookEventInfo, HandlersHooksEventsResponse, HandlersHookTestRequest, HandlersHookTestResponse, HandlersInstallPackageRequest, HandlersInstallRegistryPackageResponse, HandlersInstallRegistrySkillResponse, HandlersListSessionsResponse, HandlersListSnapshotsResponse, HandlersLocalChannelMessageRequest, HandlersLoginRequest, HandlersLoginResponse, HandlersMcpStdioRequest, HandlersMcpStdioResponse, HandlersMemoryAddPayload, HandlersMemoryCompactPayload, HandlersMemoryDeletePayload, HandlersMemorySearchPayload, HandlersMemoryUpdatePayload, HandlersModelPreferenceSeedResponse, HandlersModelTokenUsage, HandlersOauthAuthorizeRequest, HandlersOauthDiscoverRequest, HandlersOauthExchangeRequest, HandlersPingResponse, HandlersProbeResponse, HandlersQuickActionExecuteRequest, HandlersRefreshResponse, HandlersRollbackRequest, HandlersSafeSkillsResponse, HandlersSessionInfoResponse, HandlersSessionQueueResponse, HandlersSkillItem, HandlersSkillsActionRequest, HandlersSkillsDeleteRequest, HandlersSkillsOpResponse, HandlersSkillsResponse, HandlersSkillsUpsertRequest, HandlersSnapshotInfo, HandlersSteerQueueItemResponse, HandlersSteerQueueReorderRequest, HandlersSteerQueueResponse, HandlersSupermarketAuthor, HandlersSupermarketCatalogSkill, HandlersSupermarketCatalogSkillListResponse, HandlersSupermarketRegistry, HandlersSupermarketRegistryListResponse, HandlersSupermarketSkillArtifact, HandlersSupermarketSkillCategory, HandlersSupermarketSkillCategoryListResponse, HandlersSupermarketSkillCategoryRegistry, HandlersSupermarketSkillIcon, HandlersSupermarketSkillIconAsset, HandlersSupermarketSkillPackageCategory, HandlersSupermarketSkillPackageDescriptor, HandlersSupermarketSkillPackageListResponse, HandlersSupermarketSkillPackageSummary, HandlersSupermarketSkillSource, HandlersSynthesizeRequest, HandlersSynthesizeResponse, HandlersTerminalInfoResponse, HandlersTokenUsageRecord, HandlersTokenUsageRecordsResponse, HandlersTokenUsageResponse, HandlersToolApprovalDecisionRequest, HandlersToolDefBucket, HandlersTriggerCompactResponse, HandlersUiLocateMessageResponse, HandlersUiMessageListResponse, HandlersUpdateContainerMetricsRequest, HandlersUpdateContainerResourceLimitsRequest, HandlersUpdateQueueRequest, HandlersUpdateSessionRequest, HooksActionResult, HooksOutputWarning, HooksResult, HooksSystemSectionCache, HooksSystemSectionOutput, HooksSystemSectionRetention, HooksToolPayload, McpAuthorizeResult, McpDiscoveryResult, McpExportResponse, McpImportRequest, McpListResponse, McpMcpServerEntry, McpOAuthStatus, McpToolDescriptor, McpUpsertRequest, ModelsAddRequest, ModelsAddResponse, ModelsCountResponse, ModelsGetResponse, ModelsModelConfig, ModelsModelType, ModelsTestResponse, ModelsTestStatus, ModelsUpdateRequest, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelError, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningError, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponse, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponses, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdError, PatchBotsByBotIdAgentsByIdErrors, PatchBotsByBotIdAgentsByIdResponse, PatchBotsByBotIdAgentsByIdResponses, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdError, PatchBotsByBotIdConnectorsByConnectionIdErrors, PatchBotsByBotIdConnectorsByConnectionIdResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningError, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponse, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponses, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdError, PatchBotsByBotIdSessionsBySessionIdErrors, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdError, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdErrors, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponse, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponses, PatchBotsByBotIdSessionsBySessionIdResponse, PatchBotsByBotIdSessionsBySessionIdResponses, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdError, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdErrors, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponse, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponses, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdError, PatchBotsByBotIdWorkdirsByWorkdirIdErrors, PatchBotsByBotIdWorkdirsByWorkdirIdResponse, PatchBotsByBotIdWorkdirsByWorkdirIdResponses, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusError, PatchBotsByIdChannelByPlatformStatusErrors, PatchBotsByIdChannelByPlatformStatusResponse, PatchBotsByIdChannelByPlatformStatusResponses, PostAuthLoginData, PostAuthLoginError, PostAuthLoginErrors, PostAuthLoginResponse, PostAuthLoginResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostBotsBackupImportData, PostBotsBackupImportError, PostBotsBackupImportErrors, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewError, PostBotsBackupImportPreviewErrors, PostBotsBackupImportPreviewResponse, PostBotsBackupImportPreviewResponses, PostBotsBackupImportResponse, PostBotsBackupImportResponses, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesError, PostBotsByBotIdAclRulesErrors, PostBotsByBotIdAclRulesResponse, PostBotsByBotIdAclRulesResponses, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesError, PostBotsByBotIdAcpRuntimesErrors, PostBotsByBotIdAcpRuntimesResponse, PostBotsByBotIdAcpRuntimesResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeError, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponse, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelError, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelResponses, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollError, PostBotsByBotIdAgentsByIdCodexLoginDevicePollErrors, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponse, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponses, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsError, PostBotsByBotIdAgentsErrors, PostBotsByBotIdAgentsResponse, PostBotsByBotIdAgentsResponses, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportError, PostBotsByBotIdBackupExportErrors, PostBotsByBotIdBackupExportResponses, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersError, PostBotsByBotIdChannelManagersErrors, PostBotsByBotIdChannelManagersResponses, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyError, PostBotsByBotIdConnectorsApiKeyErrors, PostBotsByBotIdConnectorsApiKeyResponse, PostBotsByBotIdConnectorsApiKeyResponses, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthError, PostBotsByBotIdConnectorsByConnectionIdReauthErrors, PostBotsByBotIdConnectorsByConnectionIdReauthResponse, PostBotsByBotIdConnectorsByConnectionIdReauthResponses, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthError, PostBotsByBotIdConnectorsOauthErrors, PostBotsByBotIdConnectorsOauthResponse, PostBotsByBotIdConnectorsOauthResponses, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveError, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveErrors, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponse, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponses, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsError, PostBotsByBotIdContainerBrowserSessionsErrors, PostBotsByBotIdContainerBrowserSessionsResponse, PostBotsByBotIdContainerBrowserSessionsResponses, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreError, PostBotsByBotIdContainerDataRestoreErrors, PostBotsByBotIdContainerDataRestoreResponse, PostBotsByBotIdContainerDataRestoreResponses, PostBotsByBotIdContainerDisplayPrepareData, PostBotsByBotIdContainerDisplayPrepareError, PostBotsByBotIdContainerDisplayPrepareErrors, PostBotsByBotIdContainerDisplayPrepareResponse, PostBotsByBotIdContainerDisplayPrepareResponses, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferError, PostBotsByBotIdContainerDisplayWebrtcOfferErrors, PostBotsByBotIdContainerDisplayWebrtcOfferResponse, PostBotsByBotIdContainerDisplayWebrtcOfferResponses, PostBotsByBotIdContainerError, PostBotsByBotIdContainerErrors, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveError, PostBotsByBotIdContainerFsArchiveErrors, PostBotsByBotIdContainerFsArchiveResponses, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteError, PostBotsByBotIdContainerFsDeleteErrors, PostBotsByBotIdContainerFsDeleteResponse, PostBotsByBotIdContainerFsDeleteResponses, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractError, PostBotsByBotIdContainerFsExtractErrors, PostBotsByBotIdContainerFsExtractResponse, PostBotsByBotIdContainerFsExtractResponses, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirError, PostBotsByBotIdContainerFsMkdirErrors, PostBotsByBotIdContainerFsMkdirResponse, PostBotsByBotIdContainerFsMkdirResponses, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameError, PostBotsByBotIdContainerFsRenameErrors, PostBotsByBotIdContainerFsRenameResponse, PostBotsByBotIdContainerFsRenameResponses, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadError, PostBotsByBotIdContainerFsUploadErrors, PostBotsByBotIdContainerFsUploadResponse, PostBotsByBotIdContainerFsUploadResponses, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteError, PostBotsByBotIdContainerFsWriteErrors, PostBotsByBotIdContainerFsWriteResponse, PostBotsByBotIdContainerFsWriteResponses, PostBotsByBotIdContainerResponse, PostBotsByBotIdContainerResponses, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsError, PostBotsByBotIdContainerSkillsActionsErrors, PostBotsByBotIdContainerSkillsActionsResponse, PostBotsByBotIdContainerSkillsActionsResponses, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsError, PostBotsByBotIdContainerSkillsErrors, PostBotsByBotIdContainerSkillsResponse, PostBotsByBotIdContainerSkillsResponses, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsError, PostBotsByBotIdContainerSnapshotsErrors, PostBotsByBotIdContainerSnapshotsResponse, PostBotsByBotIdContainerSnapshotsResponses, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackError, PostBotsByBotIdContainerSnapshotsRollbackErrors, PostBotsByBotIdContainerSnapshotsRollbackResponse, PostBotsByBotIdContainerSnapshotsRollbackResponses, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartError, PostBotsByBotIdContainerStartErrors, PostBotsByBotIdContainerStartResponse, PostBotsByBotIdContainerStartResponses, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopError, PostBotsByBotIdContainerStopErrors, PostBotsByBotIdContainerStopResponse, PostBotsByBotIdContainerStopResponses, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsError, PostBotsByBotIdEmailBindingsErrors, PostBotsByBotIdEmailBindingsResponse, PostBotsByBotIdEmailBindingsResponses, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestError, PostBotsByBotIdHooksTestErrors, PostBotsByBotIdHooksTestResponse, PostBotsByBotIdHooksTestResponses, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeError, PostBotsByBotIdMcpByIdOauthAuthorizeErrors, PostBotsByBotIdMcpByIdOauthAuthorizeResponse, PostBotsByBotIdMcpByIdOauthAuthorizeResponses, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverError, PostBotsByBotIdMcpByIdOauthDiscoverErrors, PostBotsByBotIdMcpByIdOauthDiscoverResponse, PostBotsByBotIdMcpByIdOauthDiscoverResponses, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeError, PostBotsByBotIdMcpByIdOauthExchangeErrors, PostBotsByBotIdMcpByIdOauthExchangeResponse, PostBotsByBotIdMcpByIdOauthExchangeResponses, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeError, PostBotsByBotIdMcpByIdProbeErrors, PostBotsByBotIdMcpByIdProbeResponse, PostBotsByBotIdMcpByIdProbeResponses, PostBotsByBotIdMcpData, PostBotsByBotIdMcpError, PostBotsByBotIdMcpErrors, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteError, PostBotsByBotIdMcpOpsBatchDeleteErrors, PostBotsByBotIdMcpOpsBatchDeleteResponses, PostBotsByBotIdMcpResponse, PostBotsByBotIdMcpResponses, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdError, PostBotsByBotIdMcpStdioByConnectionIdErrors, PostBotsByBotIdMcpStdioByConnectionIdResponse, PostBotsByBotIdMcpStdioByConnectionIdResponses, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioError, PostBotsByBotIdMcpStdioErrors, PostBotsByBotIdMcpStdioResponse, PostBotsByBotIdMcpStdioResponses, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactError, PostBotsByBotIdMemoryCompactErrors, PostBotsByBotIdMemoryCompactResponse, PostBotsByBotIdMemoryCompactResponses, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryError, PostBotsByBotIdMemoryErrors, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestError, PostBotsByBotIdMemoryIngestErrors, PostBotsByBotIdMemoryIngestResponse, PostBotsByBotIdMemoryIngestResponses, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildError, PostBotsByBotIdMemoryRebuildErrors, PostBotsByBotIdMemoryRebuildResponse, PostBotsByBotIdMemoryRebuildResponses, PostBotsByBotIdMemoryResponse, PostBotsByBotIdMemoryResponses, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchError, PostBotsByBotIdMemorySearchErrors, PostBotsByBotIdMemorySearchResponse, PostBotsByBotIdMemorySearchResponses, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteError, PostBotsByBotIdQuickActionsExecuteErrors, PostBotsByBotIdQuickActionsExecuteResponse, PostBotsByBotIdQuickActionsExecuteResponses, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleError, PostBotsByBotIdScheduleErrors, PostBotsByBotIdScheduleResponse, PostBotsByBotIdScheduleResponses, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeError, PostBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponse, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactError, PostBotsByBotIdSessionsBySessionIdCompactErrors, PostBotsByBotIdSessionsBySessionIdCompactResponse, PostBotsByBotIdSessionsBySessionIdCompactResponses, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerError, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerErrors, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerResponse, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerResponses, PostBotsByBotIdSessionsBySessionIdFollowUpQueueData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueError, PostBotsByBotIdSessionsBySessionIdFollowUpQueueErrors, PostBotsByBotIdSessionsBySessionIdFollowUpQueueResponse, PostBotsByBotIdSessionsBySessionIdFollowUpQueueResponses, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkError, PostBotsByBotIdSessionsBySessionIdForkErrors, PostBotsByBotIdSessionsBySessionIdForkResponse, PostBotsByBotIdSessionsBySessionIdForkResponses, PostBotsByBotIdSessionsBySessionIdSteerQueueData, PostBotsByBotIdSessionsBySessionIdSteerQueueError, PostBotsByBotIdSessionsBySessionIdSteerQueueErrors, PostBotsByBotIdSessionsBySessionIdSteerQueueResponse, PostBotsByBotIdSessionsBySessionIdSteerQueueResponses, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsError, PostBotsByBotIdSessionsErrors, PostBotsByBotIdSessionsResponse, PostBotsByBotIdSessionsResponses, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsError, PostBotsByBotIdSettingsErrors, PostBotsByBotIdSettingsResponse, PostBotsByBotIdSettingsResponses, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageError, PostBotsByBotIdSupermarketInstallPackageErrors, PostBotsByBotIdSupermarketInstallPackageResponse, PostBotsByBotIdSupermarketInstallPackageResponses, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveError, PostBotsByBotIdToolApprovalsByApprovalIdApproveErrors, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponse, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponses, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectError, PostBotsByBotIdToolApprovalsByApprovalIdRejectErrors, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponse, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponses, PostBotsByBotIdToolsData, PostBotsByBotIdToolsError, PostBotsByBotIdToolsErrors, PostBotsByBotIdToolsResponse, PostBotsByBotIdToolsResponses, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeError, PostBotsByBotIdTtsSynthesizeErrors, PostBotsByBotIdTtsSynthesizeResponse, PostBotsByBotIdTtsSynthesizeResponses, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessError, PostBotsByBotIdUserAccessErrors, PostBotsByBotIdUserAccessResponse, PostBotsByBotIdUserAccessResponses, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesError, PostBotsByBotIdWebMessagesErrors, PostBotsByBotIdWebMessagesResponse, PostBotsByBotIdWebMessagesResponses, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsError, PostBotsByBotIdWorkdirsErrors, PostBotsByBotIdWorkdirsResponse, PostBotsByBotIdWorkdirsResponses, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatError, PostBotsByIdChannelByPlatformSendChatErrors, PostBotsByIdChannelByPlatformSendChatResponse, PostBotsByIdChannelByPlatformSendChatResponses, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendError, PostBotsByIdChannelByPlatformSendErrors, PostBotsByIdChannelByPlatformSendResponse, PostBotsByIdChannelByPlatformSendResponses, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointError, PostBotsByIdChannelByPlatformWebhookEndpointErrors, PostBotsByIdChannelByPlatformWebhookEndpointResponse, PostBotsByIdChannelByPlatformWebhookEndpointResponses, PostBotsData, PostBotsError, PostBotsErrors, PostBotsResponse, PostBotsResponses, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdError, PostEmailMailgunWebhookByConfigIdErrors, PostEmailMailgunWebhookByConfigIdResponse, PostEmailMailgunWebhookByConfigIdResponses, PostEmailProvidersData, PostEmailProvidersError, PostEmailProvidersErrors, PostEmailProvidersResponse, PostEmailProvidersResponses, PostFetchProvidersData, PostFetchProvidersError, PostFetchProvidersErrors, PostFetchProvidersResponse, PostFetchProvidersResponses, PostMemoryProvidersData, PostMemoryProvidersError, PostMemoryProvidersErrors, PostMemoryProvidersResponse, PostMemoryProvidersResponses, PostModelsByIdTestData, PostModelsByIdTestError, PostModelsByIdTestErrors, PostModelsByIdTestResponse, PostModelsByIdTestResponses, PostModelsData, PostModelsError, PostModelsErrors, PostModelsResponse, PostModelsResponses, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsError, PostProvidersByIdImportModelsErrors, PostProvidersByIdImportModelsResponse, PostProvidersByIdImportModelsResponses, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollError, PostProvidersByIdOauthPollErrors, PostProvidersByIdOauthPollResponse, PostProvidersByIdOauthPollResponses, PostProvidersByIdTestData, PostProvidersByIdTestError, PostProvidersByIdTestErrors, PostProvidersByIdTestResponse, PostProvidersByIdTestResponses, PostProvidersData, PostProvidersError, PostProvidersErrors, PostProvidersFromTemplateData, PostProvidersFromTemplateError, PostProvidersFromTemplateErrors, PostProvidersFromTemplateResponse, PostProvidersFromTemplateResponses, PostProvidersResponse, PostProvidersResponses, PostSearchProvidersData, PostSearchProvidersError, PostSearchProvidersErrors, PostSearchProvidersResponse, PostSearchProvidersResponses, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestError, PostSpeechModelsByIdTestErrors, PostSpeechModelsByIdTestResponses, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsError, PostSpeechProvidersByIdImportModelsErrors, PostSpeechProvidersByIdImportModelsResponse, PostSpeechProvidersByIdImportModelsResponses, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestError, PostTranscriptionModelsByIdTestErrors, PostTranscriptionModelsByIdTestResponse, PostTranscriptionModelsByIdTestResponses, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsError, PostTranscriptionProvidersByIdImportModelsErrors, PostTranscriptionProvidersByIdImportModelsResponse, PostTranscriptionProvidersByIdImportModelsResponses, PostUsersData, PostUsersError, PostUsersErrors, PostUsersMeChannelLinksData, PostUsersMeChannelLinksError, PostUsersMeChannelLinksErrors, PostUsersMeChannelLinksResponse, PostUsersMeChannelLinksResponses, PostUsersMeRuntimesData, PostUsersMeRuntimesError, PostUsersMeRuntimesErrors, PostUsersMeRuntimesResponse, PostUsersMeRuntimesResponses, PostUsersResponse, PostUsersResponses, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsError, PostVideoProvidersByIdImportModelsErrors, PostVideoProvidersByIdImportModelsResponse, PostVideoProvidersByIdImportModelsResponses, ProvidersCountResponse, ProvidersCreateFromTemplateRequest, ProvidersCreateRequest, ProvidersGetResponse, ProvidersImportModelsRequest, ProvidersImportModelsResponse, ProvidersOAuthAccount, ProvidersOAuthAuthorizeResponse, ProvidersOAuthDeviceStatus, ProvidersOAuthStatus, ProvidersTestResponse, ProvidersTestStatus, ProvidersUpdateRequest, ProvidertemplatesGetResponse, ProvidertemplatesModelResponse, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectError, PutBotsByBotIdAclDefaultEffectErrors, PutBotsByBotIdAclDefaultEffectResponses, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdError, PutBotsByBotIdAclRulesByRuleIdErrors, PutBotsByBotIdAclRulesByRuleIdResponse, PutBotsByBotIdAclRulesByRuleIdResponses, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialError, PutBotsByBotIdAgentsByIdCredentialErrors, PutBotsByBotIdAgentsByIdCredentialResponse, PutBotsByBotIdAgentsByIdCredentialResponses, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsError, PutBotsByBotIdContainerMetricsErrors, PutBotsByBotIdContainerMetricsResponse, PutBotsByBotIdContainerMetricsResponses, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdError, PutBotsByBotIdEmailBindingsByIdErrors, PutBotsByBotIdEmailBindingsByIdResponse, PutBotsByBotIdEmailBindingsByIdResponses, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdError, PutBotsByBotIdMcpByIdErrors, PutBotsByBotIdMcpByIdResponse, PutBotsByBotIdMcpByIdResponses, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportError, PutBotsByBotIdMcpOpsImportErrors, PutBotsByBotIdMcpOpsImportResponse, PutBotsByBotIdMcpOpsImportResponses, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdError, PutBotsByBotIdMemoryByMemoryIdErrors, PutBotsByBotIdMemoryByMemoryIdResponse, PutBotsByBotIdMemoryByMemoryIdResponses, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdError, PutBotsByBotIdScheduleByIdErrors, PutBotsByBotIdScheduleByIdResponse, PutBotsByBotIdScheduleByIdResponses, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderData, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderError, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderErrors, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderResponse, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderResponses, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderData, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderError, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderErrors, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderResponse, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderResponses, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsError, PutBotsByBotIdSettingsErrors, PutBotsByBotIdSettingsResponse, PutBotsByBotIdSettingsResponses, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdError, PutBotsByBotIdUserAccessByGrantIdErrors, PutBotsByBotIdUserAccessByGrantIdResponse, PutBotsByBotIdUserAccessByGrantIdResponses, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalError, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalErrors, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalResponses, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryError, PutBotsByBotIdWorkspaceTargetsPrimaryErrors, PutBotsByBotIdWorkspaceTargetsPrimaryResponses, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdError, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdErrors, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponse, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponses, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformError, PutBotsByIdChannelByPlatformErrors, PutBotsByIdChannelByPlatformResponse, PutBotsByIdChannelByPlatformResponses, PutBotsByIdData, PutBotsByIdError, PutBotsByIdErrors, PutBotsByIdOwnerData, PutBotsByIdOwnerError, PutBotsByIdOwnerErrors, PutBotsByIdOwnerResponse, PutBotsByIdOwnerResponses, PutBotsByIdResponse, PutBotsByIdResponses, PutEmailProvidersByIdData, PutEmailProvidersByIdError, PutEmailProvidersByIdErrors, PutEmailProvidersByIdResponse, PutEmailProvidersByIdResponses, PutFetchProvidersByIdData, PutFetchProvidersByIdError, PutFetchProvidersByIdErrors, PutFetchProvidersByIdResponse, PutFetchProvidersByIdResponses, PutMemoryProvidersByIdData, PutMemoryProvidersByIdError, PutMemoryProvidersByIdErrors, PutMemoryProvidersByIdResponse, PutMemoryProvidersByIdResponses, PutModelsByIdData, PutModelsByIdError, PutModelsByIdErrors, PutModelsByIdResponse, PutModelsByIdResponses, PutModelsModelByModelIdData, PutModelsModelByModelIdError, PutModelsModelByModelIdErrors, PutModelsModelByModelIdResponse, PutModelsModelByModelIdResponses, PutProvidersByIdData, PutProvidersByIdError, PutProvidersByIdErrors, PutProvidersByIdResponse, PutProvidersByIdResponses, PutSearchProvidersByIdData, PutSearchProvidersByIdError, PutSearchProvidersByIdErrors, PutSearchProvidersByIdResponse, PutSearchProvidersByIdResponses, PutSpeechModelsByIdData, PutSpeechModelsByIdError, PutSpeechModelsByIdErrors, PutSpeechModelsByIdResponse, PutSpeechModelsByIdResponses, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdError, PutTranscriptionModelsByIdErrors, PutTranscriptionModelsByIdResponse, PutTranscriptionModelsByIdResponses, PutUsersByIdData, PutUsersByIdError, PutUsersByIdErrors, PutUsersByIdResponse, PutUsersByIdResponses, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformError, PutUsersMeChannelsByPlatformErrors, PutUsersMeChannelsByPlatformResponse, PutUsersMeChannelsByPlatformResponses, PutUsersMeData, PutUsersMeError, PutUsersMeErrors, PutUsersMePasswordData, PutUsersMePasswordError, PutUsersMePasswordErrors, PutUsersMePasswordResponses, PutUsersMeResponse, PutUsersMeResponses, PutVideoModelsByIdData, PutVideoModelsByIdError, PutVideoModelsByIdErrors, PutVideoModelsByIdResponse, PutVideoModelsByIdResponses, ReasoningOptions, ScheduleCreateRequest, ScheduleExecutionConfig, ScheduleListLogsResponse, ScheduleListResponse, ScheduleLog, ScheduleNullableInt, ScheduleSchedule, ScheduleUpdateRequest, SearchprovidersCreateRequest, SearchprovidersGetResponse, SearchprovidersProviderConfigSchema, SearchprovidersProviderFieldSchema, SearchprovidersProviderMeta, SearchprovidersProviderName, SearchprovidersUpdateRequest, SessionruntimeFollowUpPendingRef, SessionruntimeQueueStatus, SessionruntimeSteerPendingRef, SessionSession, SettingsSettings, SettingsToolApprovalConfig, SettingsToolApprovalExecPolicy, SettingsToolApprovalFilePolicy, SettingsToolApprovalMode, SettingsUpsertRequest, SkillpackagesInstallation, SkillsSafeCatalogItem, SupermarketUninstallPackageResponse, UserinputUiAnswer, UserinputUiOption, UserinputUiQuestion, UserruntimeCreateRuntimeRequest, UserruntimeRuntime, VideoConfigSchema, VideoFieldSchema, VideoImportModelsResponse, VideoModelInfo, VideoModelResponse, VideoProviderMetaResponse, VideoProviderResponse, VideoUpdateModelRequest, WebhooktunnelStatus, WorkdirCreateRequest, WorkdirUpdateRequest, WorkdirWorkdir, WorkdirWorkdirsResponse, WorkspaceSetPrimaryWorkspaceTargetRequest, WorkspaceUpdateWorkspaceTargetToolApprovalRequest, WorkspaceWorkspaceTarget, WorkspaceWorkspaceTargetGrant, WorkspaceWorkspaceTargetGrantsResponse, WorkspaceWorkspaceTargetsResponse, WorkspaceWorkspaceTargetToolApproval } from './types.gen'; diff --git a/packages/sdk/src/sdk.gen.ts b/packages/sdk/src/sdk.gen.ts index 64c7b35232..49db8aa8d5 100644 --- a/packages/sdk/src/sdk.gen.ts +++ b/packages/sdk/src/sdk.gen.ts @@ -3,7 +3,7 @@ import { type Client, type ClientMeta, formDataBodySerializer, type Options as Options2, type RequestResult, type ServerSentEventsResult, type TDataShape } from './client'; import { client } from './client.gen'; import { getBotsByBotIdMessagesLocateResponseTransformer, getBotsByBotIdMessagesResponseTransformer } from './transformers.gen'; -import type { DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdErrors, DeleteBotsByBotIdAclRulesByRuleIdResponses, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdErrors, DeleteBotsByBotIdAcpRuntimesByRuntimeIdResponses, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialErrors, DeleteBotsByBotIdAgentsByIdCredentialResponses, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdErrors, DeleteBotsByBotIdAgentsByIdResponses, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdErrors, DeleteBotsByBotIdChannelManagersByChannelIdentityIdResponses, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsErrors, DeleteBotsByBotIdCompactionLogsResponses, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdErrors, DeleteBotsByBotIdConnectorsByConnectionIdResponses, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdErrors, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdResponses, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdErrors, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdResponses, DeleteBotsByBotIdContainerErrors, DeleteBotsByBotIdContainerResponses, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsErrors, DeleteBotsByBotIdContainerSkillsResponses, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdErrors, DeleteBotsByBotIdEmailBindingsByIdResponses, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdErrors, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenErrors, DeleteBotsByBotIdMcpByIdOauthTokenResponses, DeleteBotsByBotIdMcpByIdResponses, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdErrors, DeleteBotsByBotIdMemoryByIdResponses, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryErrors, DeleteBotsByBotIdMemoryResponses, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesErrors, DeleteBotsByBotIdMessagesResponses, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdErrors, DeleteBotsByBotIdScheduleByIdResponses, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsErrors, DeleteBotsByBotIdScheduleLogsResponses, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdErrors, DeleteBotsByBotIdSessionsBySessionIdResponses, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsErrors, DeleteBotsByBotIdSettingsResponses, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdErrors, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponses, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdErrors, DeleteBotsByBotIdUserAccessByGrantIdResponses, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdErrors, DeleteBotsByBotIdWorkdirsByWorkdirIdResponses, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdErrors, DeleteBotsByBotIdWorkspaceTargetsByTargetIdResponses, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformErrors, DeleteBotsByIdChannelByPlatformResponses, DeleteBotsByIdData, DeleteBotsByIdErrors, DeleteBotsByIdResponses, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdErrors, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenErrors, DeleteEmailProvidersByIdOauthTokenResponses, DeleteEmailProvidersByIdResponses, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdErrors, DeleteFetchProvidersByIdResponses, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdErrors, DeleteMemoryProvidersByIdResponses, DeleteModelsByIdData, DeleteModelsByIdErrors, DeleteModelsByIdResponses, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdErrors, DeleteModelsModelByModelIdResponses, DeleteProvidersByIdData, DeleteProvidersByIdErrors, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenErrors, DeleteProvidersByIdOauthTokenResponses, DeleteProvidersByIdResponses, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdErrors, DeleteSearchProvidersByIdResponses, DeleteUsersByIdData, DeleteUsersByIdErrors, DeleteUsersByIdResponses, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdErrors, DeleteUsersMeChannelIdentitiesByChannelIdentityIdResponses, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdErrors, DeleteUsersMeRuntimesByIdResponses, GetAcpProfilesData, GetAcpProfilesResponses, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsErrors, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponses, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesErrors, GetBotsByBotIdAclChannelIdentitiesResponses, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsErrors, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponses, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectErrors, GetBotsByBotIdAclDefaultEffectResponses, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesErrors, GetBotsByBotIdAclRulesResponses, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdErrors, GetBotsByBotIdAcpRuntimesByRuntimeIdResponses, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialErrors, GetBotsByBotIdAgentsByIdCredentialResponses, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdErrors, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsErrors, GetBotsByBotIdAgentsByIdModelsResponses, GetBotsByBotIdAgentsByIdResponses, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsErrors, GetBotsByBotIdAgentsResponses, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryErrors, GetBotsByBotIdBackupSummaryResponses, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersErrors, GetBotsByBotIdChannelManagersResponses, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsErrors, GetBotsByBotIdCompactionLogsResponses, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdErrors, GetBotsByBotIdConnectorsByConnectionIdResponses, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsErrors, GetBotsByBotIdConnectorsResponses, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayErrors, GetBotsByBotIdContainerDisplayResponses, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsErrors, GetBotsByBotIdContainerDisplaySessionsResponses, GetBotsByBotIdContainerErrors, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadErrors, GetBotsByBotIdContainerFsDownloadResponses, GetBotsByBotIdContainerFsErrors, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListErrors, GetBotsByBotIdContainerFsListResponses, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadErrors, GetBotsByBotIdContainerFsReadResponses, GetBotsByBotIdContainerFsResponses, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsErrors, GetBotsByBotIdContainerMetricsResponses, GetBotsByBotIdContainerResponses, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsErrors, GetBotsByBotIdContainerSkillsResponses, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsErrors, GetBotsByBotIdContainerSnapshotsResponses, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalErrors, GetBotsByBotIdContainerTerminalResponses, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsErrors, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsErrors, GetBotsByBotIdEmailBindingsResponses, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdErrors, GetBotsByBotIdEmailOutboxByIdResponses, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxErrors, GetBotsByBotIdEmailOutboxResponses, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsErrors, GetBotsByBotIdHooksEventsResponses, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdErrors, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusErrors, GetBotsByBotIdMcpByIdOauthStatusResponses, GetBotsByBotIdMcpByIdResponses, GetBotsByBotIdMcpData, GetBotsByBotIdMcpErrors, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportErrors, GetBotsByBotIdMcpOpsExportResponses, GetBotsByBotIdMcpResponses, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryErrors, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphErrors, GetBotsByBotIdMemoryGraphResponses, GetBotsByBotIdMemoryResponses, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusErrors, GetBotsByBotIdMemoryStatusResponses, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageErrors, GetBotsByBotIdMemoryUsageResponses, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesErrors, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateErrors, GetBotsByBotIdMessagesLocateResponses, GetBotsByBotIdMessagesResponses, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdErrors, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsErrors, GetBotsByBotIdScheduleByIdLogsResponses, GetBotsByBotIdScheduleByIdResponses, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleErrors, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsErrors, GetBotsByBotIdScheduleLogsResponses, GetBotsByBotIdScheduleResponses, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleErrors, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponses, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdErrors, GetBotsByBotIdSessionsBySessionIdResponses, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusErrors, GetBotsByBotIdSessionsBySessionIdStatusResponses, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsErrors, GetBotsByBotIdSessionsEventsData, GetBotsByBotIdSessionsEventsErrors, GetBotsByBotIdSessionsEventsResponse, GetBotsByBotIdSessionsEventsResponses, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedErrors, GetBotsByBotIdSessionsModelPreferenceSeedResponses, GetBotsByBotIdSessionsResponses, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsErrors, GetBotsByBotIdSettingsResponses, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogErrors, GetBotsByBotIdSkillsCatalogResponses, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesErrors, GetBotsByBotIdSupermarketPackagesResponses, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageErrors, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsErrors, GetBotsByBotIdTokenUsageRecordsResponses, GetBotsByBotIdTokenUsageResponses, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesErrors, GetBotsByBotIdUserAccessCandidatesResponses, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessErrors, GetBotsByBotIdUserAccessResponses, GetBotsByBotIdWebStreamData, GetBotsByBotIdWebStreamErrors, GetBotsByBotIdWebStreamResponse, GetBotsByBotIdWebStreamResponses, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsErrors, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsErrors, GetBotsByBotIdWorkdirsResponses, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsErrors, GetBotsByBotIdWorkspaceTargetsResponses, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformErrors, GetBotsByIdChannelByPlatformResponses, GetBotsByIdChecksData, GetBotsByIdChecksErrors, GetBotsByIdChecksResponses, GetBotsByIdData, GetBotsByIdErrors, GetBotsByIdResponses, GetBotsData, GetBotsErrors, GetBotsNameAvailabilityData, GetBotsNameAvailabilityErrors, GetBotsNameAvailabilityResponses, GetBotsResponses, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesErrors, GetBotsUserAccessCandidatesResponses, GetChannelsByPlatformData, GetChannelsByPlatformErrors, GetChannelsByPlatformResponses, GetChannelsData, GetChannelsErrors, GetChannelsResponses, GetConnectorsCatalogData, GetConnectorsCatalogErrors, GetConnectorsCatalogResponses, GetEmailOauthCallbackData, GetEmailOauthCallbackErrors, GetEmailOauthCallbackResponses, GetEmailProvidersByIdData, GetEmailProvidersByIdErrors, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeErrors, GetEmailProvidersByIdOauthAuthorizeResponses, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusErrors, GetEmailProvidersByIdOauthStatusResponses, GetEmailProvidersByIdResponses, GetEmailProvidersData, GetEmailProvidersErrors, GetEmailProvidersMetaData, GetEmailProvidersMetaResponses, GetEmailProvidersResponses, GetFetchProvidersByIdData, GetFetchProvidersByIdErrors, GetFetchProvidersByIdResponses, GetFetchProvidersData, GetFetchProvidersErrors, GetFetchProvidersMetaData, GetFetchProvidersMetaResponses, GetFetchProvidersResponses, GetMemoryProvidersByIdData, GetMemoryProvidersByIdErrors, GetMemoryProvidersByIdResponses, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusErrors, GetMemoryProvidersByIdStatusResponses, GetMemoryProvidersData, GetMemoryProvidersErrors, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponses, GetMemoryProvidersResponses, GetModelsByIdData, GetModelsByIdErrors, GetModelsByIdResponses, GetModelsCountData, GetModelsCountErrors, GetModelsCountResponses, GetModelsData, GetModelsErrors, GetModelsModelByModelIdData, GetModelsModelByModelIdErrors, GetModelsModelByModelIdResponses, GetModelsResponses, GetOauthMcpCallbackData, GetOauthMcpCallbackErrors, GetOauthMcpCallbackResponses, GetPingData, GetPingResponses, GetProvidersByIdData, GetProvidersByIdErrors, GetProvidersByIdModelsData, GetProvidersByIdModelsErrors, GetProvidersByIdModelsResponses, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeErrors, GetProvidersByIdOauthAuthorizeResponses, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusErrors, GetProvidersByIdOauthStatusResponses, GetProvidersByIdResponses, GetProvidersCountData, GetProvidersCountErrors, GetProvidersCountResponses, GetProvidersData, GetProvidersErrors, GetProvidersNameByNameData, GetProvidersNameByNameErrors, GetProvidersNameByNameResponses, GetProvidersOauthCallbackData, GetProvidersOauthCallbackErrors, GetProvidersOauthCallbackResponses, GetProvidersResponses, GetProviderTemplatesByIdData, GetProviderTemplatesByIdErrors, GetProviderTemplatesByIdResponses, GetProviderTemplatesData, GetProviderTemplatesErrors, GetProviderTemplatesResponses, GetSearchProvidersByIdData, GetSearchProvidersByIdErrors, GetSearchProvidersByIdResponses, GetSearchProvidersData, GetSearchProvidersErrors, GetSearchProvidersMetaData, GetSearchProvidersMetaResponses, GetSearchProvidersResponses, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesErrors, GetSpeechModelsByIdCapabilitiesResponses, GetSpeechModelsByIdData, GetSpeechModelsByIdErrors, GetSpeechModelsByIdResponses, GetSpeechModelsData, GetSpeechModelsErrors, GetSpeechModelsResponses, GetSpeechProvidersByIdData, GetSpeechProvidersByIdErrors, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsErrors, GetSpeechProvidersByIdModelsResponses, GetSpeechProvidersByIdResponses, GetSpeechProvidersData, GetSpeechProvidersErrors, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponses, GetSpeechProvidersResponses, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestErrors, GetSupermarketArtifactsIconByDigestResponses, GetSupermarketPackagesData, GetSupermarketPackagesErrors, GetSupermarketPackagesResponses, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesErrors, GetSupermarketRegistriesByRegistryIdCategoriesResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponses, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesErrors, GetSupermarketRegistriesByRegistryIdPackagesResponses, GetSupermarketRegistriesData, GetSupermarketRegistriesErrors, GetSupermarketRegistriesResponses, GetSupermarketSkillsData, GetSupermarketSkillsErrors, GetSupermarketSkillsResponses, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesErrors, GetTranscriptionModelsByIdCapabilitiesResponses, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdErrors, GetTranscriptionModelsByIdResponses, GetTranscriptionModelsData, GetTranscriptionModelsErrors, GetTranscriptionModelsResponses, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdErrors, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsErrors, GetTranscriptionProvidersByIdModelsResponses, GetTranscriptionProvidersByIdResponses, GetTranscriptionProvidersData, GetTranscriptionProvidersErrors, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponses, GetTranscriptionProvidersResponses, GetUsersByIdData, GetUsersByIdErrors, GetUsersByIdResponses, GetUsersData, GetUsersErrors, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesErrors, GetUsersMeChannelIdentitiesResponses, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformErrors, GetUsersMeChannelsByPlatformResponses, GetUsersMeComputerAccessData, GetUsersMeComputerAccessErrors, GetUsersMeComputerAccessResponses, GetUsersMeData, GetUsersMeErrors, GetUsersMeResponses, GetUsersMeRuntimesData, GetUsersMeRuntimesErrors, GetUsersMeRuntimesResponses, GetUsersResponses, GetVideoModelsByIdData, GetVideoModelsByIdErrors, GetVideoModelsByIdResponses, GetVideoModelsData, GetVideoModelsErrors, GetVideoModelsResponses, GetVideoProvidersByIdData, GetVideoProvidersByIdErrors, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsErrors, GetVideoProvidersByIdModelsResponses, GetVideoProvidersByIdResponses, GetVideoProvidersData, GetVideoProvidersErrors, GetVideoProvidersMetaData, GetVideoProvidersMetaResponses, GetVideoProvidersResponses, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponses, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdErrors, PatchBotsByBotIdAgentsByIdResponses, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdErrors, PatchBotsByBotIdConnectorsByConnectionIdResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponses, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdErrors, PatchBotsByBotIdSessionsBySessionIdResponses, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdErrors, PatchBotsByBotIdWorkdirsByWorkdirIdResponses, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusErrors, PatchBotsByIdChannelByPlatformStatusResponses, PostAuthLoginData, PostAuthLoginErrors, PostAuthLoginResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostBotsBackupImportData, PostBotsBackupImportErrors, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewErrors, PostBotsBackupImportPreviewResponses, PostBotsBackupImportResponses, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesErrors, PostBotsByBotIdAclRulesResponses, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesErrors, PostBotsByBotIdAcpRuntimesResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelResponses, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollErrors, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponses, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsErrors, PostBotsByBotIdAgentsResponses, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportErrors, PostBotsByBotIdBackupExportResponses, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersErrors, PostBotsByBotIdChannelManagersResponses, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyErrors, PostBotsByBotIdConnectorsApiKeyResponses, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthErrors, PostBotsByBotIdConnectorsByConnectionIdReauthResponses, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthErrors, PostBotsByBotIdConnectorsOauthResponses, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveErrors, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponses, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsErrors, PostBotsByBotIdContainerBrowserSessionsResponses, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreErrors, PostBotsByBotIdContainerDataRestoreResponses, PostBotsByBotIdContainerDisplayPrepareData, PostBotsByBotIdContainerDisplayPrepareErrors, PostBotsByBotIdContainerDisplayPrepareResponse, PostBotsByBotIdContainerDisplayPrepareResponses, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferErrors, PostBotsByBotIdContainerDisplayWebrtcOfferResponses, PostBotsByBotIdContainerErrors, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveErrors, PostBotsByBotIdContainerFsArchiveResponses, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteErrors, PostBotsByBotIdContainerFsDeleteResponses, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractErrors, PostBotsByBotIdContainerFsExtractResponses, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirErrors, PostBotsByBotIdContainerFsMkdirResponses, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameErrors, PostBotsByBotIdContainerFsRenameResponses, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadErrors, PostBotsByBotIdContainerFsUploadResponses, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteErrors, PostBotsByBotIdContainerFsWriteResponses, PostBotsByBotIdContainerResponses, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsErrors, PostBotsByBotIdContainerSkillsActionsResponses, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsErrors, PostBotsByBotIdContainerSkillsResponses, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsErrors, PostBotsByBotIdContainerSnapshotsResponses, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackErrors, PostBotsByBotIdContainerSnapshotsRollbackResponses, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartErrors, PostBotsByBotIdContainerStartResponses, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopErrors, PostBotsByBotIdContainerStopResponses, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsErrors, PostBotsByBotIdEmailBindingsResponses, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestErrors, PostBotsByBotIdHooksTestResponses, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeErrors, PostBotsByBotIdMcpByIdOauthAuthorizeResponses, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverErrors, PostBotsByBotIdMcpByIdOauthDiscoverResponses, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeErrors, PostBotsByBotIdMcpByIdOauthExchangeResponses, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeErrors, PostBotsByBotIdMcpByIdProbeResponses, PostBotsByBotIdMcpData, PostBotsByBotIdMcpErrors, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteErrors, PostBotsByBotIdMcpOpsBatchDeleteResponses, PostBotsByBotIdMcpResponses, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdErrors, PostBotsByBotIdMcpStdioByConnectionIdResponses, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioErrors, PostBotsByBotIdMcpStdioResponses, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactErrors, PostBotsByBotIdMemoryCompactResponses, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryErrors, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestErrors, PostBotsByBotIdMemoryIngestResponses, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildErrors, PostBotsByBotIdMemoryRebuildResponses, PostBotsByBotIdMemoryResponses, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchErrors, PostBotsByBotIdMemorySearchResponses, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteErrors, PostBotsByBotIdQuickActionsExecuteResponses, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleErrors, PostBotsByBotIdScheduleResponses, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactErrors, PostBotsByBotIdSessionsBySessionIdCompactResponses, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkErrors, PostBotsByBotIdSessionsBySessionIdForkResponses, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsErrors, PostBotsByBotIdSessionsResponses, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsErrors, PostBotsByBotIdSettingsResponses, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageErrors, PostBotsByBotIdSupermarketInstallPackageResponses, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveErrors, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponses, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectErrors, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponses, PostBotsByBotIdToolsData, PostBotsByBotIdToolsErrors, PostBotsByBotIdToolsResponses, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeErrors, PostBotsByBotIdTtsSynthesizeResponses, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessErrors, PostBotsByBotIdUserAccessResponses, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesErrors, PostBotsByBotIdWebMessagesResponses, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsErrors, PostBotsByBotIdWorkdirsResponses, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatErrors, PostBotsByIdChannelByPlatformSendChatResponses, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendErrors, PostBotsByIdChannelByPlatformSendResponses, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointErrors, PostBotsByIdChannelByPlatformWebhookEndpointResponses, PostBotsData, PostBotsErrors, PostBotsResponses, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdErrors, PostEmailMailgunWebhookByConfigIdResponses, PostEmailProvidersData, PostEmailProvidersErrors, PostEmailProvidersResponses, PostFetchProvidersData, PostFetchProvidersErrors, PostFetchProvidersResponses, PostMemoryProvidersData, PostMemoryProvidersErrors, PostMemoryProvidersResponses, PostModelsByIdTestData, PostModelsByIdTestErrors, PostModelsByIdTestResponses, PostModelsData, PostModelsErrors, PostModelsResponses, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsErrors, PostProvidersByIdImportModelsResponses, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollErrors, PostProvidersByIdOauthPollResponses, PostProvidersByIdTestData, PostProvidersByIdTestErrors, PostProvidersByIdTestResponses, PostProvidersData, PostProvidersErrors, PostProvidersFromTemplateData, PostProvidersFromTemplateErrors, PostProvidersFromTemplateResponses, PostProvidersResponses, PostSearchProvidersData, PostSearchProvidersErrors, PostSearchProvidersResponses, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestErrors, PostSpeechModelsByIdTestResponses, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsErrors, PostSpeechProvidersByIdImportModelsResponses, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestErrors, PostTranscriptionModelsByIdTestResponses, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsErrors, PostTranscriptionProvidersByIdImportModelsResponses, PostUsersData, PostUsersErrors, PostUsersMeChannelLinksData, PostUsersMeChannelLinksErrors, PostUsersMeChannelLinksResponses, PostUsersMeRuntimesData, PostUsersMeRuntimesErrors, PostUsersMeRuntimesResponses, PostUsersResponses, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsErrors, PostVideoProvidersByIdImportModelsResponses, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectErrors, PutBotsByBotIdAclDefaultEffectResponses, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdErrors, PutBotsByBotIdAclRulesByRuleIdResponses, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialErrors, PutBotsByBotIdAgentsByIdCredentialResponses, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsErrors, PutBotsByBotIdContainerMetricsResponses, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdErrors, PutBotsByBotIdEmailBindingsByIdResponses, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdErrors, PutBotsByBotIdMcpByIdResponses, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportErrors, PutBotsByBotIdMcpOpsImportResponses, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdErrors, PutBotsByBotIdMemoryByMemoryIdResponses, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdErrors, PutBotsByBotIdScheduleByIdResponses, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsErrors, PutBotsByBotIdSettingsResponses, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdErrors, PutBotsByBotIdUserAccessByGrantIdResponses, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalErrors, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalResponses, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryErrors, PutBotsByBotIdWorkspaceTargetsPrimaryResponses, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdErrors, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponses, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformErrors, PutBotsByIdChannelByPlatformResponses, PutBotsByIdData, PutBotsByIdErrors, PutBotsByIdOwnerData, PutBotsByIdOwnerErrors, PutBotsByIdOwnerResponses, PutBotsByIdResponses, PutEmailProvidersByIdData, PutEmailProvidersByIdErrors, PutEmailProvidersByIdResponses, PutFetchProvidersByIdData, PutFetchProvidersByIdErrors, PutFetchProvidersByIdResponses, PutMemoryProvidersByIdData, PutMemoryProvidersByIdErrors, PutMemoryProvidersByIdResponses, PutModelsByIdData, PutModelsByIdErrors, PutModelsByIdResponses, PutModelsModelByModelIdData, PutModelsModelByModelIdErrors, PutModelsModelByModelIdResponses, PutProvidersByIdData, PutProvidersByIdErrors, PutProvidersByIdResponses, PutSearchProvidersByIdData, PutSearchProvidersByIdErrors, PutSearchProvidersByIdResponses, PutSpeechModelsByIdData, PutSpeechModelsByIdErrors, PutSpeechModelsByIdResponses, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdErrors, PutTranscriptionModelsByIdResponses, PutUsersByIdData, PutUsersByIdErrors, PutUsersByIdResponses, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformErrors, PutUsersMeChannelsByPlatformResponses, PutUsersMeData, PutUsersMeErrors, PutUsersMePasswordData, PutUsersMePasswordErrors, PutUsersMePasswordResponses, PutUsersMeResponses, PutVideoModelsByIdData, PutVideoModelsByIdErrors, PutVideoModelsByIdResponses } from './types.gen'; +import type { DeleteBotsByBotIdAclRulesByRuleIdData, DeleteBotsByBotIdAclRulesByRuleIdErrors, DeleteBotsByBotIdAclRulesByRuleIdResponses, DeleteBotsByBotIdAcpRuntimesByRuntimeIdData, DeleteBotsByBotIdAcpRuntimesByRuntimeIdErrors, DeleteBotsByBotIdAcpRuntimesByRuntimeIdResponses, DeleteBotsByBotIdAgentsByIdCredentialData, DeleteBotsByBotIdAgentsByIdCredentialErrors, DeleteBotsByBotIdAgentsByIdCredentialResponses, DeleteBotsByBotIdAgentsByIdData, DeleteBotsByBotIdAgentsByIdErrors, DeleteBotsByBotIdAgentsByIdResponses, DeleteBotsByBotIdChannelManagersByChannelIdentityIdData, DeleteBotsByBotIdChannelManagersByChannelIdentityIdErrors, DeleteBotsByBotIdChannelManagersByChannelIdentityIdResponses, DeleteBotsByBotIdCompactionLogsData, DeleteBotsByBotIdCompactionLogsErrors, DeleteBotsByBotIdCompactionLogsResponses, DeleteBotsByBotIdConnectorsByConnectionIdData, DeleteBotsByBotIdConnectorsByConnectionIdErrors, DeleteBotsByBotIdConnectorsByConnectionIdResponses, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdData, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdErrors, DeleteBotsByBotIdContainerBrowserSessionsBySessionIdResponses, DeleteBotsByBotIdContainerData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdData, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdErrors, DeleteBotsByBotIdContainerDisplaySessionsBySessionIdResponses, DeleteBotsByBotIdContainerErrors, DeleteBotsByBotIdContainerResponses, DeleteBotsByBotIdContainerSkillsData, DeleteBotsByBotIdContainerSkillsErrors, DeleteBotsByBotIdContainerSkillsResponses, DeleteBotsByBotIdEmailBindingsByIdData, DeleteBotsByBotIdEmailBindingsByIdErrors, DeleteBotsByBotIdEmailBindingsByIdResponses, DeleteBotsByBotIdMcpByIdData, DeleteBotsByBotIdMcpByIdErrors, DeleteBotsByBotIdMcpByIdOauthTokenData, DeleteBotsByBotIdMcpByIdOauthTokenErrors, DeleteBotsByBotIdMcpByIdOauthTokenResponses, DeleteBotsByBotIdMcpByIdResponses, DeleteBotsByBotIdMemoryByIdData, DeleteBotsByBotIdMemoryByIdErrors, DeleteBotsByBotIdMemoryByIdResponses, DeleteBotsByBotIdMemoryData, DeleteBotsByBotIdMemoryErrors, DeleteBotsByBotIdMemoryResponses, DeleteBotsByBotIdMessagesData, DeleteBotsByBotIdMessagesErrors, DeleteBotsByBotIdMessagesResponses, DeleteBotsByBotIdScheduleByIdData, DeleteBotsByBotIdScheduleByIdErrors, DeleteBotsByBotIdScheduleByIdResponses, DeleteBotsByBotIdScheduleLogsData, DeleteBotsByBotIdScheduleLogsErrors, DeleteBotsByBotIdScheduleLogsResponses, DeleteBotsByBotIdSessionsBySessionIdData, DeleteBotsByBotIdSessionsBySessionIdErrors, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdErrors, DeleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponses, DeleteBotsByBotIdSessionsBySessionIdResponses, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdErrors, DeleteBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponses, DeleteBotsByBotIdSettingsData, DeleteBotsByBotIdSettingsErrors, DeleteBotsByBotIdSettingsResponses, DeleteBotsByBotIdSupermarketPackagesByInstallationIdData, DeleteBotsByBotIdSupermarketPackagesByInstallationIdErrors, DeleteBotsByBotIdSupermarketPackagesByInstallationIdResponses, DeleteBotsByBotIdUserAccessByGrantIdData, DeleteBotsByBotIdUserAccessByGrantIdErrors, DeleteBotsByBotIdUserAccessByGrantIdResponses, DeleteBotsByBotIdWorkdirsByWorkdirIdData, DeleteBotsByBotIdWorkdirsByWorkdirIdErrors, DeleteBotsByBotIdWorkdirsByWorkdirIdResponses, DeleteBotsByBotIdWorkspaceTargetsByTargetIdData, DeleteBotsByBotIdWorkspaceTargetsByTargetIdErrors, DeleteBotsByBotIdWorkspaceTargetsByTargetIdResponses, DeleteBotsByIdChannelByPlatformData, DeleteBotsByIdChannelByPlatformErrors, DeleteBotsByIdChannelByPlatformResponses, DeleteBotsByIdData, DeleteBotsByIdErrors, DeleteBotsByIdResponses, DeleteEmailProvidersByIdData, DeleteEmailProvidersByIdErrors, DeleteEmailProvidersByIdOauthTokenData, DeleteEmailProvidersByIdOauthTokenErrors, DeleteEmailProvidersByIdOauthTokenResponses, DeleteEmailProvidersByIdResponses, DeleteFetchProvidersByIdData, DeleteFetchProvidersByIdErrors, DeleteFetchProvidersByIdResponses, DeleteMemoryProvidersByIdData, DeleteMemoryProvidersByIdErrors, DeleteMemoryProvidersByIdResponses, DeleteModelsByIdData, DeleteModelsByIdErrors, DeleteModelsByIdResponses, DeleteModelsModelByModelIdData, DeleteModelsModelByModelIdErrors, DeleteModelsModelByModelIdResponses, DeleteProvidersByIdData, DeleteProvidersByIdErrors, DeleteProvidersByIdOauthTokenData, DeleteProvidersByIdOauthTokenErrors, DeleteProvidersByIdOauthTokenResponses, DeleteProvidersByIdResponses, DeleteSearchProvidersByIdData, DeleteSearchProvidersByIdErrors, DeleteSearchProvidersByIdResponses, DeleteUsersByIdData, DeleteUsersByIdErrors, DeleteUsersByIdResponses, DeleteUsersMeChannelIdentitiesByChannelIdentityIdData, DeleteUsersMeChannelIdentitiesByChannelIdentityIdErrors, DeleteUsersMeChannelIdentitiesByChannelIdentityIdResponses, DeleteUsersMeRuntimesByIdData, DeleteUsersMeRuntimesByIdErrors, DeleteUsersMeRuntimesByIdResponses, GetAcpProfilesData, GetAcpProfilesResponses, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsData, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsErrors, GetBotsByBotIdAclChannelIdentitiesByChannelIdentityIdConversationsResponses, GetBotsByBotIdAclChannelIdentitiesData, GetBotsByBotIdAclChannelIdentitiesErrors, GetBotsByBotIdAclChannelIdentitiesResponses, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsData, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsErrors, GetBotsByBotIdAclChannelTypesByChannelTypeConversationsResponses, GetBotsByBotIdAclDefaultEffectData, GetBotsByBotIdAclDefaultEffectErrors, GetBotsByBotIdAclDefaultEffectResponses, GetBotsByBotIdAclRulesData, GetBotsByBotIdAclRulesErrors, GetBotsByBotIdAclRulesResponses, GetBotsByBotIdAcpRuntimesByRuntimeIdData, GetBotsByBotIdAcpRuntimesByRuntimeIdErrors, GetBotsByBotIdAcpRuntimesByRuntimeIdResponses, GetBotsByBotIdAgentsByIdCredentialData, GetBotsByBotIdAgentsByIdCredentialErrors, GetBotsByBotIdAgentsByIdCredentialResponses, GetBotsByBotIdAgentsByIdData, GetBotsByBotIdAgentsByIdErrors, GetBotsByBotIdAgentsByIdModelsData, GetBotsByBotIdAgentsByIdModelsErrors, GetBotsByBotIdAgentsByIdModelsResponses, GetBotsByBotIdAgentsByIdResponses, GetBotsByBotIdAgentsData, GetBotsByBotIdAgentsErrors, GetBotsByBotIdAgentsResponses, GetBotsByBotIdBackupSummaryData, GetBotsByBotIdBackupSummaryErrors, GetBotsByBotIdBackupSummaryResponses, GetBotsByBotIdChannelManagersData, GetBotsByBotIdChannelManagersErrors, GetBotsByBotIdChannelManagersResponses, GetBotsByBotIdCompactionLogsData, GetBotsByBotIdCompactionLogsErrors, GetBotsByBotIdCompactionLogsResponses, GetBotsByBotIdConnectorsByConnectionIdData, GetBotsByBotIdConnectorsByConnectionIdErrors, GetBotsByBotIdConnectorsByConnectionIdResponses, GetBotsByBotIdConnectorsData, GetBotsByBotIdConnectorsErrors, GetBotsByBotIdConnectorsResponses, GetBotsByBotIdContainerData, GetBotsByBotIdContainerDisplayData, GetBotsByBotIdContainerDisplayErrors, GetBotsByBotIdContainerDisplayResponses, GetBotsByBotIdContainerDisplaySessionsData, GetBotsByBotIdContainerDisplaySessionsErrors, GetBotsByBotIdContainerDisplaySessionsResponses, GetBotsByBotIdContainerErrors, GetBotsByBotIdContainerFsData, GetBotsByBotIdContainerFsDownloadData, GetBotsByBotIdContainerFsDownloadErrors, GetBotsByBotIdContainerFsDownloadResponses, GetBotsByBotIdContainerFsErrors, GetBotsByBotIdContainerFsListData, GetBotsByBotIdContainerFsListErrors, GetBotsByBotIdContainerFsListResponses, GetBotsByBotIdContainerFsReadData, GetBotsByBotIdContainerFsReadErrors, GetBotsByBotIdContainerFsReadResponses, GetBotsByBotIdContainerFsResponses, GetBotsByBotIdContainerMetricsData, GetBotsByBotIdContainerMetricsErrors, GetBotsByBotIdContainerMetricsResponses, GetBotsByBotIdContainerResponses, GetBotsByBotIdContainerSkillsData, GetBotsByBotIdContainerSkillsErrors, GetBotsByBotIdContainerSkillsResponses, GetBotsByBotIdContainerSnapshotsData, GetBotsByBotIdContainerSnapshotsErrors, GetBotsByBotIdContainerSnapshotsResponses, GetBotsByBotIdContainerTerminalData, GetBotsByBotIdContainerTerminalErrors, GetBotsByBotIdContainerTerminalResponses, GetBotsByBotIdContainerTerminalWsData, GetBotsByBotIdContainerTerminalWsErrors, GetBotsByBotIdEmailBindingsData, GetBotsByBotIdEmailBindingsErrors, GetBotsByBotIdEmailBindingsResponses, GetBotsByBotIdEmailOutboxByIdData, GetBotsByBotIdEmailOutboxByIdErrors, GetBotsByBotIdEmailOutboxByIdResponses, GetBotsByBotIdEmailOutboxData, GetBotsByBotIdEmailOutboxErrors, GetBotsByBotIdEmailOutboxResponses, GetBotsByBotIdHooksEventsData, GetBotsByBotIdHooksEventsErrors, GetBotsByBotIdHooksEventsResponses, GetBotsByBotIdMcpByIdData, GetBotsByBotIdMcpByIdErrors, GetBotsByBotIdMcpByIdOauthStatusData, GetBotsByBotIdMcpByIdOauthStatusErrors, GetBotsByBotIdMcpByIdOauthStatusResponses, GetBotsByBotIdMcpByIdResponses, GetBotsByBotIdMcpData, GetBotsByBotIdMcpErrors, GetBotsByBotIdMcpOpsExportData, GetBotsByBotIdMcpOpsExportErrors, GetBotsByBotIdMcpOpsExportResponses, GetBotsByBotIdMcpResponses, GetBotsByBotIdMemoryData, GetBotsByBotIdMemoryErrors, GetBotsByBotIdMemoryGraphData, GetBotsByBotIdMemoryGraphErrors, GetBotsByBotIdMemoryGraphResponses, GetBotsByBotIdMemoryResponses, GetBotsByBotIdMemoryStatusData, GetBotsByBotIdMemoryStatusErrors, GetBotsByBotIdMemoryStatusResponses, GetBotsByBotIdMemoryUsageData, GetBotsByBotIdMemoryUsageErrors, GetBotsByBotIdMemoryUsageResponses, GetBotsByBotIdMessagesData, GetBotsByBotIdMessagesErrors, GetBotsByBotIdMessagesLocateData, GetBotsByBotIdMessagesLocateErrors, GetBotsByBotIdMessagesLocateResponses, GetBotsByBotIdMessagesResponses, GetBotsByBotIdScheduleByIdData, GetBotsByBotIdScheduleByIdErrors, GetBotsByBotIdScheduleByIdLogsData, GetBotsByBotIdScheduleByIdLogsErrors, GetBotsByBotIdScheduleByIdLogsResponses, GetBotsByBotIdScheduleByIdResponses, GetBotsByBotIdScheduleData, GetBotsByBotIdScheduleErrors, GetBotsByBotIdScheduleLogsData, GetBotsByBotIdScheduleLogsErrors, GetBotsByBotIdScheduleLogsResponses, GetBotsByBotIdScheduleResponses, GetBotsByBotIdSessionsBySessionIdAcpRuntimeData, GetBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, GetBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, GetBotsByBotIdSessionsBySessionIdContextLifecycleData, GetBotsByBotIdSessionsBySessionIdContextLifecycleErrors, GetBotsByBotIdSessionsBySessionIdContextLifecycleResponses, GetBotsByBotIdSessionsBySessionIdData, GetBotsByBotIdSessionsBySessionIdErrors, GetBotsByBotIdSessionsBySessionIdFollowUpQueueData, GetBotsByBotIdSessionsBySessionIdFollowUpQueueErrors, GetBotsByBotIdSessionsBySessionIdFollowUpQueueResponses, GetBotsByBotIdSessionsBySessionIdQueueData, GetBotsByBotIdSessionsBySessionIdQueueErrors, GetBotsByBotIdSessionsBySessionIdQueueResponses, GetBotsByBotIdSessionsBySessionIdResponses, GetBotsByBotIdSessionsBySessionIdStatusData, GetBotsByBotIdSessionsBySessionIdStatusErrors, GetBotsByBotIdSessionsBySessionIdStatusResponses, GetBotsByBotIdSessionsBySessionIdSteerQueueData, GetBotsByBotIdSessionsBySessionIdSteerQueueErrors, GetBotsByBotIdSessionsBySessionIdSteerQueueResponses, GetBotsByBotIdSessionsData, GetBotsByBotIdSessionsErrors, GetBotsByBotIdSessionsEventsData, GetBotsByBotIdSessionsEventsErrors, GetBotsByBotIdSessionsEventsResponse, GetBotsByBotIdSessionsEventsResponses, GetBotsByBotIdSessionsModelPreferenceSeedData, GetBotsByBotIdSessionsModelPreferenceSeedErrors, GetBotsByBotIdSessionsModelPreferenceSeedResponses, GetBotsByBotIdSessionsResponses, GetBotsByBotIdSettingsData, GetBotsByBotIdSettingsErrors, GetBotsByBotIdSettingsResponses, GetBotsByBotIdSkillsCatalogData, GetBotsByBotIdSkillsCatalogErrors, GetBotsByBotIdSkillsCatalogResponses, GetBotsByBotIdSupermarketPackagesData, GetBotsByBotIdSupermarketPackagesErrors, GetBotsByBotIdSupermarketPackagesResponses, GetBotsByBotIdTokenUsageData, GetBotsByBotIdTokenUsageErrors, GetBotsByBotIdTokenUsageRecordsData, GetBotsByBotIdTokenUsageRecordsErrors, GetBotsByBotIdTokenUsageRecordsResponses, GetBotsByBotIdTokenUsageResponses, GetBotsByBotIdUserAccessCandidatesData, GetBotsByBotIdUserAccessCandidatesErrors, GetBotsByBotIdUserAccessCandidatesResponses, GetBotsByBotIdUserAccessData, GetBotsByBotIdUserAccessErrors, GetBotsByBotIdUserAccessResponses, GetBotsByBotIdWebStreamData, GetBotsByBotIdWebStreamErrors, GetBotsByBotIdWebStreamResponse, GetBotsByBotIdWebStreamResponses, GetBotsByBotIdWebWsData, GetBotsByBotIdWebWsErrors, GetBotsByBotIdWorkdirsData, GetBotsByBotIdWorkdirsErrors, GetBotsByBotIdWorkdirsResponses, GetBotsByBotIdWorkspaceTargetsData, GetBotsByBotIdWorkspaceTargetsErrors, GetBotsByBotIdWorkspaceTargetsResponses, GetBotsByIdChannelByPlatformData, GetBotsByIdChannelByPlatformErrors, GetBotsByIdChannelByPlatformResponses, GetBotsByIdChecksData, GetBotsByIdChecksErrors, GetBotsByIdChecksResponses, GetBotsByIdData, GetBotsByIdErrors, GetBotsByIdResponses, GetBotsData, GetBotsErrors, GetBotsNameAvailabilityData, GetBotsNameAvailabilityErrors, GetBotsNameAvailabilityResponses, GetBotsResponses, GetBotsUserAccessCandidatesData, GetBotsUserAccessCandidatesErrors, GetBotsUserAccessCandidatesResponses, GetChannelsByPlatformData, GetChannelsByPlatformErrors, GetChannelsByPlatformResponses, GetChannelsData, GetChannelsErrors, GetChannelsResponses, GetConnectorsCatalogData, GetConnectorsCatalogErrors, GetConnectorsCatalogResponses, GetEmailOauthCallbackData, GetEmailOauthCallbackErrors, GetEmailOauthCallbackResponses, GetEmailProvidersByIdData, GetEmailProvidersByIdErrors, GetEmailProvidersByIdOauthAuthorizeData, GetEmailProvidersByIdOauthAuthorizeErrors, GetEmailProvidersByIdOauthAuthorizeResponses, GetEmailProvidersByIdOauthStatusData, GetEmailProvidersByIdOauthStatusErrors, GetEmailProvidersByIdOauthStatusResponses, GetEmailProvidersByIdResponses, GetEmailProvidersData, GetEmailProvidersErrors, GetEmailProvidersMetaData, GetEmailProvidersMetaResponses, GetEmailProvidersResponses, GetFetchProvidersByIdData, GetFetchProvidersByIdErrors, GetFetchProvidersByIdResponses, GetFetchProvidersData, GetFetchProvidersErrors, GetFetchProvidersMetaData, GetFetchProvidersMetaResponses, GetFetchProvidersResponses, GetMemoryProvidersByIdData, GetMemoryProvidersByIdErrors, GetMemoryProvidersByIdResponses, GetMemoryProvidersByIdStatusData, GetMemoryProvidersByIdStatusErrors, GetMemoryProvidersByIdStatusResponses, GetMemoryProvidersData, GetMemoryProvidersErrors, GetMemoryProvidersMetaData, GetMemoryProvidersMetaResponses, GetMemoryProvidersResponses, GetModelsByIdData, GetModelsByIdErrors, GetModelsByIdResponses, GetModelsCountData, GetModelsCountErrors, GetModelsCountResponses, GetModelsData, GetModelsErrors, GetModelsModelByModelIdData, GetModelsModelByModelIdErrors, GetModelsModelByModelIdResponses, GetModelsResponses, GetOauthMcpCallbackData, GetOauthMcpCallbackErrors, GetOauthMcpCallbackResponses, GetPingData, GetPingResponses, GetProvidersByIdData, GetProvidersByIdErrors, GetProvidersByIdModelsData, GetProvidersByIdModelsErrors, GetProvidersByIdModelsResponses, GetProvidersByIdOauthAuthorizeData, GetProvidersByIdOauthAuthorizeErrors, GetProvidersByIdOauthAuthorizeResponses, GetProvidersByIdOauthStatusData, GetProvidersByIdOauthStatusErrors, GetProvidersByIdOauthStatusResponses, GetProvidersByIdResponses, GetProvidersCountData, GetProvidersCountErrors, GetProvidersCountResponses, GetProvidersData, GetProvidersErrors, GetProvidersNameByNameData, GetProvidersNameByNameErrors, GetProvidersNameByNameResponses, GetProvidersOauthCallbackData, GetProvidersOauthCallbackErrors, GetProvidersOauthCallbackResponses, GetProvidersResponses, GetProviderTemplatesByIdData, GetProviderTemplatesByIdErrors, GetProviderTemplatesByIdResponses, GetProviderTemplatesData, GetProviderTemplatesErrors, GetProviderTemplatesResponses, GetSearchProvidersByIdData, GetSearchProvidersByIdErrors, GetSearchProvidersByIdResponses, GetSearchProvidersData, GetSearchProvidersErrors, GetSearchProvidersMetaData, GetSearchProvidersMetaResponses, GetSearchProvidersResponses, GetSpeechModelsByIdCapabilitiesData, GetSpeechModelsByIdCapabilitiesErrors, GetSpeechModelsByIdCapabilitiesResponses, GetSpeechModelsByIdData, GetSpeechModelsByIdErrors, GetSpeechModelsByIdResponses, GetSpeechModelsData, GetSpeechModelsErrors, GetSpeechModelsResponses, GetSpeechProvidersByIdData, GetSpeechProvidersByIdErrors, GetSpeechProvidersByIdModelsData, GetSpeechProvidersByIdModelsErrors, GetSpeechProvidersByIdModelsResponses, GetSpeechProvidersByIdResponses, GetSpeechProvidersData, GetSpeechProvidersErrors, GetSpeechProvidersMetaData, GetSpeechProvidersMetaResponses, GetSpeechProvidersResponses, GetSupermarketArtifactsIconByDigestData, GetSupermarketArtifactsIconByDigestErrors, GetSupermarketArtifactsIconByDigestResponses, GetSupermarketPackagesData, GetSupermarketPackagesErrors, GetSupermarketPackagesResponses, GetSupermarketRegistriesByRegistryIdCategoriesData, GetSupermarketRegistriesByRegistryIdCategoriesErrors, GetSupermarketRegistriesByRegistryIdCategoriesResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdReleasesByRevisionResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdResponses, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdData, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdErrors, GetSupermarketRegistriesByRegistryIdPackagesByPackageIdSkillsBySkillIdResponses, GetSupermarketRegistriesByRegistryIdPackagesData, GetSupermarketRegistriesByRegistryIdPackagesErrors, GetSupermarketRegistriesByRegistryIdPackagesResponses, GetSupermarketRegistriesData, GetSupermarketRegistriesErrors, GetSupermarketRegistriesResponses, GetSupermarketSkillsData, GetSupermarketSkillsErrors, GetSupermarketSkillsResponses, GetTranscriptionModelsByIdCapabilitiesData, GetTranscriptionModelsByIdCapabilitiesErrors, GetTranscriptionModelsByIdCapabilitiesResponses, GetTranscriptionModelsByIdData, GetTranscriptionModelsByIdErrors, GetTranscriptionModelsByIdResponses, GetTranscriptionModelsData, GetTranscriptionModelsErrors, GetTranscriptionModelsResponses, GetTranscriptionProvidersByIdData, GetTranscriptionProvidersByIdErrors, GetTranscriptionProvidersByIdModelsData, GetTranscriptionProvidersByIdModelsErrors, GetTranscriptionProvidersByIdModelsResponses, GetTranscriptionProvidersByIdResponses, GetTranscriptionProvidersData, GetTranscriptionProvidersErrors, GetTranscriptionProvidersMetaData, GetTranscriptionProvidersMetaResponses, GetTranscriptionProvidersResponses, GetUsersByIdData, GetUsersByIdErrors, GetUsersByIdResponses, GetUsersData, GetUsersErrors, GetUsersMeChannelIdentitiesData, GetUsersMeChannelIdentitiesErrors, GetUsersMeChannelIdentitiesResponses, GetUsersMeChannelsByPlatformData, GetUsersMeChannelsByPlatformErrors, GetUsersMeChannelsByPlatformResponses, GetUsersMeComputerAccessData, GetUsersMeComputerAccessErrors, GetUsersMeComputerAccessResponses, GetUsersMeData, GetUsersMeErrors, GetUsersMeResponses, GetUsersMeRuntimesData, GetUsersMeRuntimesErrors, GetUsersMeRuntimesResponses, GetUsersResponses, GetVideoModelsByIdData, GetVideoModelsByIdErrors, GetVideoModelsByIdResponses, GetVideoModelsData, GetVideoModelsErrors, GetVideoModelsResponses, GetVideoProvidersByIdData, GetVideoProvidersByIdErrors, GetVideoProvidersByIdModelsData, GetVideoProvidersByIdModelsErrors, GetVideoProvidersByIdModelsResponses, GetVideoProvidersByIdResponses, GetVideoProvidersData, GetVideoProvidersErrors, GetVideoProvidersMetaData, GetVideoProvidersMetaResponses, GetVideoProvidersResponses, GetWebhookTunnelStatusData, GetWebhookTunnelStatusResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelData, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdModelResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdModeResponses, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningData, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningErrors, PatchBotsByBotIdAcpRuntimesByRuntimeIdReasoningResponses, PatchBotsByBotIdAgentsByIdData, PatchBotsByBotIdAgentsByIdErrors, PatchBotsByBotIdAgentsByIdResponses, PatchBotsByBotIdConnectorsByConnectionIdData, PatchBotsByBotIdConnectorsByConnectionIdErrors, PatchBotsByBotIdConnectorsByConnectionIdResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModelResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeModeResponses, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningData, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningErrors, PatchBotsByBotIdSessionsBySessionIdAcpRuntimeReasoningResponses, PatchBotsByBotIdSessionsBySessionIdData, PatchBotsByBotIdSessionsBySessionIdErrors, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdErrors, PatchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdResponses, PatchBotsByBotIdSessionsBySessionIdResponses, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdData, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdErrors, PatchBotsByBotIdSessionsBySessionIdSteerQueueByItemIdResponses, PatchBotsByBotIdWorkdirsByWorkdirIdData, PatchBotsByBotIdWorkdirsByWorkdirIdErrors, PatchBotsByBotIdWorkdirsByWorkdirIdResponses, PatchBotsByIdChannelByPlatformStatusData, PatchBotsByIdChannelByPlatformStatusErrors, PatchBotsByIdChannelByPlatformStatusResponses, PostAuthLoginData, PostAuthLoginErrors, PostAuthLoginResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostBotsBackupImportData, PostBotsBackupImportErrors, PostBotsBackupImportPreviewData, PostBotsBackupImportPreviewErrors, PostBotsBackupImportPreviewResponses, PostBotsBackupImportResponses, PostBotsByBotIdAclRulesData, PostBotsByBotIdAclRulesErrors, PostBotsByBotIdAclRulesResponses, PostBotsByBotIdAcpRuntimesData, PostBotsByBotIdAcpRuntimesErrors, PostBotsByBotIdAcpRuntimesResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeData, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceAuthorizeResponses, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelData, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelErrors, PostBotsByBotIdAgentsByIdCodexLoginDeviceCancelResponses, PostBotsByBotIdAgentsByIdCodexLoginDevicePollData, PostBotsByBotIdAgentsByIdCodexLoginDevicePollErrors, PostBotsByBotIdAgentsByIdCodexLoginDevicePollResponses, PostBotsByBotIdAgentsData, PostBotsByBotIdAgentsErrors, PostBotsByBotIdAgentsResponses, PostBotsByBotIdBackupExportData, PostBotsByBotIdBackupExportErrors, PostBotsByBotIdBackupExportResponses, PostBotsByBotIdChannelManagersData, PostBotsByBotIdChannelManagersErrors, PostBotsByBotIdChannelManagersResponses, PostBotsByBotIdConnectorsApiKeyData, PostBotsByBotIdConnectorsApiKeyErrors, PostBotsByBotIdConnectorsApiKeyResponses, PostBotsByBotIdConnectorsByConnectionIdReauthData, PostBotsByBotIdConnectorsByConnectionIdReauthErrors, PostBotsByBotIdConnectorsByConnectionIdReauthResponses, PostBotsByBotIdConnectorsOauthData, PostBotsByBotIdConnectorsOauthErrors, PostBotsByBotIdConnectorsOauthResponses, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveData, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveErrors, PostBotsByBotIdContainerBrowserSessionsBySessionIdKeepaliveResponses, PostBotsByBotIdContainerBrowserSessionsData, PostBotsByBotIdContainerBrowserSessionsErrors, PostBotsByBotIdContainerBrowserSessionsResponses, PostBotsByBotIdContainerData, PostBotsByBotIdContainerDataRestoreData, PostBotsByBotIdContainerDataRestoreErrors, PostBotsByBotIdContainerDataRestoreResponses, PostBotsByBotIdContainerDisplayPrepareData, PostBotsByBotIdContainerDisplayPrepareErrors, PostBotsByBotIdContainerDisplayPrepareResponse, PostBotsByBotIdContainerDisplayPrepareResponses, PostBotsByBotIdContainerDisplayWebrtcOfferData, PostBotsByBotIdContainerDisplayWebrtcOfferErrors, PostBotsByBotIdContainerDisplayWebrtcOfferResponses, PostBotsByBotIdContainerErrors, PostBotsByBotIdContainerFsArchiveData, PostBotsByBotIdContainerFsArchiveErrors, PostBotsByBotIdContainerFsArchiveResponses, PostBotsByBotIdContainerFsDeleteData, PostBotsByBotIdContainerFsDeleteErrors, PostBotsByBotIdContainerFsDeleteResponses, PostBotsByBotIdContainerFsExtractData, PostBotsByBotIdContainerFsExtractErrors, PostBotsByBotIdContainerFsExtractResponses, PostBotsByBotIdContainerFsMkdirData, PostBotsByBotIdContainerFsMkdirErrors, PostBotsByBotIdContainerFsMkdirResponses, PostBotsByBotIdContainerFsRenameData, PostBotsByBotIdContainerFsRenameErrors, PostBotsByBotIdContainerFsRenameResponses, PostBotsByBotIdContainerFsUploadData, PostBotsByBotIdContainerFsUploadErrors, PostBotsByBotIdContainerFsUploadResponses, PostBotsByBotIdContainerFsWriteData, PostBotsByBotIdContainerFsWriteErrors, PostBotsByBotIdContainerFsWriteResponses, PostBotsByBotIdContainerResponses, PostBotsByBotIdContainerSkillsActionsData, PostBotsByBotIdContainerSkillsActionsErrors, PostBotsByBotIdContainerSkillsActionsResponses, PostBotsByBotIdContainerSkillsData, PostBotsByBotIdContainerSkillsErrors, PostBotsByBotIdContainerSkillsResponses, PostBotsByBotIdContainerSnapshotsData, PostBotsByBotIdContainerSnapshotsErrors, PostBotsByBotIdContainerSnapshotsResponses, PostBotsByBotIdContainerSnapshotsRollbackData, PostBotsByBotIdContainerSnapshotsRollbackErrors, PostBotsByBotIdContainerSnapshotsRollbackResponses, PostBotsByBotIdContainerStartData, PostBotsByBotIdContainerStartErrors, PostBotsByBotIdContainerStartResponses, PostBotsByBotIdContainerStopData, PostBotsByBotIdContainerStopErrors, PostBotsByBotIdContainerStopResponses, PostBotsByBotIdEmailBindingsData, PostBotsByBotIdEmailBindingsErrors, PostBotsByBotIdEmailBindingsResponses, PostBotsByBotIdHooksTestData, PostBotsByBotIdHooksTestErrors, PostBotsByBotIdHooksTestResponses, PostBotsByBotIdMcpByIdOauthAuthorizeData, PostBotsByBotIdMcpByIdOauthAuthorizeErrors, PostBotsByBotIdMcpByIdOauthAuthorizeResponses, PostBotsByBotIdMcpByIdOauthDiscoverData, PostBotsByBotIdMcpByIdOauthDiscoverErrors, PostBotsByBotIdMcpByIdOauthDiscoverResponses, PostBotsByBotIdMcpByIdOauthExchangeData, PostBotsByBotIdMcpByIdOauthExchangeErrors, PostBotsByBotIdMcpByIdOauthExchangeResponses, PostBotsByBotIdMcpByIdProbeData, PostBotsByBotIdMcpByIdProbeErrors, PostBotsByBotIdMcpByIdProbeResponses, PostBotsByBotIdMcpData, PostBotsByBotIdMcpErrors, PostBotsByBotIdMcpOpsBatchDeleteData, PostBotsByBotIdMcpOpsBatchDeleteErrors, PostBotsByBotIdMcpOpsBatchDeleteResponses, PostBotsByBotIdMcpResponses, PostBotsByBotIdMcpStdioByConnectionIdData, PostBotsByBotIdMcpStdioByConnectionIdErrors, PostBotsByBotIdMcpStdioByConnectionIdResponses, PostBotsByBotIdMcpStdioData, PostBotsByBotIdMcpStdioErrors, PostBotsByBotIdMcpStdioResponses, PostBotsByBotIdMemoryCompactData, PostBotsByBotIdMemoryCompactErrors, PostBotsByBotIdMemoryCompactResponses, PostBotsByBotIdMemoryData, PostBotsByBotIdMemoryErrors, PostBotsByBotIdMemoryIngestData, PostBotsByBotIdMemoryIngestErrors, PostBotsByBotIdMemoryIngestResponses, PostBotsByBotIdMemoryRebuildData, PostBotsByBotIdMemoryRebuildErrors, PostBotsByBotIdMemoryRebuildResponses, PostBotsByBotIdMemoryResponses, PostBotsByBotIdMemorySearchData, PostBotsByBotIdMemorySearchErrors, PostBotsByBotIdMemorySearchResponses, PostBotsByBotIdQuickActionsExecuteData, PostBotsByBotIdQuickActionsExecuteErrors, PostBotsByBotIdQuickActionsExecuteResponses, PostBotsByBotIdScheduleData, PostBotsByBotIdScheduleErrors, PostBotsByBotIdScheduleResponses, PostBotsByBotIdSessionsBySessionIdAcpRuntimeData, PostBotsByBotIdSessionsBySessionIdAcpRuntimeErrors, PostBotsByBotIdSessionsBySessionIdAcpRuntimeResponses, PostBotsByBotIdSessionsBySessionIdCompactData, PostBotsByBotIdSessionsBySessionIdCompactErrors, PostBotsByBotIdSessionsBySessionIdCompactResponses, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerErrors, PostBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteerResponses, PostBotsByBotIdSessionsBySessionIdFollowUpQueueData, PostBotsByBotIdSessionsBySessionIdFollowUpQueueErrors, PostBotsByBotIdSessionsBySessionIdFollowUpQueueResponses, PostBotsByBotIdSessionsBySessionIdForkData, PostBotsByBotIdSessionsBySessionIdForkErrors, PostBotsByBotIdSessionsBySessionIdForkResponses, PostBotsByBotIdSessionsBySessionIdSteerQueueData, PostBotsByBotIdSessionsBySessionIdSteerQueueErrors, PostBotsByBotIdSessionsBySessionIdSteerQueueResponses, PostBotsByBotIdSessionsData, PostBotsByBotIdSessionsErrors, PostBotsByBotIdSessionsResponses, PostBotsByBotIdSettingsData, PostBotsByBotIdSettingsErrors, PostBotsByBotIdSettingsResponses, PostBotsByBotIdSupermarketInstallPackageData, PostBotsByBotIdSupermarketInstallPackageErrors, PostBotsByBotIdSupermarketInstallPackageResponses, PostBotsByBotIdToolApprovalsByApprovalIdApproveData, PostBotsByBotIdToolApprovalsByApprovalIdApproveErrors, PostBotsByBotIdToolApprovalsByApprovalIdApproveResponses, PostBotsByBotIdToolApprovalsByApprovalIdRejectData, PostBotsByBotIdToolApprovalsByApprovalIdRejectErrors, PostBotsByBotIdToolApprovalsByApprovalIdRejectResponses, PostBotsByBotIdToolsData, PostBotsByBotIdToolsErrors, PostBotsByBotIdToolsResponses, PostBotsByBotIdTtsSynthesizeData, PostBotsByBotIdTtsSynthesizeErrors, PostBotsByBotIdTtsSynthesizeResponses, PostBotsByBotIdUserAccessData, PostBotsByBotIdUserAccessErrors, PostBotsByBotIdUserAccessResponses, PostBotsByBotIdWebMessagesData, PostBotsByBotIdWebMessagesErrors, PostBotsByBotIdWebMessagesResponses, PostBotsByBotIdWorkdirsData, PostBotsByBotIdWorkdirsErrors, PostBotsByBotIdWorkdirsResponses, PostBotsByIdChannelByPlatformSendChatData, PostBotsByIdChannelByPlatformSendChatErrors, PostBotsByIdChannelByPlatformSendChatResponses, PostBotsByIdChannelByPlatformSendData, PostBotsByIdChannelByPlatformSendErrors, PostBotsByIdChannelByPlatformSendResponses, PostBotsByIdChannelByPlatformWebhookEndpointData, PostBotsByIdChannelByPlatformWebhookEndpointErrors, PostBotsByIdChannelByPlatformWebhookEndpointResponses, PostBotsData, PostBotsErrors, PostBotsResponses, PostEmailMailgunWebhookByConfigIdData, PostEmailMailgunWebhookByConfigIdErrors, PostEmailMailgunWebhookByConfigIdResponses, PostEmailProvidersData, PostEmailProvidersErrors, PostEmailProvidersResponses, PostFetchProvidersData, PostFetchProvidersErrors, PostFetchProvidersResponses, PostMemoryProvidersData, PostMemoryProvidersErrors, PostMemoryProvidersResponses, PostModelsByIdTestData, PostModelsByIdTestErrors, PostModelsByIdTestResponses, PostModelsData, PostModelsErrors, PostModelsResponses, PostProvidersByIdImportModelsData, PostProvidersByIdImportModelsErrors, PostProvidersByIdImportModelsResponses, PostProvidersByIdOauthPollData, PostProvidersByIdOauthPollErrors, PostProvidersByIdOauthPollResponses, PostProvidersByIdTestData, PostProvidersByIdTestErrors, PostProvidersByIdTestResponses, PostProvidersData, PostProvidersErrors, PostProvidersFromTemplateData, PostProvidersFromTemplateErrors, PostProvidersFromTemplateResponses, PostProvidersResponses, PostSearchProvidersData, PostSearchProvidersErrors, PostSearchProvidersResponses, PostSpeechModelsByIdTestData, PostSpeechModelsByIdTestErrors, PostSpeechModelsByIdTestResponses, PostSpeechProvidersByIdImportModelsData, PostSpeechProvidersByIdImportModelsErrors, PostSpeechProvidersByIdImportModelsResponses, PostTranscriptionModelsByIdTestData, PostTranscriptionModelsByIdTestErrors, PostTranscriptionModelsByIdTestResponses, PostTranscriptionProvidersByIdImportModelsData, PostTranscriptionProvidersByIdImportModelsErrors, PostTranscriptionProvidersByIdImportModelsResponses, PostUsersData, PostUsersErrors, PostUsersMeChannelLinksData, PostUsersMeChannelLinksErrors, PostUsersMeChannelLinksResponses, PostUsersMeRuntimesData, PostUsersMeRuntimesErrors, PostUsersMeRuntimesResponses, PostUsersResponses, PostVideoProvidersByIdImportModelsData, PostVideoProvidersByIdImportModelsErrors, PostVideoProvidersByIdImportModelsResponses, PutBotsByBotIdAclDefaultEffectData, PutBotsByBotIdAclDefaultEffectErrors, PutBotsByBotIdAclDefaultEffectResponses, PutBotsByBotIdAclRulesByRuleIdData, PutBotsByBotIdAclRulesByRuleIdErrors, PutBotsByBotIdAclRulesByRuleIdResponses, PutBotsByBotIdAgentsByIdCredentialData, PutBotsByBotIdAgentsByIdCredentialErrors, PutBotsByBotIdAgentsByIdCredentialResponses, PutBotsByBotIdContainerMetricsData, PutBotsByBotIdContainerMetricsErrors, PutBotsByBotIdContainerMetricsResponses, PutBotsByBotIdEmailBindingsByIdData, PutBotsByBotIdEmailBindingsByIdErrors, PutBotsByBotIdEmailBindingsByIdResponses, PutBotsByBotIdMcpByIdData, PutBotsByBotIdMcpByIdErrors, PutBotsByBotIdMcpByIdResponses, PutBotsByBotIdMcpOpsImportData, PutBotsByBotIdMcpOpsImportErrors, PutBotsByBotIdMcpOpsImportResponses, PutBotsByBotIdMemoryByMemoryIdData, PutBotsByBotIdMemoryByMemoryIdErrors, PutBotsByBotIdMemoryByMemoryIdResponses, PutBotsByBotIdScheduleByIdData, PutBotsByBotIdScheduleByIdErrors, PutBotsByBotIdScheduleByIdResponses, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderData, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderErrors, PutBotsByBotIdSessionsBySessionIdFollowUpQueueReorderResponses, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderData, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderErrors, PutBotsByBotIdSessionsBySessionIdSteerQueueReorderResponses, PutBotsByBotIdSettingsData, PutBotsByBotIdSettingsErrors, PutBotsByBotIdSettingsResponses, PutBotsByBotIdUserAccessByGrantIdData, PutBotsByBotIdUserAccessByGrantIdErrors, PutBotsByBotIdUserAccessByGrantIdResponses, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalData, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalErrors, PutBotsByBotIdWorkspaceTargetsByTargetIdToolApprovalResponses, PutBotsByBotIdWorkspaceTargetsPrimaryData, PutBotsByBotIdWorkspaceTargetsPrimaryErrors, PutBotsByBotIdWorkspaceTargetsPrimaryResponses, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdData, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdErrors, PutBotsByBotIdWorkspaceTargetsRemotesByRuntimeIdResponses, PutBotsByIdChannelByPlatformData, PutBotsByIdChannelByPlatformErrors, PutBotsByIdChannelByPlatformResponses, PutBotsByIdData, PutBotsByIdErrors, PutBotsByIdOwnerData, PutBotsByIdOwnerErrors, PutBotsByIdOwnerResponses, PutBotsByIdResponses, PutEmailProvidersByIdData, PutEmailProvidersByIdErrors, PutEmailProvidersByIdResponses, PutFetchProvidersByIdData, PutFetchProvidersByIdErrors, PutFetchProvidersByIdResponses, PutMemoryProvidersByIdData, PutMemoryProvidersByIdErrors, PutMemoryProvidersByIdResponses, PutModelsByIdData, PutModelsByIdErrors, PutModelsByIdResponses, PutModelsModelByModelIdData, PutModelsModelByModelIdErrors, PutModelsModelByModelIdResponses, PutProvidersByIdData, PutProvidersByIdErrors, PutProvidersByIdResponses, PutSearchProvidersByIdData, PutSearchProvidersByIdErrors, PutSearchProvidersByIdResponses, PutSpeechModelsByIdData, PutSpeechModelsByIdErrors, PutSpeechModelsByIdResponses, PutTranscriptionModelsByIdData, PutTranscriptionModelsByIdErrors, PutTranscriptionModelsByIdResponses, PutUsersByIdData, PutUsersByIdErrors, PutUsersByIdResponses, PutUsersMeChannelsByPlatformData, PutUsersMeChannelsByPlatformErrors, PutUsersMeChannelsByPlatformResponses, PutUsersMeData, PutUsersMeErrors, PutUsersMePasswordData, PutUsersMePasswordErrors, PutUsersMePasswordResponses, PutUsersMeResponses, PutVideoModelsByIdData, PutVideoModelsByIdErrors, PutVideoModelsByIdResponses } from './types.gen'; export type Options = Options2 & { /** @@ -1400,6 +1400,57 @@ export const postBotsByBotIdSessionsBySessionIdCompact = (options: Options): RequestResult => (options.client ?? client).get({ url: '/bots/{bot_id}/sessions/{session_id}/context-lifecycle', ...options }); +/** + * List pending follow-up inputs + */ +export const getBotsByBotIdSessionsBySessionIdFollowUpQueue = (options: Options): RequestResult => (options.client ?? client).get({ url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue', ...options }); + +/** + * Enqueue follow-up input for the active session run + */ +export const postBotsByBotIdSessionsBySessionIdFollowUpQueue = (options: Options): RequestResult => (options.client ?? client).post({ + url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Reorder accepted follow-up inputs + */ +export const putBotsByBotIdSessionsBySessionIdFollowUpQueueReorder = (options: Options): RequestResult => (options.client ?? client).put({ + url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue/reorder', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Cancel an accepted follow-up input + */ +export const deleteBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId = (options: Options): RequestResult => (options.client ?? client).delete({ url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue/{item_id}', ...options }); + +/** + * Edit an accepted follow-up input + */ +export const patchBotsByBotIdSessionsBySessionIdFollowUpQueueByItemId = (options: Options): RequestResult => (options.client ?? client).patch({ + url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue/{item_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Promote an accepted follow-up input to steer the active run + */ +export const postBotsByBotIdSessionsBySessionIdFollowUpQueueByItemIdSteer = (options: Options): RequestResult => (options.client ?? client).post({ url: '/bots/{bot_id}/sessions/{session_id}/follow-up-queue/{item_id}/steer', ...options }); + /** * Fork a chat session from an assistant reply */ @@ -1412,6 +1463,11 @@ export const postBotsByBotIdSessionsBySessionIdFork = (options: Options): RequestResult => (options.client ?? client).get({ url: '/bots/{bot_id}/sessions/{session_id}/queue', ...options }); + /** * Get session info * @@ -1419,6 +1475,52 @@ export const postBotsByBotIdSessionsBySessionIdFork = (options: Options): RequestResult => (options.client ?? client).get({ url: '/bots/{bot_id}/sessions/{session_id}/status', ...options }); +/** + * List pending steer inputs + */ +export const getBotsByBotIdSessionsBySessionIdSteerQueue = (options: Options): RequestResult => (options.client ?? client).get({ url: '/bots/{bot_id}/sessions/{session_id}/steer-queue', ...options }); + +/** + * Enqueue steer input for the active session run + */ +export const postBotsByBotIdSessionsBySessionIdSteerQueue = (options: Options): RequestResult => (options.client ?? client).post({ + url: '/bots/{bot_id}/sessions/{session_id}/steer-queue', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Reorder accepted steer inputs + */ +export const putBotsByBotIdSessionsBySessionIdSteerQueueReorder = (options: Options): RequestResult => (options.client ?? client).put({ + url: '/bots/{bot_id}/sessions/{session_id}/steer-queue/reorder', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Cancel an accepted steer input + */ +export const deleteBotsByBotIdSessionsBySessionIdSteerQueueByItemId = (options: Options): RequestResult => (options.client ?? client).delete({ url: '/bots/{bot_id}/sessions/{session_id}/steer-queue/{item_id}', ...options }); + +/** + * Edit an accepted steer input + */ +export const patchBotsByBotIdSessionsBySessionIdSteerQueueByItemId = (options: Options): RequestResult => (options.client ?? client).patch({ + url: '/bots/{bot_id}/sessions/{session_id}/steer-queue/{item_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + /** * Delete user settings * diff --git a/packages/sdk/src/types.gen.ts b/packages/sdk/src/types.gen.ts index 4808b1ea76..5fc20d3414 100644 --- a/packages/sdk/src/types.gen.ts +++ b/packages/sdk/src/types.gen.ts @@ -2626,13 +2626,13 @@ export type HandlersFollowUpQueueItemResponse = { enqueued_during_run_id?: string; item_id?: string; position?: number; - status?: QueueStatus; + status?: SessionruntimeQueueStatus; text?: string; }; export type HandlersFollowUpQueueReorderRequest = { - before?: QueueFollowUpPendingRef; - item?: QueueFollowUpPendingRef; + before?: SessionruntimeFollowUpPendingRef; + item?: SessionruntimeFollowUpPendingRef; }; export type HandlersFollowUpQueueResponse = { @@ -2752,6 +2752,7 @@ export type HandlersOauthExchangeRequest = { export type HandlersSessionQueueResponse = { follow_up?: Array; steer?: Array; + steer_supported?: boolean; }; export type HandlersSkillsOpResponse = { @@ -2761,14 +2762,14 @@ export type HandlersSkillsOpResponse = { export type HandlersSteerQueueItemResponse = { item_id?: string; position?: number; - status?: QueueStatus; + status?: SessionruntimeQueueStatus; target_run_id?: string; text?: string; }; export type HandlersSteerQueueReorderRequest = { - before?: QueueSteerPendingRef; - item?: QueueSteerPendingRef; + before?: SessionruntimeSteerPendingRef; + item?: SessionruntimeSteerPendingRef; }; export type HandlersSteerQueueResponse = { @@ -2804,6 +2805,8 @@ export type HandlersUpdateSessionRequest = { * PreferredChatModelID / PreferredReasoningEffort are the picker pair * (issue #879). The composer always patches the pair together; either one * alone is reconciled against the model the session would actually use. + * Changing either requires ExpectedModelPreferenceRevision ("" when the + * session has none yet): picker writes are always compare-and-set. */ preferred_chat_model_id?: string; preferred_reasoning_effort?: string; @@ -3202,16 +3205,6 @@ export type ProvidertemplatesModelResponse = { type?: string; }; -export type QueueFollowUpPendingRef = { - item_id?: string; -}; - -export type QueueStatus = 'accepted' | 'claimed' | 'applied' | 'rejected' | 'canceled'; - -export type QueueSteerPendingRef = { - item_id?: string; -}; - export type ReasoningOptions = { /** * CanDisable reports whether picking "off" actually reaches the model. @@ -3519,6 +3512,16 @@ export type SessionSession = { workdir_id?: string; }; +export type SessionruntimeFollowUpPendingRef = { + item_id?: string; +}; + +export type SessionruntimeQueueStatus = 'accepted' | 'claimed' | 'applied' | 'rejected' | 'expired' | 'canceled'; + +export type SessionruntimeSteerPendingRef = { + item_id?: string; +}; + export type SettingsSettings = { acl_default_effect?: string; chat_acp_agent_id?: string; diff --git a/spec/docs.go b/spec/docs.go index 159cd31827..5818da7f4b 100644 --- a/spec/docs.go +++ b/spec/docs.go @@ -22647,7 +22647,7 @@ const docTemplate = `{ "type": "integer" }, "status": { - "$ref": "#/definitions/queue.Status" + "$ref": "#/definitions/sessionruntime.QueueStatus" }, "text": { "type": "string" @@ -22658,10 +22658,10 @@ const docTemplate = `{ "type": "object", "properties": { "before": { - "$ref": "#/definitions/queue.FollowUpPendingRef" + "$ref": "#/definitions/sessionruntime.FollowUpPendingRef" }, "item": { - "$ref": "#/definitions/queue.FollowUpPendingRef" + "$ref": "#/definitions/sessionruntime.FollowUpPendingRef" } } }, @@ -22949,6 +22949,9 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/handlers.steerQueueItemResponse" } + }, + "steer_supported": { + "type": "boolean" } } }, @@ -22970,7 +22973,7 @@ const docTemplate = `{ "type": "integer" }, "status": { - "$ref": "#/definitions/queue.Status" + "$ref": "#/definitions/sessionruntime.QueueStatus" }, "target_run_id": { "type": "string" @@ -22984,10 +22987,10 @@ const docTemplate = `{ "type": "object", "properties": { "before": { - "$ref": "#/definitions/queue.SteerPendingRef" + "$ref": "#/definitions/sessionruntime.SteerPendingRef" }, "item": { - "$ref": "#/definitions/queue.SteerPendingRef" + "$ref": "#/definitions/sessionruntime.SteerPendingRef" } } }, @@ -23060,7 +23063,7 @@ const docTemplate = `{ "additionalProperties": {} }, "preferred_chat_model_id": { - "description": "PreferredChatModelID / PreferredReasoningEffort are the picker pair\n(issue #879). The composer always patches the pair together; either one\nalone is reconciled against the model the session would actually use.", + "description": "PreferredChatModelID / PreferredReasoningEffort are the picker pair\n(issue #879). The composer always patches the pair together; either one\nalone is reconciled against the model the session would actually use.\nChanging either requires ExpectedModelPreferenceRevision (\"\" when the\nsession has none yet): picker writes are always compare-and-set.", "type": "string" }, "preferred_reasoning_effort": { @@ -23996,39 +23999,6 @@ const docTemplate = `{ } } }, - "queue.FollowUpPendingRef": { - "type": "object", - "properties": { - "item_id": { - "type": "string" - } - } - }, - "queue.Status": { - "type": "string", - "enum": [ - "accepted", - "claimed", - "applied", - "rejected", - "canceled" - ], - "x-enum-varnames": [ - "Accepted", - "Claimed", - "Applied", - "Rejected", - "Canceled" - ] - }, - "queue.SteerPendingRef": { - "type": "object", - "properties": { - "item_id": { - "type": "string" - } - } - }, "reasoning.Options": { "type": "object", "properties": { @@ -24549,6 +24519,41 @@ const docTemplate = `{ } } }, + "sessionruntime.FollowUpPendingRef": { + "type": "object", + "properties": { + "item_id": { + "type": "string" + } + } + }, + "sessionruntime.QueueStatus": { + "type": "string", + "enum": [ + "accepted", + "claimed", + "applied", + "rejected", + "expired", + "canceled" + ], + "x-enum-varnames": [ + "QueueAccepted", + "QueueClaimed", + "QueueApplied", + "QueueRejected", + "QueueExpired", + "QueueCanceled" + ] + }, + "sessionruntime.SteerPendingRef": { + "type": "object", + "properties": { + "item_id": { + "type": "string" + } + } + }, "settings.Settings": { "type": "object", "properties": { diff --git a/spec/swagger.json b/spec/swagger.json index d9bfa10ce1..dab23aec1d 100644 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -22638,7 +22638,7 @@ "type": "integer" }, "status": { - "$ref": "#/definitions/queue.Status" + "$ref": "#/definitions/sessionruntime.QueueStatus" }, "text": { "type": "string" @@ -22649,10 +22649,10 @@ "type": "object", "properties": { "before": { - "$ref": "#/definitions/queue.FollowUpPendingRef" + "$ref": "#/definitions/sessionruntime.FollowUpPendingRef" }, "item": { - "$ref": "#/definitions/queue.FollowUpPendingRef" + "$ref": "#/definitions/sessionruntime.FollowUpPendingRef" } } }, @@ -22940,6 +22940,9 @@ "items": { "$ref": "#/definitions/handlers.steerQueueItemResponse" } + }, + "steer_supported": { + "type": "boolean" } } }, @@ -22961,7 +22964,7 @@ "type": "integer" }, "status": { - "$ref": "#/definitions/queue.Status" + "$ref": "#/definitions/sessionruntime.QueueStatus" }, "target_run_id": { "type": "string" @@ -22975,10 +22978,10 @@ "type": "object", "properties": { "before": { - "$ref": "#/definitions/queue.SteerPendingRef" + "$ref": "#/definitions/sessionruntime.SteerPendingRef" }, "item": { - "$ref": "#/definitions/queue.SteerPendingRef" + "$ref": "#/definitions/sessionruntime.SteerPendingRef" } } }, @@ -23051,7 +23054,7 @@ "additionalProperties": {} }, "preferred_chat_model_id": { - "description": "PreferredChatModelID / PreferredReasoningEffort are the picker pair\n(issue #879). The composer always patches the pair together; either one\nalone is reconciled against the model the session would actually use.", + "description": "PreferredChatModelID / PreferredReasoningEffort are the picker pair\n(issue #879). The composer always patches the pair together; either one\nalone is reconciled against the model the session would actually use.\nChanging either requires ExpectedModelPreferenceRevision (\"\" when the\nsession has none yet): picker writes are always compare-and-set.", "type": "string" }, "preferred_reasoning_effort": { @@ -23987,39 +23990,6 @@ } } }, - "queue.FollowUpPendingRef": { - "type": "object", - "properties": { - "item_id": { - "type": "string" - } - } - }, - "queue.Status": { - "type": "string", - "enum": [ - "accepted", - "claimed", - "applied", - "rejected", - "canceled" - ], - "x-enum-varnames": [ - "Accepted", - "Claimed", - "Applied", - "Rejected", - "Canceled" - ] - }, - "queue.SteerPendingRef": { - "type": "object", - "properties": { - "item_id": { - "type": "string" - } - } - }, "reasoning.Options": { "type": "object", "properties": { @@ -24540,6 +24510,41 @@ } } }, + "sessionruntime.FollowUpPendingRef": { + "type": "object", + "properties": { + "item_id": { + "type": "string" + } + } + }, + "sessionruntime.QueueStatus": { + "type": "string", + "enum": [ + "accepted", + "claimed", + "applied", + "rejected", + "expired", + "canceled" + ], + "x-enum-varnames": [ + "QueueAccepted", + "QueueClaimed", + "QueueApplied", + "QueueRejected", + "QueueExpired", + "QueueCanceled" + ] + }, + "sessionruntime.SteerPendingRef": { + "type": "object", + "properties": { + "item_id": { + "type": "string" + } + } + }, "settings.Settings": { "type": "object", "properties": { diff --git a/spec/swagger.yaml b/spec/swagger.yaml index bcdfc76a43..5fd29a3d85 100644 --- a/spec/swagger.yaml +++ b/spec/swagger.yaml @@ -4655,16 +4655,16 @@ definitions: position: type: integer status: - $ref: '#/definitions/queue.Status' + $ref: '#/definitions/sessionruntime.QueueStatus' text: type: string type: object handlers.followUpQueueReorderRequest: properties: before: - $ref: '#/definitions/queue.FollowUpPendingRef' + $ref: '#/definitions/sessionruntime.FollowUpPendingRef' item: - $ref: '#/definitions/queue.FollowUpPendingRef' + $ref: '#/definitions/sessionruntime.FollowUpPendingRef' type: object handlers.followUpQueueResponse: properties: @@ -4857,6 +4857,8 @@ definitions: items: $ref: '#/definitions/handlers.steerQueueItemResponse' type: array + steer_supported: + type: boolean type: object handlers.skillsOpResponse: properties: @@ -4870,7 +4872,7 @@ definitions: position: type: integer status: - $ref: '#/definitions/queue.Status' + $ref: '#/definitions/sessionruntime.QueueStatus' target_run_id: type: string text: @@ -4879,9 +4881,9 @@ definitions: handlers.steerQueueReorderRequest: properties: before: - $ref: '#/definitions/queue.SteerPendingRef' + $ref: '#/definitions/sessionruntime.SteerPendingRef' item: - $ref: '#/definitions/queue.SteerPendingRef' + $ref: '#/definitions/sessionruntime.SteerPendingRef' type: object handlers.steerQueueResponse: properties: @@ -4932,6 +4934,8 @@ definitions: PreferredChatModelID / PreferredReasoningEffort are the picker pair (issue #879). The composer always patches the pair together; either one alone is reconciled against the model the session would actually use. + Changing either requires ExpectedModelPreferenceRevision ("" when the + session has none yet): picker writes are always compare-and-set. type: string preferred_reasoning_effort: type: string @@ -5581,30 +5585,6 @@ definitions: type: type: string type: object - queue.FollowUpPendingRef: - properties: - item_id: - type: string - type: object - queue.Status: - enum: - - accepted - - claimed - - applied - - rejected - - canceled - type: string - x-enum-varnames: - - Accepted - - Claimed - - Applied - - Rejected - - Canceled - queue.SteerPendingRef: - properties: - item_id: - type: string - type: object reasoning.Options: properties: can_disable: @@ -6012,6 +5992,32 @@ definitions: workdir_id: type: string type: object + sessionruntime.FollowUpPendingRef: + properties: + item_id: + type: string + type: object + sessionruntime.QueueStatus: + enum: + - accepted + - claimed + - applied + - rejected + - expired + - canceled + type: string + x-enum-varnames: + - QueueAccepted + - QueueClaimed + - QueueApplied + - QueueRejected + - QueueExpired + - QueueCanceled + sessionruntime.SteerPendingRef: + properties: + item_id: + type: string + type: object settings.Settings: properties: acl_default_effect: From 85543f1ebc467009ff01e5183ff2130aec0490be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=A8=E8=8B=92?= <16112591+chen-ran@users.noreply.github.com> Date: Wed, 9 Sep 2026 14:41:30 +0800 Subject: [PATCH 7/7] feat(runtime): preempt native model invocations for steer Wake the fenced owner when steer input is accepted or promoted, stop the current model invocation, persist its checkpoint, and continue within the same run. Preserve tool and decision boundaries, earlier inputs, and retry-time steering. Replace asynchronous queue forwarding with synchronous PrepareStep consumption. Add consolidated native and real-server acceptance cases for silent, consecutive, promoted, retry-time, and post-steer abort flows. Validation: full Go tests, native/application and Redis/Valkey race checks, PostgreSQL integration checks, 21 cluster acceptance tests, 11 single-instance acceptance tests, 61 Web tests, changed-code lint, and browser QA. Existing full-repository lint failures remain; no human QA recorded. --- apps/web/src/i18n/locales/en.json | 2 +- apps/web/src/i18n/locales/ja.json | 2 +- apps/web/src/i18n/locales/zh.json | 2 +- docs/design/session-input-queues.md | 36 ++- docs/design/session-runtime-requirements.md | 4 +- internal/agent/application/contract.go | 7 +- .../native_decision_continuation.go | 3 +- .../agent/application/queue_step_binding.go | 50 +-- .../application/queue_step_coordinator.go | 26 +- .../application/queue_step_deferred_test.go | 59 ++-- .../application/queue_step_failure_test.go | 3 +- .../agent/application/service_retry_edit.go | 16 +- internal/agent/application/step_commit.go | 50 ++- internal/agent/application/turn_service.go | 2 +- internal/agent/runtime/native/agent.go | 62 +++- .../native/provider_stream_observer.go | 17 +- internal/agent/runtime/native/steer.go | 159 ++++++++++ internal/agent/runtime/native/steer_test.go | 286 ++++++++++++++++++ internal/agent/runtime/native/types.go | 7 + .../runtime/session/acceptance/README.md | 11 +- .../session/acceptance/fake_model_test.go | 8 +- .../session/acceptance/queue_contract_test.go | 125 +++++++- internal/agent/runtime/session/commands.go | 11 +- .../runtime/session/live_queue_manager.go | 74 ++++- internal/agent/runtime/session/manager.go | 1 + .../agent/runtime/session/steer_wake_test.go | 64 ++++ internal/agent/runtime/session/types.go | 1 + internal/handlers/local_channel.go | 6 +- 28 files changed, 933 insertions(+), 161 deletions(-) create mode 100644 internal/agent/runtime/native/steer.go create mode 100644 internal/agent/runtime/native/steer_test.go create mode 100644 internal/agent/runtime/session/steer_wake_test.go diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 3bbc6e1638..5b5f3897c6 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -702,7 +702,6 @@ "fragmentSeparator": ", " }, "currentBot": "Current Bot", - "inputPlaceholder": "Ask anything", "queue": { "steer": "Current response", "followUp": "Up next", @@ -720,6 +719,7 @@ "switchToFollowUp": "Queue after this run", "switchToSteer": "Steer the current run" }, + "inputPlaceholder": "Ask anything", "readonlyHint": "This chat is read-only", "readonlyPlaceholder": "This chat is read-only. Sending messages is disabled.", "composerActions": "Add files or switch Agent", diff --git a/apps/web/src/i18n/locales/ja.json b/apps/web/src/i18n/locales/ja.json index 0ae405ee4a..d523588e37 100644 --- a/apps/web/src/i18n/locales/ja.json +++ b/apps/web/src/i18n/locales/ja.json @@ -686,7 +686,6 @@ "fragmentSeparator": "、" }, "currentBot": "現在のBot", - "inputPlaceholder": "質問を入力してください", "queue": { "steer": "現在の応答", "followUp": "次のメッセージ", @@ -704,6 +703,7 @@ "switchToFollowUp": "この実行の後に追加", "switchToSteer": "現在の実行に追加" }, + "inputPlaceholder": "質問を入力してください", "readonlyHint": "このチャットは読み取り専用です", "readonlyPlaceholder": "このチャットは読み取り専用です。メッセージの送信は無効になっています。", "composerActions": "ファイルの追加またはエージェントの切り替え", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index ce5ab52804..f109f9915b 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -702,7 +702,6 @@ "fragmentSeparator": "、" }, "currentBot": "当前 Bot", - "inputPlaceholder": "问点什么", "queue": { "steer": "当前回复", "followUp": "接下来", @@ -720,6 +719,7 @@ "switchToFollowUp": "排到本次运行之后", "switchToSteer": "插入当前运行" }, + "inputPlaceholder": "问点什么", "readonlyHint": "该聊天为只读", "readonlyPlaceholder": "该聊天为只读,无法发送消息", "composerActions": "添加文件或切换 Agent", diff --git a/docs/design/session-input-queues.md b/docs/design/session-input-queues.md index 5a588ad2cf..ee36a79e90 100644 --- a/docs/design/session-input-queues.md +++ b/docs/design/session-input-queues.md @@ -71,14 +71,27 @@ after the steer's turn as soon as that turn is known. - Admission records the active run as `TargetRunID`. Without an active run, or after the run has been sealed, admission returns `ErrQueueNoActiveRun`. +- Native streaming admission/promotion wakes the fenced owner through the + existing command transport. Notifications coalesce locally; pending queue + state remains authoritative. Each provider admission also checks that state. +- During model sampling (including waiting for response headers), steer cancels + only the current invocation. Once its SDK stream is quiescent, the application + persists an interrupted checkpoint, applies any input in that checkpoint, + and claims the next input. Execution continues with the same run and an + advanced step cursor, without another agent-start or a run-abort event. + Unfinished reasoning is projected as text rather than replaying incomplete + provider signatures. Failure to quiesce or persist fails the run safely. +- The interruption gate closes before the SDK receives tool-call output or + finish-step. Already admitted tools and decisions are not cancelled by steer; + they keep their normal result/approval lifecycle before input is consumed. - At each committed step the application applies the previously claimed steer and claims the next accepted steer for the same run. During a tool loop the claimed text is injected into the next model request; at a final step the claim reopens the same run with the steer as the next model input. - A step that parks the run for a tool approval or user input applies the - previous claim but does not claim a new one. The continuation's first - committed step claims instead, so no claim waits unapplied across the - decision. + previous claim but does not claim a new one. The resumed invocation claims + at its next committed step or interruption checkpoint, so no claim waits + unapplied across the decision. - `ClaimNextSteer` returns the run's existing unapplied claim before selecting a new item. When the run has been reclaimed by a new owner, the stored claim is advanced to the new owner, generation, and fencing token; the previous @@ -92,6 +105,23 @@ after the steer's turn as soon as that turn is known. A claim is valid only for the run's current owner, generation, and fencing token; applying with a stale claim returns `ErrRunOwnershipLost`. +### Codex comparison + +Reference: OpenAI Codex commit +[`1530f828cbaea015bc0fc53c0486e2f889a677f3`](https://github.com/openai/codex/tree/1530f828cbaea015bc0fc53c0486e2f889a677f3). +Its `codex_thread.rs::steer_turn` requires the expected active turn and cannot +start another turn. `session/turn_input.rs::steer_input` appends input atomically +to the active task; `session/input_queue.rs` exposes queue activity notifications. +The public `turn/steer` contract is distinct from `turn/interrupt`: +https://developers.openai.com/codex/app-server#steer-an-active-turn. + +This is not a claim that public Codex always cancels an in-flight HTTP request: +`core/tests/suite/pending_input.rs::user_input_does_not_preempt_after_reasoning_item` +explicitly preserves the original response and tool call. Memoh follows the +same run identity and safe tool boundaries, and additionally implements the +requested force-steer behavior during native model sampling. External drivers +and provider-specific Responses WebSocket steering are outside this mechanism. + ## Follow-up A follow-up is bound to the session. It records the run that was active when diff --git a/docs/design/session-runtime-requirements.md b/docs/design/session-runtime-requirements.md index e301ac82e7..6640d0298c 100644 --- a/docs/design/session-runtime-requirements.md +++ b/docs/design/session-runtime-requirements.md @@ -173,7 +173,9 @@ ledger 成为终态后,系统必须以该 durable outcome 修复可能滞后 每个已准入 run 必须显式关联一个服务端生成的起始 `turn_id`。普通 run 的用户消息与回答归于该 turn。已应用的 steer 是同一 run 中新的用户输入,可以打开新的 canonical turn;该输入之后的 Agent 输出和工具消息归于新 turn,所有这些 turn 通过显式 `run_id` 关联,不能重新准入第二个 run。 -run 控制记录及其 decision 的 `turn_id` 保持起始 turn 身份,用于 owner/fence 与决策恢复校验;聊天消息的 `turn_id` 表示该消息所属的 canonical turn。客户端提交决策以 `decision_id` 和 run 身份为准,不能把临时渲染 ID 或某个显示分段的 ID 当作 run 控制身份。steer 的用户消息必须进入实际 provider 请求,并与对应完整 step 一起持久化,不能只在实时投影中展示。 +run 控制记录及其 decision 的 `turn_id` 保持起始 turn 身份,用于 owner/fence 与决策恢复校验;聊天消息的 `turn_id` 表示该消息所属的 canonical turn。客户端提交决策以 `decision_id` 和 run 身份为准,不能把临时渲染 ID 或某个显示分段的 ID 当作 run 控制身份。steer 的用户消息必须进入实际 provider 请求,并与对应完整 step 或被后续 steer 中断的 checkpoint 一起持久化,不能只在实时投影中展示。 + +Native 流式执行的 steer 必须能中断正在生成文本/推理或等待响应的模型调用,保存有效 checkpoint 后,在原 run 内加入新指令续跑。不能依赖测试先释放原模型才能消费。队列 `accepted` 只代表接收输入;owner 唤醒命令只代表控制信号送达,均不等于输入已应用。已接收的工具调用、工具执行和决策停等保持安全边界:不因 steer 取消或重复执行工具,不跳过审批;到下一次模型调用时优先处理新输入。用户 abort、owner 丢失和 `finishing` 的规则不变,steer 不得重新准入 run 或复活已终止的 run。 决策停等后,同一 owner 的续跑必须接续其已消费的 step 游标;owner 更换后游标可随 generation 重建。该游标仅服务进程内排序与投影屏障,不宣称跨进程模型采样重放。 diff --git a/internal/agent/application/contract.go b/internal/agent/application/contract.go index 56c2f0734d..f1735baec7 100644 --- a/internal/agent/application/contract.go +++ b/internal/agent/application/contract.go @@ -90,10 +90,9 @@ type ChatRequest struct { // InjectCh receives user messages between tool rounds. Remote transports // use turn.RunHandle.Inject instead. InjectCh <-chan turn.InjectMessage `json:"-"` - // QueueInjectCh is the execution-owned sender paired with InjectCh. Only the - // durable step coordinator uses it after claiming a steer item. - QueueInjectCh chan<- turn.InjectMessage `json:"-"` - StepIndexOffset int `json:"-"` + // QueueSteerEnabled enables the fenced native queue consumer. + QueueSteerEnabled bool `json:"-"` + StepIndexOffset int `json:"-"` // PublishRuntimeEvents is set for server-owned continuations, which do not // have a client runHandle pump to publish native events into the session // runtime projection. diff --git a/internal/agent/application/native_decision_continuation.go b/internal/agent/application/native_decision_continuation.go index 68e7f8cea5..f644baf86f 100644 --- a/internal/agent/application/native_decision_continuation.go +++ b/internal/agent/application/native_decision_continuation.go @@ -33,11 +33,10 @@ func (s *Service) runNativeDecisionContinuation(ctx context.Context, req ChatReq }() continuationRC := resolvedContext{runConfig: cfg, model: models.GetResponse{ID: modelID}} - stepCommitter, stopQueueBinding, err := s.bindQueueContinuation(ctx, &req, &cfg, continuationRC) + stepCommitter, err := s.bindQueueContinuation(ctx, &req, &cfg, continuationRC) if err != nil { return err } - defer stopQueueBinding() reasoningTiming := newReasoningTimingTracker(nil) configureNativeReasoningTiming(&cfg, reasoningTiming, stepCommitter) idleCtx, idleCancel := s.withStreamIdleTimeout(ctx, reasoningEffortForIdle(cfg)) diff --git a/internal/agent/application/queue_step_binding.go b/internal/agent/application/queue_step_binding.go index af2e59ca22..dc59118160 100644 --- a/internal/agent/application/queue_step_binding.go +++ b/internal/agent/application/queue_step_binding.go @@ -3,10 +3,8 @@ package application import ( "context" "errors" - "sync" "github.com/felinics/memoh/internal/agent/runtime/native" - "github.com/felinics/memoh/internal/agent/turn" ) // bindQueueContinuation installs the live queue step boundary used by an @@ -18,60 +16,24 @@ func (s *Service) bindQueueContinuation( req *ChatRequest, cfg *native.RunConfig, rc resolvedContext, -) (*agentStepCommitter, func(), error) { - noop := func() {} +) (*agentStepCommitter, error) { if s == nil || req == nil || cfg == nil || s.sessionManager == nil || req.RunHandle.RunID == "" || req.RunHandle.OwnerID == "" || req.RunHandle.FencingToken <= 0 { - return nil, noop, nil + return nil, nil } stepIndex, err := s.sessionManager.ContinuationStepIndex(req.RunHandle) if err != nil { - return nil, noop, err + return nil, err } req.StepIndexOffset = stepIndex cfg.StepIndexOffset = stepIndex - queueInput := make(chan turn.InjectMessage, 16) - nativeInput := make(chan native.InjectMessage, 16) - done := make(chan struct{}) - var stopOnce sync.Once - stop := func() { stopOnce.Do(func() { close(done) }) } - existingInput := cfg.InjectCh - go func() { - defer close(nativeInput) - for { - select { - case <-done: - return - case msg, ok := <-existingInput: - if !ok { - existingInput = nil - continue - } - select { - case nativeInput <- msg: - case <-done: - return - } - case msg := <-queueInput: - nativeMessage := native.InjectMessage{Text: msg.Text, HeaderifiedText: msg.HeaderifiedText} - select { - case nativeInput <- nativeMessage: - case <-done: - return - } - } - } - }() - - req.QueueInjectCh = queueInput - cfg.InjectCh = nativeInput + req.QueueSteerEnabled = true committer := s.newAgentStepCommitter(ctx, *req, rc) if committer == nil { - stop() - return nil, noop, errors.New("live queue step committer is unavailable for decision continuation") + return nil, errors.New("live queue step committer is unavailable for decision continuation") } committer.bindContinuation(cfg) - return committer, stop, nil + return committer, nil } diff --git a/internal/agent/application/queue_step_coordinator.go b/internal/agent/application/queue_step_coordinator.go index a367d36306..dbb02c4b7a 100644 --- a/internal/agent/application/queue_step_coordinator.go +++ b/internal/agent/application/queue_step_coordinator.go @@ -8,7 +8,6 @@ import ( sdk "github.com/felinics/twilight/sdk" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - "github.com/felinics/memoh/internal/agent/turn" messagepkg "github.com/felinics/memoh/internal/chat/message" ) @@ -36,7 +35,7 @@ type queueStepOutcome struct { } func newQueueStepCoordinator(s *Service, req ChatRequest) *queueStepCoordinator { - if req.QueueInjectCh == nil && req.TurnReplacement == nil { + if !req.QueueSteerEnabled && req.TurnReplacement == nil { return nil } if s == nil || s.sessionManager == nil || s.messageService == nil || req.RunHandle.RunID == "" || req.RunHandle.OwnerID == "" || req.RunHandle.FencingToken <= 0 { @@ -54,7 +53,7 @@ func newQueueStepCoordinator(s *Service, req ChatRequest) *queueStepCoordinator service: s, req: req, persister: persister, replacementPersister: replacementPersister, run: req.RunHandle, - steerEnabled: req.QueueInjectCh != nil, + steerEnabled: req.QueueSteerEnabled, } return q } @@ -106,9 +105,9 @@ func (q *queueStepCoordinator) commit( if kind == queueStepDeferredDecision { // The loop parks after this step and its inject channel is never read // again. A claim taken here would sit unapplied across the decision, and - // across any owner change while the run waits. The continuation's first - // committed step claims instead, so the steer enters the request after - // the decision result exactly as a tool-loop claim would. + // across any owner change while the run waits. The resumed execution + // claims at its next complete step or model-interruption checkpoint, + // keeping the approved tool result ahead of the new input. return outcome, nil } @@ -124,20 +123,8 @@ func (q *queueStepCoordinator) commit( if claimed { q.pendingSteer = &claim outcome.claimedSteer = &item - if kind == queueStepFinal { + if kind == queueStepFinal || kind == queueStepSteered { outcome.continueAfterFinal = true - } else { - text := QueuePayloadText(item.Payload) - if q.req.QueueInjectCh == nil { - q.releaseSteerClaim(ctx) - return outcome, errors.New("steer queue injection channel is unavailable") - } - select { - case q.req.QueueInjectCh <- turn.InjectMessage{Text: text, HeaderifiedText: text}: - default: - q.releaseSteerClaim(ctx) - return outcome, errors.New("steer queue injection channel is full") - } } } else if q.req.TurnReplacement != nil && kind == queueStepFinal { allPersisted := append([]messagepkg.Message(nil), previouslyPersisted...) @@ -165,6 +152,7 @@ const ( queueStepToolLoop queueStepKind = "tool_loop" queueStepDeferredDecision queueStepKind = "deferred_decision" queueStepFinal queueStepKind = "final" + queueStepSteered queueStepKind = "steered" ) func classifyQueueStep(step *sdk.StepResult) queueStepKind { diff --git a/internal/agent/application/queue_step_deferred_test.go b/internal/agent/application/queue_step_deferred_test.go index 42a86e20bb..543bc7d633 100644 --- a/internal/agent/application/queue_step_deferred_test.go +++ b/internal/agent/application/queue_step_deferred_test.go @@ -6,13 +6,17 @@ import ( "testing" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - "github.com/felinics/memoh/internal/agent/turn" messagepkg "github.com/felinics/memoh/internal/chat/message" dbstore "github.com/felinics/memoh/internal/db/store" + "github.com/felinics/memoh/internal/testutil/sessionledger" ) type nilQueries struct{ dbstore.Queries } +type queueTestFence struct{} + +func (queueTestFence) Activate(context.Context, string, string, int64) error { return nil } + // newDeferredSteerTestService builds a Service whose queue step transaction // can run without PostgreSQL: steps carry no messages, so history persistence // is skipped, and queue state lives in a memory backend with one active run. @@ -22,43 +26,29 @@ func newDeferredSteerTestService(t *testing.T, backends ...sessionruntime.Backen if len(backends) > 0 { backend = backends[0] } - key := sessionruntime.Key{BotID: "bot", SessionID: "session"} - _, _, err := backend.Update(context.Background(), key, func(snapshot sessionruntime.Snapshot, _ bool) (sessionruntime.Snapshot, bool, error) { - snapshot.BotID, snapshot.SessionID = key.BotID, key.SessionID - snapshot.CurrentRunView = &sessionruntime.CurrentRunView{ - RunID: "run-1", TurnID: "turn-1", Generation: "gen-1", SteerSupported: true, Status: sessionruntime.RunStatusRunning, - } - return snapshot, true, nil + manager := sessionruntime.NewManager(backend, sessionruntime.Options{OwnerID: "owner-1", Ledger: sessionledger.New(), Fence: queueTestFence{}}) + t.Cleanup(func() { _ = manager.Close() }) + admitted, err := manager.Admit(context.Background(), sessionruntime.AdmitInput{ + BotID: "bot", SessionID: "session", InvocationID: "initial", Payload: []byte(`{}`), + Execution: sessionruntime.Execution{Admission: func(context.Context, sessionruntime.RunHandle) (sessionruntime.RunAdmissionView, error) { + return sessionruntime.RunAdmissionView{}, nil + }}, }) if err != nil { t.Fatal(err) } - manager := sessionruntime.NewManager(backend, sessionruntime.Options{OwnerID: "owner-1"}) - t.Cleanup(func() { _ = manager.Close() }) + handle := admitted.Handle + if err := manager.EnableSteer(context.Background(), handle); err != nil { + t.Fatal(err) + } service := &Service{ sessionManager: manager, messageService: &recordingStepPersister{recordingMessageService: &recordingMessageService{}}, queries: nilQueries{}, } - handle := sessionruntime.RunHandle{ - BotID: key.BotID, SessionID: key.SessionID, RunID: "run-1", TurnID: "turn-1", - OwnerID: "owner-1", Generation: "gen-1", FencingToken: 1, - } return service, handle } -func drainInject(ch chan turn.InjectMessage) []string { - var texts []string - for { - select { - case msg := <-ch: - texts = append(texts, msg.Text) - default: - return texts - } - } -} - // A deferred step parks the loop, so its commit must not claim a steer: the // inject channel is never read again and a claim would sit unapplied across // the decision and across any owner change. The continuation's first committed @@ -73,10 +63,9 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { } // Original run: the deferred step commits without touching the queue. - parkedInject := make(chan turn.InjectMessage, 16) original := newQueueStepCoordinator(service, ChatRequest{ BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, - RunHandle: handle, QueueInjectCh: parkedInject, + RunHandle: handle, QueueSteerEnabled: true, }) if original == nil { t.Fatal("queue step transaction unavailable") @@ -88,19 +77,15 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { if outcome.claimedSteer != nil || outcome.appliedSteerItemID != "" { t.Fatalf("deferred step touched the steer queue: %#v", outcome) } - if got := drainInject(parkedInject); len(got) != 0 { - t.Fatalf("parked inject channel received %v", got) - } + if steers, _, err := service.sessionManager.PendingQueues(ctx, key, 0); err != nil || len(steers) != 1 || steers[0].Status != sessionruntime.QueueAccepted { t.Fatalf("steer should stay accepted across the park: %#v, %v", steers, err) } - // Continuation after the decision: a fresh request, - // a fresh inject channel. This mirrors continueToolApprovalSession. - continuationInject := make(chan turn.InjectMessage, 16) + // Continuation after the decision uses a fresh coordinator with the same run. continuation := newQueueStepCoordinator(service, ChatRequest{ BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, - RunHandle: handle, QueueInjectCh: continuationInject, UserMessagePersisted: true, + RunHandle: handle, QueueSteerEnabled: true, UserMessagePersisted: true, }) if continuation == nil { t.Fatal("continuation queue step transaction unavailable") @@ -118,9 +103,7 @@ func TestDeferredStepDoesNotClaimSteerAndContinuationDeliversIt(t *testing.T) { if outcome.claimedSteer == nil || outcome.claimedSteer.ID != item.ID { t.Fatalf("continuation did not claim the steer: %#v", outcome) } - if got := drainInject(continuationInject); len(got) != 1 || got[0] != "steer me" { - t.Fatalf("continuation inject channel = %v", got) - } + steers, _, err := service.sessionManager.PendingQueues(ctx, key, 0) if err != nil || len(steers) != 0 { t.Fatalf("pending steers while claimed = %#v, %v", steers, err) diff --git a/internal/agent/application/queue_step_failure_test.go b/internal/agent/application/queue_step_failure_test.go index 17b67eb7c1..c848aa1d44 100644 --- a/internal/agent/application/queue_step_failure_test.go +++ b/internal/agent/application/queue_step_failure_test.go @@ -10,7 +10,6 @@ import ( contextfrag "github.com/felinics/memoh/internal/agent/context/fragment" "github.com/felinics/memoh/internal/agent/runtime/native" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" - "github.com/felinics/memoh/internal/agent/turn" messagepkg "github.com/felinics/memoh/internal/chat/message" "github.com/felinics/memoh/internal/runtimefence" ) @@ -44,7 +43,7 @@ func TestStepCommitSeparatesHistoryFailureFromQueueFailure(t *testing.T) { if err != nil { t.Fatal(err) } - req := ChatRequest{BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, RunHandle: handle, UserMessagePersisted: true, PersistedUserMessageID: "user", QueueInjectCh: make(chan turn.InjectMessage, 1)} + req := ChatRequest{BotID: handle.BotID, ThreadID: handle.SessionID, RunID: handle.RunID, RunHandle: handle, UserMessagePersisted: true, PersistedUserMessageID: "user", QueueSteerEnabled: true} committer := service.newAgentStepCommitter(ctx, req, resolvedContext{runConfig: native.RunConfig{ContextLifecycle: contextfrag.NewLifecycleHolder()}}) if committer == nil { t.Fatal("missing fenced step committer") diff --git a/internal/agent/application/service_retry_edit.go b/internal/agent/application/service_retry_edit.go index 8d60becdb6..08c8be8029 100644 --- a/internal/agent/application/service_retry_edit.go +++ b/internal/agent/application/service_retry_edit.go @@ -31,14 +31,14 @@ type RetryLatestMessageInput struct { ReasoningEffort string WorkspaceTargetID string ToolHTTPURL string - // OnModelPreferenceSettled releases subsequent picker writes once this - // turn's preference write-back has finished (issue #879). Same contract - // as ChatRequest.OnModelPreferenceSettled. - OnModelPreferenceSettled func() // RunHandle and InjectCh are server-owned admission capabilities. They are // populated only by the in-process Web runtime, never by client JSON. RunHandle sessionruntime.RunHandle InjectCh chan turnpkg.InjectMessage + // OnModelPreferenceSettled releases subsequent picker writes once this + // turn's preference write-back has finished (issue #879). Same contract + // as ChatRequest.OnModelPreferenceSettled. + OnModelPreferenceSettled func() } type EditLatestMessageInput struct { @@ -58,10 +58,10 @@ type EditLatestMessageInput struct { ReasoningEffort string WorkspaceTargetID string ToolHTTPURL string - // OnModelPreferenceSettled: see RetryLatestMessageInput. - OnModelPreferenceSettled func() RunHandle sessionruntime.RunHandle InjectCh chan turnpkg.InjectMessage + // OnModelPreferenceSettled: see RetryLatestMessageInput. + OnModelPreferenceSettled func() } func (s *Service) RetryLatestMessageWS(ctx context.Context, input RetryLatestMessageInput, eventCh chan<- WSStreamEvent, abortCh <-chan struct{}) error { @@ -102,7 +102,7 @@ func (s *Service) RetryLatestMessageWS(ctx context.Context, input RetryLatestMes WorkspaceTargetID: strings.TrimSpace(input.WorkspaceTargetID), ToolHTTPURL: strings.TrimSpace(input.ToolHTTPURL), InjectCh: input.InjectCh, - QueueInjectCh: input.InjectCh, + QueueSteerEnabled: input.InjectCh != nil, ReusePersistedUserMessage: true, PersistedUserMessageID: requestMessage.ID, SkipHistoryTurn: true, @@ -149,7 +149,7 @@ func (s *Service) EditLatestMessageWS(ctx context.Context, input EditLatestMessa WorkspaceTargetID: strings.TrimSpace(input.WorkspaceTargetID), ToolHTTPURL: strings.TrimSpace(input.ToolHTTPURL), InjectCh: input.InjectCh, - QueueInjectCh: input.InjectCh, + QueueSteerEnabled: input.InjectCh != nil, SkipHistoryTurn: true, HistoryCutoffBeforeMessageID: strings.TrimSpace(turn.RequestMessageID), OnModelPreferenceSettled: input.OnModelPreferenceSettled, diff --git a/internal/agent/application/step_commit.go b/internal/agent/application/step_commit.go index 211336f1fb..0e875ef042 100644 --- a/internal/agent/application/step_commit.go +++ b/internal/agent/application/step_commit.go @@ -94,17 +94,43 @@ func (c *agentStepCommitter) bindContinuation(cfg *native.RunConfig) { } cfg.ContinueAfterFinal = &c.continueAfterFinal cfg.NextModelInputs = &c.nextModelInputs + if c.queueStep != nil && c.queueStep.steerEnabled { + cfg.SteerWake = c.service.sessionManager.SteerWake(c.req.RunHandle) + cfg.PendingSteer = func(ctx context.Context) (bool, error) { + items, _, err := c.service.sessionManager.PendingQueues(ctx, sessionruntime.Key{ + BotID: c.req.BotID, SessionID: c.req.ThreadID, + }, sessionruntime.MaxPendingQueueItems) + for _, item := range items { + if item.TargetRunID == c.req.RunID && item.Status == sessionruntime.QueueAccepted { + return true, err + } + } + return false, err + } + cfg.OnSteer = func(ctx context.Context, index int, step *sdk.StepResult) error { + return c.persist(ctx, index, step, stepSteered) + } + } } +type stepCommitMode uint8 + +const ( + stepCompleted stepCommitMode = iota + stepInterrupted + stepSteered +) + func (c *agentStepCommitter) commit(ctx context.Context, stepIndex int, step *sdk.StepResult) error { - return c.persist(ctx, stepIndex, step, false) + return c.persist(ctx, stepIndex, step, stepCompleted) } func (c *agentStepCommitter) interrupt(ctx context.Context, stepIndex int, step *sdk.StepResult) error { - return c.persist(ctx, stepIndex, step, true) + return c.persist(ctx, stepIndex, step, stepInterrupted) } -func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *sdk.StepResult, interrupted bool) error { +func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *sdk.StepResult, mode stepCommitMode) error { + interrupted := mode != stepCompleted if c == nil || step == nil { return errors.New("agent step is missing") } @@ -126,7 +152,7 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s // recorded as a commit failure: the turn is already ending, and losing an // unfinished snapshot must not turn an abort into a turn error. fail := func(err error) error { - if !interrupted { + if mode != stepInterrupted { c.commitErr = err } return err @@ -139,11 +165,11 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s // provider result: final handoff and queue reconciliation are keyed to the // step, not to whether the provider emitted a message. Legacy/non-durable // paths retain the old cheap no-op behavior. - if !hasAssistantOutput && (c.queueStep == nil || interrupted) { + if !hasAssistantOutput && mode != stepSteered && (c.queueStep == nil || interrupted) { c.nextStep++ return nil } - if hasAssistantOutput && stepIndex == 0 && !c.req.UserMessagePersisted && !c.req.ReusePersistedUserMessage { + if (hasAssistantOutput || mode == stepSteered) && stepIndex == 0 && !c.req.UserMessagePersisted && !c.req.ReusePersistedUserMessage { messages = prependTurnUserMessage(c.req, messages) } storeReq := c.req @@ -180,10 +206,14 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s agentStep := messagepkg.AgentStep{RunID: c.req.RunID, Messages: inputs, Interrupted: interrupted} var persisted []messagepkg.Message var queueErr error - if c.queueStep != nil && !interrupted { + if c.queueStep != nil && mode != stepInterrupted { stepCtx := context.WithoutCancel(ctx) + kind := classifyQueueStep(step) + if mode == stepSteered { + kind = queueStepSteered + } outcome, commitErr := c.queueStep.commit( - stepCtx, classifyQueueStep(step), agentStep, c.persisted, + stepCtx, kind, agentStep, c.persisted, ) if !outcome.historyCommitted { return fail(commitErr) @@ -194,11 +224,11 @@ func (c *agentStepCommitter) persist(ctx context.Context, stepIndex int, step *s queueErr = commitErr persisted = outcome.persisted c.replacementFinalized = outcome.replacementFinalized - if queueErr == nil && outcome.continueAfterFinal && classifyQueueStep(step) == queueStepFinal { + if queueErr == nil { if outcome.claimedSteer != nil { c.nextModelInputs = append(c.nextModelInputs, sdk.UserMessage(QueuePayloadText(outcome.claimedSteer.Payload))) } - c.continueAfterFinal.Store(true) + c.continueAfterFinal.Store(outcome.continueAfterFinal) } if queueErr == nil { c.publishQueueUserTurns(context.WithoutCancel(ctx), stepIndex, outcome) diff --git a/internal/agent/application/turn_service.go b/internal/agent/application/turn_service.go index 0b73e2471d..75f0ea8d4a 100644 --- a/internal/agent/application/turn_service.go +++ b/internal/agent/application/turn_service.go @@ -79,7 +79,7 @@ func (s *Service) StartTurn(ctx context.Context, cmd turn.StartTurnCommand) (tur req.TurnID = admission.TurnID req.TurnPosition = &admission.TurnPosition req.InjectCh = injectCh - req.QueueInjectCh = injectCh + req.QueueSteerEnabled = injectCh != nil req.OutboundAssetCollector = func() []turn.OutboundAssetRef { assetMu.Lock() defer assetMu.Unlock() diff --git a/internal/agent/runtime/native/agent.go b/internal/agent/runtime/native/agent.go index 47a747b2c8..cddfe33779 100644 --- a/internal/agent/runtime/native/agent.go +++ b/internal/agent/runtime/native/agent.go @@ -281,19 +281,28 @@ func sendEvent(ctx context.Context, ch chan<- StreamEvent, evt StreamEvent) bool } func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEvent) { - cfg.Model = modelWithProviderStreamEventObserver(cfg.Model, cfg.OnProviderStreamEventObserved) if cfg.ContextLifecycle == nil { cfg.ContextLifecycle = contextfrag.NewLifecycleHolder() } streamCtx, cancel := context.WithCancelCause(ctx) + var steerGate *modelSteerGate + if cfg.PendingSteer != nil && cfg.OnSteer != nil { + steerGate = &modelSteerGate{cancel: cancel, ready: make(chan struct{}, 1)} + go a.watchSteer(streamCtx, cfg, steerGate) + } + cfg.Model = modelWithProviderStreamEventObserver(cfg.Model, cfg.OnProviderStreamEventObserved, steerGate) eventGate := newStreamEmitterGate(streamCtx, ch) defer func() { cancel(nil) eventGate.close() }() aborted := false + continued := false turnError := "" defer func() { + if continued { + return + } event := hooks.EventTurnEnd if aborted || strings.TrimSpace(turnError) != "" { event = hooks.EventTurnError @@ -442,6 +451,7 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv } } + prepareStep = prepareQueuedSteer(prepareStep, cfg) prepareStep, committedStepMessages := capturePreparedStepMessages(prepareStep) committedStepMessages.byStep[0] = cloneProviderMessages(cfg.initialStepInputs) if readMediaState != nil { @@ -740,7 +750,7 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv } case *sdk.ErrorPart: - if contextStepBudgetError(streamCtx) != nil { + if streamCtx.Err() != nil { aborted = true break } @@ -780,7 +790,8 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv break } } - if ctx.Err() != nil { + steered := errors.Is(context.Cause(streamCtx), errModelSteered) + if ctx.Err() != nil || steered { aborted = true } @@ -800,6 +811,45 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv streamClosed = drainStreamUntilClosed(streamResult.Stream, streamCancelDrainGrace, interruptedStep.observe) } + if steered && ctx.Err() == nil && streamClosed { + // The SDK is now quiescent: it cannot commit or execute a late tool. + // Checkpoint even a silent attempt so the original user/input admission + // and step cursor survive before we append the next input to this run. + step := interruptedStep.snapshot(nextDurableStep) + if step == nil { + step = &sdk.StepResult{} + } + step = committedStepMessages.decorate(nextDurableStep, step, toolExecutionMetadata) + checkpointMessages := step.Messages + if nextDurableStep == 0 { + // Initial steer inputs already live in cfg.Messages. The decorated + // step includes them for persistence, not a second prompt insertion. + checkpointMessages = checkpointMessages[min(len(cfg.initialStepInputs), len(checkpointMessages)):] + } + index := cfg.StepIndexOffset + nextDurableStep + sendEvent(ctx, ch, StreamEvent{Type: EventStepEnd, StepNumber: index}) + if err := cfg.OnSteer(ctx, index, step); err == nil { + messages := append(steerContinuationMessages(cfg, streamResult.Steps, committedStepMessages), steerCheckpointMessages(checkpointMessages)...) + cfg = appendSteerContinuation(cfg, messages, nextDurableStep+1) + if cfg.ContinueAfterFinal != nil { + cfg.ContinueAfterFinal.Store(false) + } + eventGate.close() + continued = true + a.runStream(ctx, cfg, ch) + return + } else { + a.logger.Error("checkpoint steered model invocation failed", slog.Any("error", err)) + } + } + if steered && ctx.Err() == nil { + // An unquiesced invocation or failed checkpoint cannot safely resume. + // Use the existing public interruption error; diagnostics stay in logs. + public, _ := apperror.PublicFrom(apperror.New(apperror.CodeAgentResponseInterrupted, nil), "") + turnError = public.Detail + sendEvent(ctx, ch, StreamEvent{Type: EventError, Error: public.Detail, Code: string(public.Code)}) + } + // Only external cancellation can represent a user/session abort. Provider // errors and loop guards keep their existing failure semantics. // @@ -895,12 +945,13 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv // steer becomes the next model input instead of being stranded after the // terminal event. if streamClosed && !aborted && streamResult != nil && cfg.ContinueAfterFinal != nil && cfg.ContinueAfterFinal.Swap(false) { - cfg = appendSteerContinuation(cfg, streamResult.Messages, len(streamResult.Steps)) + cfg = appendSteerContinuation(cfg, steerContinuationMessages(cfg, streamResult.Steps, committedStepMessages), len(streamResult.Steps)) // The completed invocation must stop its emitters before the continuation // starts; the continuation gets a fresh child context from the original // run context so closing the old stream does not cancel it. cancel(context.Canceled) eventGate.close() + continued = true a.runStream(ctx, cfg, ch) return } @@ -1055,6 +1106,7 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat prepareStep = readMediaState.prepareStep } + prepareStep = prepareQueuedSteer(prepareStep, cfg) prepareStep, committedStepMessages := capturePreparedStepMessages(prepareStep) committedStepMessages.byStep[0] = cloneProviderMessages(cfg.initialStepInputs) if readMediaState != nil { @@ -1142,7 +1194,7 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (result *Generat } finalMessages = toolExecutionMetadata.annotate(finalMessages) if cfg.ContinueAfterFinal != nil && cfg.ContinueAfterFinal.Swap(false) && len(genResult.Steps) > 0 { - cfg = appendSteerContinuation(cfg, finalMessages, len(genResult.Steps)) + cfg = appendSteerContinuation(cfg, steerContinuationMessages(cfg, genResult.Steps, committedStepMessages), len(genResult.Steps)) next, nextErr := a.runGenerate(genCtx, cfg) if nextErr != nil { return nil, nextErr diff --git a/internal/agent/runtime/native/provider_stream_observer.go b/internal/agent/runtime/native/provider_stream_observer.go index 02d6d500a9..9523cb6292 100644 --- a/internal/agent/runtime/native/provider_stream_observer.go +++ b/internal/agent/runtime/native/provider_stream_observer.go @@ -9,10 +9,11 @@ import ( type providerStreamEventObserver struct { sdk.Provider observe func(StreamEvent) + steer *modelSteerGate } -func modelWithProviderStreamEventObserver(model *sdk.Model, observe func(StreamEvent)) *sdk.Model { - if model == nil || model.Provider == nil || observe == nil { +func modelWithProviderStreamEventObserver(model *sdk.Model, observe func(StreamEvent), steer *modelSteerGate) *sdk.Model { + if model == nil || model.Provider == nil || (observe == nil && steer == nil) { return model } observed := *model @@ -26,7 +27,7 @@ func modelWithProviderStreamEventObserver(model *sdk.Model, observe func(StreamE } provider = previous.Provider } - observed.Provider = providerStreamEventObserver{Provider: provider, observe: observe} + observed.Provider = providerStreamEventObserver{Provider: provider, observe: observe, steer: steer} return &observed } @@ -34,7 +35,10 @@ func (p providerStreamEventObserver) DoStream(ctx context.Context, params sdk.Ge // Every provider call begins a fresh attempt. For ordinary multi-step runs // the previous step has already consumed or checkpointed its timings; for a // retry this discards the failed attempt before replacement parts arrive. - p.observe(StreamEvent{Type: EventRetry}) + if p.observe != nil { + p.observe(StreamEvent{Type: EventRetry}) + } + p.steer.begin() result, err := p.Provider.DoStream(ctx, params) if err != nil || result == nil || result.Stream == nil { return result, err @@ -56,7 +60,10 @@ func (p providerStreamEventObserver) DoStream(ctx context.Context, params sdk.Ge case <-ctx.Done(): return } - if event, ok := providerPartTimingEvent(part); ok { + if !p.steer.observe(part) { + return + } + if event, ok := providerPartTimingEvent(part); ok && p.observe != nil { p.observe(event) } select { diff --git a/internal/agent/runtime/native/steer.go b/internal/agent/runtime/native/steer.go new file mode 100644 index 0000000000..14738391c1 --- /dev/null +++ b/internal/agent/runtime/native/steer.go @@ -0,0 +1,159 @@ +package native + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sync" + + sdk "github.com/felinics/twilight/sdk" + + contextfrag "github.com/felinics/memoh/internal/agent/context/fragment" +) + +var errModelSteered = errors.New("model invocation steered") + +// The commit callback and PrepareStep run serially on the SDK loop. Reading +// here avoids a sender/forwarder race with an immediately following tool step. +func prepareQueuedSteer(prepare func(*sdk.GenerateParams) *sdk.GenerateParams, cfg RunConfig) func(*sdk.GenerateParams) *sdk.GenerateParams { + if cfg.NextModelInputs == nil { + return prepare + } + return func(params *sdk.GenerateParams) *sdk.GenerateParams { + if prepare != nil { + if override := prepare(params); override != nil { + params = override + } + } + if len(*cfg.NextModelInputs) > 0 { + cfg.ContextMutations.Record(contextfrag.MutationInjectedMessage, fmt.Sprintf("messages=%d", len(*cfg.NextModelInputs))) + params.Messages = append(params.Messages, *cfg.NextModelInputs...) + *cfg.NextModelInputs = nil + } + return params + } +} + +// modelSteerGate serializes interruption with provider output BEFORE the SDK +// can execute tools or commit a completed step. Consumer-side stream flags are +// too late: provider events may already be buffered ahead of the UI consumer. +type modelSteerGate struct { + mu sync.Mutex + sampling bool + stopped bool + cancel context.CancelCauseFunc + ready chan struct{} +} + +// Notifications must remain live while the main consumer is inside retry +// handling as well as its normal stream loop. This worker owns no input or +// history state and stops with this invocation's context. +func (a *Agent) watchSteer(ctx context.Context, cfg RunConfig, gate *modelSteerGate) { + for { + select { + case <-ctx.Done(): + return + case <-cfg.SteerWake: + case <-gate.ready: + } + pending, err := cfg.PendingSteer(ctx) + if err != nil { + if ctx.Err() == nil { + a.logger.Warn("check pending steer failed", slog.Any("error", err)) + } + continue + } + if pending && gate.interrupt() { + return + } + } +} + +// SDK output omits inputs inserted by PrepareStep. Rebuild the committed +// transcript with their admitted provenance so a later steer cannot forget +// an earlier steer/read_media input. Initial inputs already live in cfg.Messages. +func steerContinuationMessages(cfg RunConfig, steps []sdk.StepResult, capture *stepMessageCapture) []sdk.Message { + var messages []sdk.Message + for i, step := range steps { + inputs := capture.messages(i) + if i == 0 { + inputs = inputs[min(len(cfg.initialStepInputs), len(inputs)):] + } + messages = append(messages, inputs...) + messages = append(messages, step.Messages...) + } + return messages +} + +func (g *modelSteerGate) begin() { + if g == nil { + return + } + g.mu.Lock() + defer g.mu.Unlock() + g.sampling = !g.stopped + select { + case g.ready <- struct{}{}: + default: + } +} + +func (g *modelSteerGate) observe(part sdk.StreamPart) bool { + if g == nil { + return true + } + g.mu.Lock() + defer g.mu.Unlock() + if g.stopped { + return false + } + switch part.(type) { + case *sdk.ToolInputStartPart, *sdk.ToolInputDeltaPart, *sdk.ToolInputEndPart, + *sdk.StreamToolCallPart, *sdk.FinishStepPart, *sdk.ErrorPart, *sdk.AbortPart: + g.sampling = false + } + return true +} + +func (g *modelSteerGate) interrupt() bool { + if g == nil { + return false + } + g.mu.Lock() + defer g.mu.Unlock() + if !g.sampling || g.stopped { + return false + } + g.stopped = true + g.cancel(errModelSteered) + return true +} + +// An unfinished reasoning block can lack the provider's final signature. +// Keep its text as a checkpoint, never replay opaque/incomplete reasoning. +func steerCheckpointMessages(messages []sdk.Message) []sdk.Message { + result := make([]sdk.Message, 0, len(messages)) + for _, message := range cloneProviderMessages(messages) { + var parts []sdk.MessagePart + for _, part := range message.Content { + switch p := part.(type) { + case sdk.ReasoningPart: + if p.Text != "" { + parts = append(parts, sdk.TextPart{Text: "[Interrupted reasoning checkpoint]\n" + p.Text}) + } + case *sdk.ReasoningPart: + if p.Text != "" { + parts = append(parts, sdk.TextPart{Text: "[Interrupted reasoning checkpoint]\n" + p.Text}) + } + default: + parts = append(parts, part) + } + } + if len(parts) > 0 { + message.Content = parts + result = append(result, message) + } + } + return result +} diff --git a/internal/agent/runtime/native/steer_test.go b/internal/agent/runtime/native/steer_test.go new file mode 100644 index 0000000000..dd290df492 --- /dev/null +++ b/internal/agent/runtime/native/steer_test.go @@ -0,0 +1,286 @@ +package native + +import ( + "context" + "errors" + "fmt" + "strings" + "sync/atomic" + "testing" + "time" + + sdk "github.com/felinics/twilight/sdk" + "github.com/google/jsonschema-go/jsonschema" + + agenttools "github.com/felinics/memoh/internal/agent/tool" +) + +func TestStreamSteerInterruptsOnlyInvocation(t *testing.T) { + for _, mode := range []string{"text", "reasoning", "headers", "consecutive", "retry", "checkpoint_failure"} { + t.Run(mode, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + wake := make(chan struct{}, 1) + started := make(chan int, 3) + var calls, disconnected atomic.Int32 + var pending, continueAfter atomic.Bool + var nextInputs []sdk.Message + var checkpoints, starts, terminals int + var steps []int + var finalInput []sdk.Message + interruptions := 1 + retryAttempts := 0 + if mode == "retry" { + retryAttempts = 1 + } + if mode == "consecutive" { + interruptions = 2 + } + provider := agentStreamTestProvider(func(ctx context.Context, params sdk.GenerateParams) (*sdk.StreamResult, error) { + call := int(calls.Add(1)) + if mode == "retry" && call == 1 { + return closedAgentTestStream(&sdk.ErrorPart{Error: errors.New("unexpected EOF")}), nil + } + call -= retryAttempts + if call > interruptions { + finalInput = cloneProviderMessages(params.Messages) + return closedAgentTestStream(&sdk.TextDeltaPart{Text: "done"}, &sdk.FinishStepPart{FinishReason: sdk.FinishReasonStop}), nil + } + if mode == "headers" { + started <- call + <-ctx.Done() + disconnected.Add(1) + return nil, ctx.Err() + } + parts := make(chan sdk.StreamPart) + go func() { + defer close(parts) + var part sdk.StreamPart = &sdk.TextDeltaPart{Text: fmt.Sprintf("partial-%d", call)} + if mode == "reasoning" { + part = &sdk.ReasoningDeltaPart{Text: "unfinished thinking"} + } + select { + case parts <- part: + case <-ctx.Done(): + } + <-ctx.Done() + disconnected.Add(1) + }() + return &sdk.StreamResult{Stream: parts}, nil + }) + events := New(Deps{}).Stream(ctx, RunConfig{ + Model: &sdk.Model{ID: "mock", Provider: provider}, Messages: []sdk.Message{sdk.UserMessage("original")}, + SteerWake: wake, PendingSteer: func(context.Context) (bool, error) { return pending.Load(), nil }, + ContinueAfterFinal: &continueAfter, NextModelInputs: &nextInputs, + OnSteer: func(_ context.Context, index int, _ *sdk.StepResult) error { + if index != checkpoints { + return fmt.Errorf("step %d, want %d", index, checkpoints) + } + checkpoints++ + if mode == "checkpoint_failure" { + return errors.New("SECRET database diagnostic") + } + pending.Store(false) + nextInputs = []sdk.Message{sdk.UserMessage(fmt.Sprintf("steer-%d", checkpoints))} + continueAfter.Store(true) + return nil + }, + }) + for events != nil { + select { + case <-started: + pending.Store(true) + wake <- struct{}{} + case e, ok := <-events: + if !ok { + events = nil + continue + } + if e.Type == EventTextDelta && strings.HasPrefix(e.Delta, "partial-") || e.Type == EventReasoningDelta { + pending.Store(true) + wake <- struct{}{} + } + if e.Type == EventAgentStart { + starts++ + } + if e.Type == EventStepEnd { + steps = append(steps, e.StepNumber) + } + if strings.Contains(e.Error, "SECRET") || (e.Type == EventError && mode != "checkpoint_failure" && mode != "retry") { + t.Fatalf("unexpected public error: %+v", e) + } + if e.IsTerminal() { + terminals++ + if mode != "checkpoint_failure" && e.Type != EventAgentEnd { + t.Fatalf("steer terminated run: %+v", e) + } + } + case <-ctx.Done(): + t.Fatal("steer failed to continue the blocked invocation") + } + } + if mode == "checkpoint_failure" { + if calls.Load() != 1 { + t.Fatal("continued after failed persistence") + } + return + } + if calls.Load() != int32(interruptions+retryAttempts+1) || disconnected.Load() != int32(interruptions) || starts != 1 || terminals != 1 { + t.Fatalf("calls=%d disconnected=%d starts=%d terminals=%d", calls.Load(), disconnected.Load(), starts, terminals) + } + for i, step := range steps { + if step != i { + t.Fatalf("step cursor: %v", steps) + } + } + var transcript strings.Builder + for _, message := range finalInput { + transcript.WriteString(messageContentText(message)) + for _, part := range message.Content { + if _, ok := part.(sdk.ReasoningPart); ok { + t.Fatal("replayed unfinished provider reasoning") + } + } + } + text := transcript.String() + if !strings.Contains(text, "original") || strings.Count(text, "steer-1") != 1 || mode == "consecutive" && strings.Count(text, "steer-2") != 1 { + t.Fatalf("continuation input: %q", text) + } + }) + } +} + +func TestSteerGatePreservesToolAndCommitBoundaries(t *testing.T) { + for _, part := range []sdk.StreamPart{&sdk.ToolInputStartPart{}, &sdk.StreamToolCallPart{}, &sdk.FinishStepPart{}} { + t.Run(fmt.Sprintf("%T", part), func(t *testing.T) { + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + g := &modelSteerGate{cancel: cancel, ready: make(chan struct{}, 1)} + g.begin() + g.observe(part) + if g.interrupt() || ctx.Err() != nil { + t.Fatal("interrupted tool/commit boundary") + } + g.begin() + if !g.interrupt() || !errors.Is(context.Cause(ctx), errModelSteered) || g.observe(&sdk.FinishStepPart{}) { + t.Fatal("next model invocation failed to fence late output") + } + }) + } +} + +func TestSteerPreservesToolsAndEarlierInput(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + started := make(chan context.Context, 1) + release := make(chan struct{}) + wake := make(chan struct{}, 1) + var calls, executions atomic.Int32 + var pending atomic.Bool + var immediateInput atomic.Bool + var nextInputs []sdk.Message + var finalInput []sdk.Message + provider := agentStreamTestProvider(func(ctx context.Context, params sdk.GenerateParams) (*sdk.StreamResult, error) { + call := calls.Add(1) + if call <= 2 { + if call == 2 { + for _, message := range params.Messages { + if messageContentText(message) == "change direction" { + immediateInput.Store(true) + } + } + } + return closedAgentTestStream( + &sdk.StreamToolCallPart{ToolCallID: fmt.Sprintf("call-%d", call), ToolName: "held_tool", Input: map[string]any{}}, + &sdk.FinishStepPart{FinishReason: sdk.FinishReasonToolCalls}, + ), nil + } + if call == 3 { + parts := make(chan sdk.StreamPart, 1) + parts <- &sdk.TextDeltaPart{Text: "after-tools"} + go func() { <-ctx.Done(); close(parts) }() + return &sdk.StreamResult{Stream: parts}, nil + } + finalInput = cloneProviderMessages(params.Messages) + return closedAgentTestStream(&sdk.TextDeltaPart{Text: "done"}, &sdk.FinishStepPart{FinishReason: sdk.FinishReasonStop}), nil + }) + a := New(Deps{}) + a.SetToolProviders([]agenttools.ToolProvider{staticToolProvider{tools: []sdk.Tool{{ + Name: "held_tool", Parameters: &jsonschema.Schema{Type: "object"}, + Execute: func(ctx *sdk.ToolExecContext, _ any) (any, error) { + if executions.Add(1) > 1 { + return "completed second tool result", nil + } + started <- ctx + select { + case <-release: + return "completed tool result", nil + case <-ctx.Done(): + return nil, ctx.Err() + } + }, + }}}}) + events := a.Stream(ctx, RunConfig{ + Model: &sdk.Model{ID: "mock", Provider: provider}, Messages: []sdk.Message{sdk.UserMessage("original")}, + SupportsToolCall: true, SteerWake: wake, NextModelInputs: &nextInputs, + PendingSteer: func(context.Context) (bool, error) { return pending.Load(), nil }, + OnSteer: func(_ context.Context, index int, _ *sdk.StepResult) error { + if index != 2 { + return errors.New("must not preempt a tool") + } + nextInputs = []sdk.Message{sdk.UserMessage("second change")} + pending.Store(false) + return nil + }, + OnStepCommitted: func(_ context.Context, index int, _ *sdk.StepResult) error { + if index == 0 { + nextInputs = []sdk.Message{sdk.UserMessage("change direction")} + pending.Store(false) + } + return nil + }, + }) + var releaseTimer <-chan time.Time + var toolCtx context.Context + for events != nil { + select { + case toolCtx = <-started: + pending.Store(true) + wake <- struct{}{} + releaseTimer = time.After(25 * time.Millisecond) + case <-releaseTimer: + if toolCtx.Err() != nil { + t.Fatal("steer cancelled the tool") + } + close(release) + releaseTimer = nil + case e, ok := <-events: + switch { + case !ok: + events = nil + case e.Type == EventError || e.Type == EventAgentAbort: + t.Fatalf("tool continuation failed: %+v", e) + case e.Type == EventTextDelta && e.Delta == "after-tools": + pending.Store(true) + wake <- struct{}{} + } + case <-ctx.Done(): + t.Fatal("tool continuation timed out") + } + } + users, secondUsers, results := 0, 0, 0 + for _, message := range finalInput { + if message.Role == sdk.MessageRoleUser && messageContentText(message) == "change direction" { + users++ + } + if message.Role == sdk.MessageRoleTool { + results++ + } + if message.Role == sdk.MessageRoleUser && messageContentText(message) == "second change" { + secondUsers++ + } + } + if calls.Load() != 4 || executions.Load() != 2 || users != 1 || secondUsers != 1 || results != 2 || !immediateInput.Load() { + t.Fatalf("calls=%d tools=%d steer inputs=%d,%d results=%d immediate=%v", calls.Load(), executions.Load(), users, secondUsers, results, immediateInput.Load()) + } +} diff --git a/internal/agent/runtime/native/types.go b/internal/agent/runtime/native/types.go index aeaa18f0f1..38c049878a 100644 --- a/internal/agent/runtime/native/types.go +++ b/internal/agent/runtime/native/types.go @@ -206,6 +206,13 @@ type RunConfig struct { // step found steer input. Native runtime uses it to reopen the same run. ContinueAfterFinal *atomic.Bool NextModelInputs *[]sdk.Message + + // SteerWake announces queue changes; PendingSteer rechecks the authoritative + // queue. OnSteer checkpoints a stopped model attempt and claims its next input. + // These callbacks retain the run context, unlike the cancelled invocation. + SteerWake <-chan struct{} + PendingSteer func(context.Context) (bool, error) + OnSteer func(context.Context, int, *sdk.StepResult) error SuppressAgentStart bool // PromptCacheTTL controls prompt caching for this run. Empty or diff --git a/internal/agent/runtime/session/acceptance/README.md b/internal/agent/runtime/session/acceptance/README.md index d420b0356c..9cb4706f09 100644 --- a/internal/agent/runtime/session/acceptance/README.md +++ b/internal/agent/runtime/session/acceptance/README.md @@ -121,9 +121,18 @@ registry. A skipped or merely compiled suite is not acceptance evidence. `TestQueueFollowUpsPreserveRepeatedReorderAndDrain` checks serial follow-up admission after two reorder operations. `TestQueueSteerDecisionKeepsInputAndHistory` -checks a final-step steer, an ask_user pause, another steer admitted while parked, +checks a steer, an ask_user pause, another steer admitted while parked, and persisted user inputs after the decision continuation. +`TestQueueSteerPreemptsModel` keeps the previous HTTP model request blocked until +test cleanup. Direct steer, follow-up promotion, silent sampling, consecutive +steers, provider retry, and stopping the continued run must preserve the run ID +and exactly one history input per submission. Each steer starts one successor +model call; the retry case also checks the original request's expected retry. +Cluster runs mutate through the +peer Server. Releasing the original request before asserting replacement would +test only eventual step-boundary consumption and is not a passing steer test. + `TestSRDUR002PreparedFinishSurvivesProcessCrash` is opt-in with `MEMOH_SESSION_RUNTIME_ACCEPTANCE_CRASH=1` and requires two Servers. Its temporary PostgreSQL trigger gates only the generated invocation's terminal UPDATE after diff --git a/internal/agent/runtime/session/acceptance/fake_model_test.go b/internal/agent/runtime/session/acceptance/fake_model_test.go index bb22f6dee1..e6c1abb9f9 100644 --- a/internal/agent/runtime/session/acceptance/fake_model_test.go +++ b/internal/agent/runtime/session/acceptance/fake_model_test.go @@ -189,6 +189,12 @@ func (m *fakeModel) handleChatCompletions(writer http.ResponseWriter, request *h directive := parseDirective(userText) m.begin(directive.marker) defer m.finish() + if directive.mode == "retry_block" && m.RequestCount(directive.marker) == 1 { + writeJSON(writer, http.StatusServiceUnavailable, map[string]any{ + "error": map[string]any{"message": "controlled retry", "type": "server_error"}, + }) + return + } requestID := fmt.Sprintf("acceptance-%d", time.Now().UnixNano()) if stream, _ := payload["stream"].(bool); !stream { @@ -251,7 +257,7 @@ func (m *fakeModel) handleChatCompletions(writer http.ResponseWriter, request *h return } } - if directive.mode == "partial_block" { + if directive.mode == "partial_block" || directive.mode == "retry_block" { if err := m.waitForRelease(request.Context(), directive.marker); err != nil { m.markDisconnected(directive.marker) return diff --git a/internal/agent/runtime/session/acceptance/queue_contract_test.go b/internal/agent/runtime/session/acceptance/queue_contract_test.go index 4494365c1a..6a71517322 100644 --- a/internal/agent/runtime/session/acceptance/queue_contract_test.go +++ b/internal/agent/runtime/session/acceptance/queue_contract_test.go @@ -24,6 +24,114 @@ func enqueueTestInput(t *testing.T, fixture acceptanceFixture, sessionID, kind, return item.ID } +// Never release a blocked request to make steer pass: the new model request +// and cancellation of its predecessor must happen while that predecessor is held. +func TestQueueSteerPreemptsModel(t *testing.T) { + for _, scenario := range []string{"direct", "promoted", "silent", "consecutive", "retry", "stop_after_steer"} { + t.Run(scenario, func(t *testing.T) { + env := loadEnvironment() + fixture := requireFixture(t, env.mode == "cluster") + prepareFakeModel(t) + sessionID := mustCreateSession(t, fixture, "steer-preempt") + origin := uniqueMarker("original") + mode := "partial_block" + if scenario == "silent" { + mode = "block" + } else if scenario == "retry" { + mode = "retry_block" + } + conn := mustDial(t, env.primaryURL, fixture) + defer closeWebSocket(conn) + mustSubscribeAndReadSnapshot(t, conn, sessionID) + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, origin, directiveMode(origin, 2, 5, mode)) + defer globalFakeModel.Release(origin) + waitBlocked := func(marker string, silent bool) { + t.Helper() + if silent { + if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { + t.Fatal("original model request never started") + } + } else if _, err := readUntil(conn, 5*time.Second, func(e wsEvent) bool { + return eventsContainString([]wsEvent{e}, marker+"-chunk-01") + }); err != nil { + t.Fatal(err) + } + } + waitBlocked(origin, scenario == "silent") + mutator := fixture + if env.mode == "cluster" { + mutator.api = fixture.api.forBaseURL(env.secondaryURL) + } + markers := []string{origin} + count := 1 + if scenario == "consecutive" { + count = 2 + } + for i := 0; i < count; i++ { + marker := uniqueMarker("steered") + defer globalFakeModel.Release(marker) + blocked := i < count-1 || scenario == "stop_after_steer" + text := directive(marker, 2, 5) + if blocked { + text = directiveMode(marker, 2, 5, "partial_block") + } + started := time.Now() + if scenario == "promoted" { + id := enqueueTestInput(t, mutator, sessionID, "follow-up", text) + if err := mutator.api.request(http.MethodPost, "/bots/"+fixture.botID+"/sessions/"+sessionID+"/follow-up-queue/"+id+"/steer", nil, nil, http.StatusAccepted); err != nil { + t.Fatal(err) + } + } else { + enqueueTestInput(t, mutator, sessionID, "steer", text) + } + if !globalFakeModel.WaitRequestCount(marker, 1, 3*time.Second) || !globalFakeModel.WaitDisconnected(markers[len(markers)-1], time.Second) { + t.Fatal("steer did not replace the blocked invocation") + } + t.Logf("same_run=%s preempt=%d latency=%s", admitted.RunID, i+1, time.Since(started)) + markers = append(markers, marker) + if blocked { + waitBlocked(marker, false) + } + } + terminalState := "completed" + if scenario == "stop_after_steer" { + terminalState = "aborted" + control := uniqueMarker("stop") + if err := sendAbort(conn, sessionID, admitted.RunID, control); err != nil { + t.Fatal(err) + } + if ack := mustReadControlAck(t, conn, control); !ack.Applied { + t.Fatal("steer detached continuation from run abort") + } + } + mustReadRunTerminal(t, conn, admitted.RunID) + run := mustWaitRunState(t, sessionID, origin, func(r sessionRunRecord) bool { return r.State == terminalState }) + if run.RunID != admitted.RunID { + t.Fatal("steer re-admitted a run") + } + history, err := fixture.api.history(fixture.botID, sessionID) + if err != nil { + t.Fatal(err) + } + for _, marker := range markers { + users := 0 + for _, msg := range objectList(history) { + if stringValue(msg["role"]) == "user" && valueContainsString(msg, marker) { + users++ + } + } + requests := 1 + if marker == origin && scenario == "retry" { + requests = 2 + } + if users != 1 || globalFakeModel.RequestCount(marker) != requests { + t.Fatalf("%s: durable users=%d model requests=%d", marker, users, globalFakeModel.RequestCount(marker)) + } + } + }) + } +} + func TestQueueFollowUpsPreserveRepeatedReorderAndDrain(t *testing.T) { fixture := requireFixture(t, false) prepareFakeModel(t) @@ -33,9 +141,11 @@ func TestQueueFollowUpsPreserveRepeatedReorderAndDrain(t *testing.T) { conn := mustDial(t, loadEnvironment().primaryURL, fixture) defer closeWebSocket(conn) mustSubscribeAndReadSnapshot(t, conn, sessionID) - _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "block")) - if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { - t.Fatal("origin never reached model") + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "partial_block")) + if _, err := readUntil(conn, 5*time.Second, func(e wsEvent) bool { + return eventsContainString([]wsEvent{e}, marker+"-chunk-00") + }); err != nil { + t.Fatal(err) } defer globalFakeModel.Release(marker) ids := make([]string, 3) @@ -97,15 +207,16 @@ func testQueueSteerDecisionKeepsInputAndHistory(t *testing.T, restartOwner bool) conn := mustDial(t, loadEnvironment().primaryURL, fixture) defer closeWebSocket(conn) mustSubscribeAndReadSnapshot(t, conn, sessionID) - _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "block")) - if !globalFakeModel.WaitRequestCount(marker, 1, 5*time.Second) { - t.Fatal("origin never reached model") + _, admitted := mustSendAndAccept(t, fixture, conn, sessionID, invocation, directiveMode(marker, 1, 0, "partial_block")) + if _, err := readUntil(conn, 5*time.Second, func(e wsEvent) bool { + return eventsContainString([]wsEvent{e}, marker+"-chunk-00") + }); err != nil { + t.Fatal(err) } defer globalFakeModel.Release(marker) steerMarker := uniqueMarker("steer-decision") steerText := directiveMode(steerMarker, 2, 5, "ask_user") + " ask after steering" enqueueTestInput(t, fixture, sessionID, "steer", steerText) - globalFakeModel.Release(marker) waiting := mustWaitRunState(t, sessionID, invocation, func(run sessionRunRecord) bool { return run.State == "waiting_decision" }) decision := mustPendingUserInput(t, waiting) answers, err := firstDecisionAnswer(decision.UIPayload) diff --git a/internal/agent/runtime/session/commands.go b/internal/agent/runtime/session/commands.go index 4a6fb71d89..d7cc771d7b 100644 --- a/internal/agent/runtime/session/commands.go +++ b/internal/agent/runtime/session/commands.go @@ -809,7 +809,7 @@ func (m *Manager) requestAbort(ctx context.Context, ctrl *runControl) (bool, err func (m *Manager) applyCommand(ctx context.Context, cmd Command) { switch strings.TrimSpace(cmd.Type) { - case CommandAbort, CommandToolApprovalResponse, CommandUserInputResponse, CommandHistoryReset: + case CommandAbort, CommandSteerWake, CommandToolApprovalResponse, CommandUserInputResponse, CommandHistoryReset: m.publishStoredCommandResult(ctx, cmd, m.executeRoutedCommand(ctx, cmd)) case CommandResult: m.completePendingCommand(cmd) @@ -881,6 +881,13 @@ func (m *Manager) applyRoutedCommand(ctx context.Context, cmd Command) error { _, err := m.abortLocal(commandCtx, ctrl) return err } + if strings.TrimSpace(cmd.Type) == CommandSteerWake { + if !run.SteerSupported || (run.Status != RunStatusRunning && run.Status != RunStatusWaitingDecision) { + return ErrCommandTargetNotActive + } + m.wakeSteer(ctrl) + return nil + } if strings.TrimSpace(cmd.Type) == CommandHistoryReset { return m.applyHistoryResetCommand(commandCtx, cmd, ctrl) } @@ -1259,7 +1266,7 @@ func (m *Manager) finishCommandExecution(commandID string, done chan struct{}) { func isDurableRoutedCommand(cmd Command) bool { switch strings.TrimSpace(cmd.Type) { - case CommandAbort, CommandToolApprovalResponse, CommandUserInputResponse, CommandHistoryReset: + case CommandAbort, CommandSteerWake, CommandToolApprovalResponse, CommandUserInputResponse, CommandHistoryReset: return strings.TrimSpace(cmd.ID) != "" default: return false diff --git a/internal/agent/runtime/session/live_queue_manager.go b/internal/agent/runtime/session/live_queue_manager.go index 71f02ba054..8a3e94529f 100644 --- a/internal/agent/runtime/session/live_queue_manager.go +++ b/internal/agent/runtime/session/live_queue_manager.go @@ -2,7 +2,10 @@ package sessionruntime import ( "context" + "log/slog" "time" + + "github.com/google/uuid" ) func (m *Manager) liveQueueBackend() (LiveQueueBackend, error) { @@ -19,6 +22,9 @@ func (m *Manager) liveQueueBackend() (LiveQueueBackend, error) { // EnableSteer advertises an actual execution consumer, not just an allocated // channel. Other instances therefore reject queues aimed at old/unsupported owners. func (m *Manager) EnableSteer(ctx context.Context, handle RunHandle) error { + if m.SteerWake(handle) == nil { + return ErrRunOwnershipLost + } _, _, err := m.updateActiveAndPublish(ctx, handle, func(snapshot Snapshot, now time.Time) (Snapshot, bool, error) { run := snapshot.CurrentRunView if !runMatchesHandle(run, handle) || !m.runOwnerMatches(run) || @@ -42,7 +48,11 @@ func (m *Manager) EnqueueSteer(ctx context.Context, key Key, itemID, invocationI if err != nil { return SteerItem{}, err } - return queue.EnqueueSteer(ctx, key, itemID, invocationID, payload) + item, err := queue.EnqueueSteer(ctx, key, itemID, invocationID, payload) + if err == nil && item.Status == QueueAccepted { + m.notifySteer(ctx, key, item.TargetRunID) + } + return item, err } func (m *Manager) EnqueueFollowUp(ctx context.Context, key Key, itemID, invocationID string, payload []byte) (FollowUpItem, error) { @@ -114,7 +124,67 @@ func (m *Manager) PromoteFollowUpToSteer(ctx context.Context, key Key, ref Follo if err != nil { return PromoteFollowUpResult{}, err } - return queue.PromoteFollowUpToSteer(ctx, key, ref) + result, err := queue.PromoteFollowUpToSteer(ctx, key, ref) + if err == nil && result.Steer.Status == QueueAccepted { + m.notifySteer(ctx, key, result.Steer.TargetRunID) + } + return result, err +} + +// SteerWake is an owner-local, coalesced notification. The queue remains the +// source of truth; neither duplicate notifications nor a stale wake apply input. +func (m *Manager) SteerWake(handle RunHandle) <-chan struct{} { + m.mu.Lock() + defer m.mu.Unlock() + ctrl := m.controls[scopedRunControlKey(handle.BotID, handle.SessionID, handle.RunID)] + if ctrl == nil || ctrl.generation != handle.Generation { + return nil + } + if ctrl.steerWake == nil { + ctrl.steerWake = make(chan struct{}, 1) + } + return ctrl.steerWake +} + +func (m *Manager) wakeSteer(ctrl *runControl) { + m.mu.Lock() + defer m.mu.Unlock() + if m.controls[ctrl.key()] != ctrl || ctrl.steerWake == nil { + return + } + select { + case ctrl.steerWake <- struct{}{}: + default: + } +} + +func (m *Manager) notifySteer(ctx context.Context, key Key, runID string) { + // Input is already accepted. Finish the bounded notification independently + // of the HTTP connection, and never report a delivery error as a rejected input. + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), m.commandTimeout()) + defer cancel() + snapshot, ok, err := m.backend.Load(ctx, key) + if err != nil || !ok || snapshot.CurrentRunView == nil || snapshot.CurrentRunView.RunID != runID { + return + } + run := snapshot.CurrentRunView + now, err := m.backend.Now(ctx) + if err != nil { + return + } + cmd := Command{ + Type: CommandSteerWake, ID: uuid.NewString(), BotID: key.BotID, + SessionID: key.SessionID, RunID: runID, Generation: run.Generation, + FencingToken: run.FencingToken, CreatedAt: now, ExpiresAt: now.Add(m.commandTimeout()), + } + if m.runOwnerMatches(run) { + err = m.applyRoutedCommand(ctx, cmd) + } else if m.distributed != nil { + err = m.dispatchRemoteCommand(ctx, run.OwnerID, cmd) + } + if err != nil { + m.logger.Warn("notify accepted steer failed", slog.String("run_id", runID), slog.Any("error", err)) + } } func (m *Manager) ClaimNextSteer(ctx context.Context, handle RunHandle, sealIfEmpty bool) (SteerItem, SteerClaimRef, bool, error) { diff --git a/internal/agent/runtime/session/manager.go b/internal/agent/runtime/session/manager.go index 6c8569a6dc..e94779fe63 100644 --- a/internal/agent/runtime/session/manager.go +++ b/internal/agent/runtime/session/manager.go @@ -107,6 +107,7 @@ type runControl struct { injectCh chan<- turn.InjectMessage injectMu sync.Mutex injectStopped bool + steerWake chan struct{} // guarded by Manager.mu; never closed converter *chatview.UIMessageStreamConverter leaseStop func() leaseDone chan struct{} diff --git a/internal/agent/runtime/session/steer_wake_test.go b/internal/agent/runtime/session/steer_wake_test.go new file mode 100644 index 0000000000..ce2bcae3ac --- /dev/null +++ b/internal/agent/runtime/session/steer_wake_test.go @@ -0,0 +1,64 @@ +package sessionruntime + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestSteerWakeRetainsQueueAndFencesOwner(t *testing.T) { + f := newAdmitFixture(t) + ctx := context.Background() + admitted, err := f.manager.Admit(ctx, f.input("steer", `{"text":"start"}`)) + if err != nil { + t.Fatal(err) + } + handle := admitted.Handle + if err := f.manager.EnableSteer(ctx, handle); err != nil { + t.Fatal(err) + } + wake := f.manager.SteerWake(handle) + key := handle.key() + item, err := f.manager.EnqueueSteer(ctx, key, "one", "one", []byte(`{"text":"adjust"}`)) + if err != nil { + t.Fatal(err) + } + select { + case <-wake: + case <-time.After(time.Second): + t.Fatal("accepted queue input did not wake its owner") + } + cmd := Command{ + Type: CommandSteerWake, BotID: handle.BotID, SessionID: handle.SessionID, + RunID: handle.RunID, Generation: handle.Generation, FencingToken: handle.FencingToken, + } + for range 3 { + if err := f.manager.applyRoutedCommand(ctx, cmd); err != nil { + t.Fatal(err) + } + } + <-wake + select { + case <-wake: + t.Fatal("duplicate wakes did not coalesce") + default: + } + cmd.Generation = "stale" + if err := f.manager.applyRoutedCommand(ctx, cmd); !errors.Is(err, ErrCommandTargetNotActive) { + t.Fatalf("stale owner wake: %v", err) + } + steers, _, err := f.manager.PendingQueues(ctx, key, 0) + if err != nil || len(steers) != 1 || steers[0].ID != item.ID || steers[0].Status != QueueAccepted { + t.Fatalf("wake applied or lost input: %+v, %v", steers, err) + } + // Cancellation between acknowledgement and consumption leaves at most a + // stale notification, never a second input or a resurrected queue item. + if err := f.manager.CancelSteer(ctx, key, item.ID); err != nil { + t.Fatal(err) + } + steers, _, err = f.manager.PendingQueues(ctx, key, 0) + if err != nil || len(steers) != 0 { + t.Fatalf("cancelled input survived: %+v %v", steers, err) + } +} diff --git a/internal/agent/runtime/session/types.go b/internal/agent/runtime/session/types.go index 3d200b18f8..4c9014dc42 100644 --- a/internal/agent/runtime/session/types.go +++ b/internal/agent/runtime/session/types.go @@ -35,6 +35,7 @@ const ( RunOperationEdit = "edit" CommandAbort = "abort" + CommandSteerWake = "steer_wake" CommandToolApprovalResponse = "tool_approval_response" CommandUserInputResponse = "user_input_response" CommandHistoryReset = "history_reset" diff --git a/internal/handlers/local_channel.go b/internal/handlers/local_channel.go index 62333b1ab2..12ba23ad10 100644 --- a/internal/handlers/local_channel.go +++ b/internal/handlers/local_channel.go @@ -2335,7 +2335,7 @@ func (h *LocalChannelHandler) HandleWebSocket(c echo.Context) error { AgentCommand: decision.AgentCommand, RunHandle: admittedTurn.Handle, InjectCh: admittedTurn.InjectCh, - QueueInjectCh: admittedTurn.InjectCh, + QueueSteerEnabled: admittedTurn.InjectCh != nil, } if preparedActivationReq != nil { req.Messages = preparedActivationReq.Messages @@ -2415,6 +2415,8 @@ func (h *LocalChannelHandler) HandleWebSocket(c echo.Context) error { input.RunID = runRef.RunID input.TurnID = admittedTurn.TurnID input.TurnPosition = admittedTurn.Position + input.RunHandle = admittedTurn.Handle + input.InjectCh = admittedTurn.InjectCh input.OnModelPreferenceSettled = func() { writer.SendJSON(wsOutboundEvent{ Type: "model_preference_settled", @@ -2423,8 +2425,6 @@ func (h *LocalChannelHandler) HandleWebSocket(c echo.Context) error { SessionID: runRef.SessionID, }) } - input.RunHandle = admittedTurn.Handle - input.InjectCh = admittedTurn.InjectCh return h.agentService.RetryLatestMessageWS(ctx, input, eventCh, abortCh) }, )