mirror of
https://github.com/openai/codex.git
synced 2026-05-22 20:14:17 +00:00
Compare commits
3 Commits
rust-v0.13
...
dev/efraze
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80120d0e81 | ||
|
|
fd9abc9b63 | ||
|
|
1e4ee86f65 |
@@ -1863,6 +1863,162 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AskForApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the approval policy for this turn and subsequent turns."
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ApprovalsReviewer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
|
||||
},
|
||||
"collaborationMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "EXPERIMENTAL - Set a pre-set collaboration mode. Takes precedence over model, reasoning_effort, and developer instructions if set.\n\nFor `collaboration_mode.settings.developer_instructions`, `null` means \"use the built-in instructions for the selected mode\"."
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Override the working directory for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning effort for this turn and subsequent turns."
|
||||
},
|
||||
"environments": {
|
||||
"description": "Optional turn-scoped environments.\n\nOmitted uses the thread sticky environments. Empty disables environment access for this turn. Non-empty selects the first environment as the current turn environment for this turn.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TurnEnvironmentParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"description": "Override the model for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"permissions": {
|
||||
"description": "Select a named permissions profile id for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
},
|
||||
"responsesapiClientMetadata": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional turn-scoped Responses API client metadata.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"description": "Replace the thread's runtime workspace roots for this turn and subsequent turns. Relative paths are resolved against the effective cwd for the turn.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningSummary"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning summary for this turn and subsequent turns."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RealtimeOutputModality": {
|
||||
"enum": [
|
||||
"text",
|
||||
|
||||
@@ -415,6 +415,65 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||
"enum": [
|
||||
"user",
|
||||
"auto_review",
|
||||
"guardian_subagent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AskForApproval": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"granular": {
|
||||
"properties": {
|
||||
"mcp_elicitations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"request_permissions": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sandbox_approval": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"skill_approval": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mcp_elicitations",
|
||||
"rules",
|
||||
"sandbox_approval"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"granular"
|
||||
],
|
||||
"title": "GranularAskForApproval",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AuthMode": {
|
||||
"description": "Authentication mode for OpenAI-backed providers.",
|
||||
"oneOf": [
|
||||
@@ -658,6 +717,22 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CollaborationMode": {
|
||||
"description": "Collaboration mode for a Codex session.",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"$ref": "#/definitions/ModeKind"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/Settings"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mode",
|
||||
"settings"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CommandAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2258,6 +2333,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
"plan",
|
||||
"default"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModelRerouteReason": {
|
||||
"enum": [
|
||||
"highRiskCyberActivity"
|
||||
@@ -2319,6 +2402,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
"enabled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkApprovalProtocol": {
|
||||
"enum": [
|
||||
"http",
|
||||
@@ -2402,6 +2492,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PlanDeltaNotification": {
|
||||
"description": "EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should not assume concatenated deltas match the completed plan item content.",
|
||||
"properties": {
|
||||
@@ -2533,6 +2631,221 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"QueuedTurn": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/QueuedTurnStatus"
|
||||
},
|
||||
"turnStartParams": {
|
||||
"$ref": "#/definitions/QueuedTurnStartParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"turnStartParams"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AskForApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the approval policy for this turn and subsequent turns."
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ApprovalsReviewer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
|
||||
},
|
||||
"collaborationMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "EXPERIMENTAL - Set a pre-set collaboration mode. Takes precedence over model, reasoning_effort, and developer instructions if set.\n\nFor `collaboration_mode.settings.developer_instructions`, `null` means \"use the built-in instructions for the selected mode\"."
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Override the working directory for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning effort for this turn and subsequent turns."
|
||||
},
|
||||
"environments": {
|
||||
"description": "Optional turn-scoped environments.\n\nOmitted uses the thread sticky environments. Empty disables environment access for this turn. Non-empty selects the first environment as the current turn environment for this turn.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TurnEnvironmentParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"description": "Override the model for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"permissions": {
|
||||
"description": "Select a named permissions profile id for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
},
|
||||
"responsesapiClientMetadata": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional turn-scoped Responses API client metadata.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"description": "Replace the thread's runtime workspace roots for this turn and subsequent turns. Relative paths are resolved against the effective cwd for the turn.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningSummary"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning summary for this turn and subsequent turns."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"pending"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatus",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/TurnError"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatus",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RateLimitReachedType": {
|
||||
"enum": [
|
||||
"rate_limit_reached",
|
||||
@@ -2655,6 +2968,26 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"auto",
|
||||
"concise",
|
||||
"detailed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Option to disable reasoning summaries.",
|
||||
"enum": [
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReasoningSummaryPartAddedNotification": {
|
||||
"properties": {
|
||||
"itemId": {
|
||||
@@ -2807,6 +3140,105 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"dangerFullAccess"
|
||||
],
|
||||
"title": "DangerFullAccessSandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DangerFullAccessSandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"readOnly"
|
||||
],
|
||||
"title": "ReadOnlySandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "ReadOnlySandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"networkAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkAccess"
|
||||
}
|
||||
],
|
||||
"default": "restricted"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"externalSandbox"
|
||||
],
|
||||
"title": "ExternalSandboxSandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalSandboxSandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"excludeSlashTmp": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTmpdirEnvVar": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
],
|
||||
"title": "WorkspaceWriteSandboxPolicyType",
|
||||
"type": "string"
|
||||
},
|
||||
"writableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WorkspaceWriteSandboxPolicy",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServerRequestResolvedNotification": {
|
||||
"properties": {
|
||||
"requestId": {
|
||||
@@ -2862,6 +3294,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Settings for a collaboration mode.",
|
||||
"properties": {
|
||||
"developer_instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reasoning_effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"model"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsChangedNotification": {
|
||||
"description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.",
|
||||
"type": "object"
|
||||
@@ -3966,6 +4426,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadQueueChangedNotification": {
|
||||
"properties": {
|
||||
"queuedTurns": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/QueuedTurn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"queuedTurns",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadRealtimeAudioChunk": {
|
||||
"description": "EXPERIMENTAL - thread realtime audio chunk.",
|
||||
"properties": {
|
||||
@@ -4443,6 +4921,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"environmentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
@@ -5089,6 +5582,26 @@
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/queue/changed"
|
||||
],
|
||||
"title": "Thread/queue/changedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadQueueChangedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/queue/changedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -4031,6 +4031,26 @@
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/queue/changed"
|
||||
],
|
||||
"title": "Thread/queue/changedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadQueueChangedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/queue/changedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -13040,6 +13060,221 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurn": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/QueuedTurnStatus"
|
||||
},
|
||||
"turnStartParams": {
|
||||
"$ref": "#/definitions/v2/QueuedTurnStartParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"turnStartParams"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AskForApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the approval policy for this turn and subsequent turns."
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ApprovalsReviewer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
|
||||
},
|
||||
"collaborationMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/CollaborationMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "EXPERIMENTAL - Set a pre-set collaboration mode. Takes precedence over model, reasoning_effort, and developer instructions if set.\n\nFor `collaboration_mode.settings.developer_instructions`, `null` means \"use the built-in instructions for the selected mode\"."
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Override the working directory for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning effort for this turn and subsequent turns."
|
||||
},
|
||||
"environments": {
|
||||
"description": "Optional turn-scoped environments.\n\nOmitted uses the thread sticky environments. Empty disables environment access for this turn. Non-empty selects the first environment as the current turn environment for this turn.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/TurnEnvironmentParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/UserInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"description": "Override the model for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"permissions": {
|
||||
"description": "Select a named permissions profile id for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
},
|
||||
"responsesapiClientMetadata": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional turn-scoped Responses API client metadata.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"description": "Replace the thread's runtime workspace roots for this turn and subsequent turns. Relative paths are resolved against the effective cwd for the turn.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ReasoningSummary"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning summary for this turn and subsequent turns."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"pending"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatus",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/v2/TurnError"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatus",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RateLimitReachedType": {
|
||||
"enum": [
|
||||
"rate_limit_reached",
|
||||
@@ -16616,6 +16851,26 @@
|
||||
"title": "ThreadNameUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadQueueChangedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"queuedTurns": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/QueuedTurn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"queuedTurns",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadQueueChangedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
@@ -9589,6 +9589,221 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurn": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/QueuedTurnStatus"
|
||||
},
|
||||
"turnStartParams": {
|
||||
"$ref": "#/definitions/QueuedTurnStartParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"turnStartParams"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AskForApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the approval policy for this turn and subsequent turns."
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ApprovalsReviewer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
|
||||
},
|
||||
"collaborationMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "EXPERIMENTAL - Set a pre-set collaboration mode. Takes precedence over model, reasoning_effort, and developer instructions if set.\n\nFor `collaboration_mode.settings.developer_instructions`, `null` means \"use the built-in instructions for the selected mode\"."
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Override the working directory for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning effort for this turn and subsequent turns."
|
||||
},
|
||||
"environments": {
|
||||
"description": "Optional turn-scoped environments.\n\nOmitted uses the thread sticky environments. Empty disables environment access for this turn. Non-empty selects the first environment as the current turn environment for this turn.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TurnEnvironmentParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"description": "Override the model for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"permissions": {
|
||||
"description": "Select a named permissions profile id for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
},
|
||||
"responsesapiClientMetadata": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional turn-scoped Responses API client metadata.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"description": "Replace the thread's runtime workspace roots for this turn and subsequent turns. Relative paths are resolved against the effective cwd for the turn.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningSummary"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning summary for this turn and subsequent turns."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"pending"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatus",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/TurnError"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatus",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RateLimitReachedType": {
|
||||
"enum": [
|
||||
"rate_limit_reached",
|
||||
@@ -11290,6 +11505,26 @@
|
||||
"title": "Thread/goal/clearedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/queue/changed"
|
||||
],
|
||||
"title": "Thread/queue/changedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadQueueChangedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/queue/changedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -14440,6 +14675,26 @@
|
||||
"title": "ThreadNameUpdatedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadQueueChangedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"queuedTurns": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/QueuedTurn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"queuedTurns",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadQueueChangedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
||||
876
codex-rs/app-server-protocol/schema/json/v2/ThreadQueueChangedNotification.json
generated
Normal file
876
codex-rs/app-server-protocol/schema/json/v2/ThreadQueueChangedNotification.json
generated
Normal file
@@ -0,0 +1,876 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"ApprovalsReviewer": {
|
||||
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
|
||||
"enum": [
|
||||
"user",
|
||||
"auto_review",
|
||||
"guardian_subagent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AskForApproval": {
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"granular": {
|
||||
"properties": {
|
||||
"mcp_elicitations": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"request_permissions": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sandbox_approval": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"skill_approval": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mcp_elicitations",
|
||||
"rules",
|
||||
"sandbox_approval"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"granular"
|
||||
],
|
||||
"title": "GranularAskForApproval",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ByteRange": {
|
||||
"properties": {
|
||||
"end": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"start": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"end",
|
||||
"start"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"contextWindowExceeded",
|
||||
"usageLimitExceeded",
|
||||
"serverOverloaded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"unauthorized",
|
||||
"badRequest",
|
||||
"threadRollbackFailed",
|
||||
"sandboxError",
|
||||
"other"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"title": "HttpConnectionFailedCodexErrorInfo",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "Failed to connect to the response SSE stream.",
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"title": "ResponseStreamConnectionFailedCodexErrorInfo",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "The response SSE stream disconnected in the middle of a turn before completion.",
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"title": "ResponseStreamDisconnectedCodexErrorInfo",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "Reached the retry limit for responses.",
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"title": "ResponseTooManyFailedAttemptsCodexErrorInfo",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.",
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/definitions/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"title": "ActiveTurnNotSteerableCodexErrorInfo",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollaborationMode": {
|
||||
"description": "Collaboration mode for a Codex session.",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"$ref": "#/definitions/ModeKind"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/Settings"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mode",
|
||||
"settings"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetail": {
|
||||
"enum": [
|
||||
"high",
|
||||
"original"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
"plan",
|
||||
"default"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkAccess": {
|
||||
"enum": [
|
||||
"restricted",
|
||||
"enabled"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"review",
|
||||
"compact"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"QueuedTurn": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/QueuedTurnStatus"
|
||||
},
|
||||
"turnStartParams": {
|
||||
"$ref": "#/definitions/QueuedTurnStartParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"turnStartParams"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStartParams": {
|
||||
"properties": {
|
||||
"approvalPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AskForApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the approval policy for this turn and subsequent turns."
|
||||
},
|
||||
"approvalsReviewer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ApprovalsReviewer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override where approval requests are routed for review on this turn and subsequent turns."
|
||||
},
|
||||
"collaborationMode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "EXPERIMENTAL - Set a pre-set collaboration mode. Takes precedence over model, reasoning_effort, and developer instructions if set.\n\nFor `collaboration_mode.settings.developer_instructions`, `null` means \"use the built-in instructions for the selected mode\"."
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Override the working directory for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning effort for this turn and subsequent turns."
|
||||
},
|
||||
"environments": {
|
||||
"description": "Optional turn-scoped environments.\n\nOmitted uses the thread sticky environments. Empty disables environment access for this turn. Non-empty selects the first environment as the current turn environment for this turn.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TurnEnvironmentParams"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"input": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserInput"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"description": "Override the model for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"permissions": {
|
||||
"description": "Select a named permissions profile id for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
},
|
||||
"responsesapiClientMetadata": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Optional turn-scoped Responses API client metadata.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"runtimeWorkspaceRoots": {
|
||||
"description": "Replace the thread's runtime workspace roots for this turn and subsequent turns. Relative paths are resolved against the effective cwd for the turn.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"description": "Override the service tier for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningSummary"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the reasoning summary for this turn and subsequent turns."
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuedTurnStatus": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"pending"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "PendingQueuedTurnStatus",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/definitions/TurnError"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"failed"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatusType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"title": "FailedQueuedTurnStatus",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
"description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"auto",
|
||||
"concise",
|
||||
"detailed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Option to disable reasoning summaries.",
|
||||
"enum": [
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"dangerFullAccess"
|
||||
],
|
||||
"title": "DangerFullAccessSandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "DangerFullAccessSandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"readOnly"
|
||||
],
|
||||
"title": "ReadOnlySandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "ReadOnlySandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"networkAccess": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NetworkAccess"
|
||||
}
|
||||
],
|
||||
"default": "restricted"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"externalSandbox"
|
||||
],
|
||||
"title": "ExternalSandboxSandboxPolicyType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "ExternalSandboxSandboxPolicy",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"excludeSlashTmp": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"excludeTmpdirEnvVar": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"networkAccess": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"workspaceWrite"
|
||||
],
|
||||
"title": "WorkspaceWriteSandboxPolicyType",
|
||||
"type": "string"
|
||||
},
|
||||
"writableRoots": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"title": "WorkspaceWriteSandboxPolicy",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Settings for a collaboration mode.",
|
||||
"properties": {
|
||||
"developer_instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"reasoning_effort": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"model"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ByteRange"
|
||||
}
|
||||
],
|
||||
"description": "Byte range in the parent `text` buffer that this element occupies."
|
||||
},
|
||||
"placeholder": {
|
||||
"description": "Optional human-readable placeholder for the element, displayed in the UI.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"byteRange"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnEnvironmentParams": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"environmentId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"environmentId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserInput": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"text_elements": {
|
||||
"default": [],
|
||||
"description": "UI-defined spans within `text` used to render or persist special elements.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TextElement"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"text"
|
||||
],
|
||||
"title": "TextUserInputType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
"type"
|
||||
],
|
||||
"title": "TextUserInput",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ImageDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"image"
|
||||
],
|
||||
"title": "ImageUserInputType",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"title": "ImageUserInput",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"detail": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ImageDetail"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"localImage"
|
||||
],
|
||||
"title": "LocalImageUserInputType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "LocalImageUserInput",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"skill"
|
||||
],
|
||||
"title": "SkillUserInputType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "SkillUserInput",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"mention"
|
||||
],
|
||||
"title": "MentionUserInputType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "MentionUserInput",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"queuedTurns": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/QueuedTurn"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"queuedTurns",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadQueueChangedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
7
codex-rs/app-server-protocol/schema/typescript/v2/QueuedTurn.ts
generated
Normal file
7
codex-rs/app-server-protocol/schema/typescript/v2/QueuedTurn.ts
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { QueuedTurnStatus } from "./QueuedTurnStatus";
|
||||
import type { TurnStartParams } from "./TurnStartParams";
|
||||
|
||||
export type QueuedTurn = { id: string, turnStartParams: TurnStartParams, status: QueuedTurnStatus, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/QueuedTurnStatus.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/QueuedTurnStatus.ts
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TurnError } from "./TurnError";
|
||||
|
||||
export type QueuedTurnStatus = { "type": "pending" } | { "type": "failed", error: TurnError, };
|
||||
6
codex-rs/app-server-protocol/schema/typescript/v2/ThreadQueueChangedNotification.ts
generated
Normal file
6
codex-rs/app-server-protocol/schema/typescript/v2/ThreadQueueChangedNotification.ts
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { QueuedTurn } from "./QueuedTurn";
|
||||
|
||||
export type ThreadQueueChangedNotification = { threadId: string, queuedTurns: Array<QueuedTurn>, };
|
||||
@@ -306,6 +306,8 @@ export type { ProcessOutputDeltaNotification } from "./ProcessOutputDeltaNotific
|
||||
export type { ProcessOutputStream } from "./ProcessOutputStream";
|
||||
export type { ProcessTerminalSize } from "./ProcessTerminalSize";
|
||||
export type { ProfileV2 } from "./ProfileV2";
|
||||
export type { QueuedTurn } from "./QueuedTurn";
|
||||
export type { QueuedTurnStatus } from "./QueuedTurnStatus";
|
||||
export type { RateLimitReachedType } from "./RateLimitReachedType";
|
||||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||||
export type { RateLimitWindow } from "./RateLimitWindow";
|
||||
@@ -376,6 +378,7 @@ export type { ThreadMetadataGitInfoUpdateParams } from "./ThreadMetadataGitInfoU
|
||||
export type { ThreadMetadataUpdateParams } from "./ThreadMetadataUpdateParams";
|
||||
export type { ThreadMetadataUpdateResponse } from "./ThreadMetadataUpdateResponse";
|
||||
export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification";
|
||||
export type { ThreadQueueChangedNotification } from "./ThreadQueueChangedNotification";
|
||||
export type { ThreadReadParams } from "./ThreadReadParams";
|
||||
export type { ThreadReadResponse } from "./ThreadReadResponse";
|
||||
export type { ThreadRealtimeAudioChunk } from "./ThreadRealtimeAudioChunk";
|
||||
|
||||
@@ -512,6 +512,30 @@ client_request_definitions! {
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadGoalClearResponse,
|
||||
},
|
||||
#[experimental("thread/queue/add")]
|
||||
ThreadQueueAdd => "thread/queue/add" {
|
||||
params: v2::ThreadQueueAddParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadQueueAddResponse,
|
||||
},
|
||||
#[experimental("thread/queue/list")]
|
||||
ThreadQueueList => "thread/queue/list" {
|
||||
params: v2::ThreadQueueListParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadQueueListResponse,
|
||||
},
|
||||
#[experimental("thread/queue/delete")]
|
||||
ThreadQueueDelete => "thread/queue/delete" {
|
||||
params: v2::ThreadQueueDeleteParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadQueueDeleteResponse,
|
||||
},
|
||||
#[experimental("thread/queue/reorder")]
|
||||
ThreadQueueReorder => "thread/queue/reorder" {
|
||||
params: v2::ThreadQueueReorderParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadQueueReorderResponse,
|
||||
},
|
||||
ThreadMetadataUpdate => "thread/metadata/update" {
|
||||
params: v2::ThreadMetadataUpdateParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
@@ -1465,6 +1489,8 @@ server_notification_definitions! {
|
||||
ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification),
|
||||
#[experimental("thread/goal/cleared")]
|
||||
ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification),
|
||||
#[experimental("thread/queue/changed")]
|
||||
ThreadQueueChanged => "thread/queue/changed" (v2::ThreadQueueChangedNotification),
|
||||
ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification),
|
||||
TurnStarted => "turn/started" (v2::TurnStartedNotification),
|
||||
HookStarted => "hook/started" (v2::HookStartedNotification),
|
||||
|
||||
@@ -8,7 +8,9 @@ use super::ThreadItem;
|
||||
use super::ThreadSource;
|
||||
use super::Turn;
|
||||
use super::TurnEnvironmentParams;
|
||||
use super::TurnError;
|
||||
use super::TurnItemsView;
|
||||
use super::TurnStartParams;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -19,6 +21,8 @@ use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::Schema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -659,6 +663,109 @@ pub struct ThreadGoalClearResponse {
|
||||
pub cleared: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", export_to = "v2/")]
|
||||
pub enum QueuedTurnStatus {
|
||||
Pending,
|
||||
Failed { error: TurnError },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct QueuedTurn {
|
||||
pub id: String,
|
||||
#[schemars(with = "QueuedTurnStartParamsSchema")]
|
||||
pub turn_start_params: TurnStartParams,
|
||||
pub status: QueuedTurnStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueAddParams {
|
||||
pub thread_id: String,
|
||||
#[schemars(with = "QueuedTurnStartParamsSchema")]
|
||||
pub turn_start_params: TurnStartParams,
|
||||
}
|
||||
|
||||
struct QueuedTurnStartParamsSchema;
|
||||
|
||||
impl JsonSchema for QueuedTurnStartParamsSchema {
|
||||
fn schema_name() -> String {
|
||||
"QueuedTurnStartParams".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
TurnStartParams::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueAddResponse {
|
||||
pub queued_turn: QueuedTurn,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueListParams {
|
||||
pub thread_id: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueListResponse {
|
||||
pub data: Vec<QueuedTurn>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueDeleteParams {
|
||||
pub thread_id: String,
|
||||
pub queued_turn_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueDeleteResponse {
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueReorderParams {
|
||||
pub thread_id: String,
|
||||
pub queued_turn_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueReorderResponse {
|
||||
pub queued_turns: Vec<QueuedTurn>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadQueueChangedNotification {
|
||||
pub thread_id: String,
|
||||
pub queued_turns: Vec<QueuedTurn>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -147,6 +147,11 @@ Example with notification opt-out:
|
||||
- `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes.
|
||||
- `thread/goal/updated` — notification emitted whenever a thread goal changes; includes the full current goal.
|
||||
- `thread/goal/cleared` — notification emitted whenever a thread goal is removed.
|
||||
- `thread/queue/add` — experimental; persist a future `turn/start` request for a loaded thread. The queue stores the original `TurnStartParams`, dispatches the oldest pending row once the thread is idle unless an older failed row blocks FIFO order, and emits `thread/queue/changed`.
|
||||
- `thread/queue/list` — experimental; page through the visible queued turns for a thread with cursor/limit pagination. Pending and failed rows are visible; the short-lived dispatch claim is internal.
|
||||
- `thread/queue/delete` — experimental; remove a visible queued turn by id.
|
||||
- `thread/queue/reorder` — experimental; replace the visible queue order by queued-turn id.
|
||||
- `thread/queue/changed` — experimental notification emitted after visible queue state changes, including restart recovery that surfaces an interrupted dispatch as failed.
|
||||
- `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`).
|
||||
- `thread/archive` — move a thread’s rollout file into the archived directory and attempt to move any spawned descendant thread rollout files; returns `{}` on success and emits `thread/archived` for each archived thread.
|
||||
- `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server keeps the thread loaded and unloads it only after it has had no subscribers and no thread activity for 30 minutes, then emits `thread/closed`.
|
||||
@@ -558,6 +563,34 @@ Use `thread/goal/clear` to remove the current goal.
|
||||
{ "method": "thread/goal/cleared", "params": { "threadId": "thr_123" } }
|
||||
```
|
||||
|
||||
### Example: Queue a follow-up turn
|
||||
|
||||
Experimental clients can store a later turn with `thread/queue/add`. The queue item keeps the same `TurnStartParams` object a future `turn/start` would use, then app-server starts it when the thread becomes idle.
|
||||
|
||||
```json
|
||||
{ "method": "thread/queue/add", "id": 31, "params": {
|
||||
"threadId": "thr_123",
|
||||
"turnStartParams": {
|
||||
"threadId": "thr_123",
|
||||
"input": [{ "type": "text", "text": "Run the follow-up benchmark" }]
|
||||
}
|
||||
} }
|
||||
{ "id": 31, "result": { "queuedTurn": {
|
||||
"id": "queued_123",
|
||||
"turnStartParams": {
|
||||
"threadId": "thr_123",
|
||||
"input": [{ "type": "text", "text": "Run the follow-up benchmark" }]
|
||||
},
|
||||
"status": { "type": "pending" }
|
||||
} } }
|
||||
{ "method": "thread/queue/changed", "params": {
|
||||
"threadId": "thr_123",
|
||||
"queuedTurns": []
|
||||
} }
|
||||
```
|
||||
|
||||
If dispatch cannot hand the row to a real turn, the row stays visible with `status.type: "failed"`. Delete or reorder visible queue rows with `thread/queue/delete` and `thread/queue/reorder`; list order is authoritative for rendering.
|
||||
|
||||
### Example: Archive a thread
|
||||
|
||||
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory and attempt to move any spawned descendant thread rollouts.
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::request_processors::ProcessExecRequestProcessor;
|
||||
use crate::request_processors::RemoteControlRequestProcessor;
|
||||
use crate::request_processors::SearchRequestProcessor;
|
||||
use crate::request_processors::ThreadGoalRequestProcessor;
|
||||
use crate::request_processors::ThreadQueueRequestProcessor;
|
||||
use crate::request_processors::ThreadRequestProcessor;
|
||||
use crate::request_processors::TurnRequestProcessor;
|
||||
use crate::request_processors::WindowsSandboxRequestProcessor;
|
||||
@@ -177,6 +178,7 @@ pub(crate) struct MessageProcessor {
|
||||
remote_control_processor: RemoteControlRequestProcessor,
|
||||
search_processor: SearchRequestProcessor,
|
||||
thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
thread_queue_processor: ThreadQueueRequestProcessor,
|
||||
thread_processor: ThreadRequestProcessor,
|
||||
turn_processor: TurnRequestProcessor,
|
||||
windows_sandbox_processor: WindowsSandboxRequestProcessor,
|
||||
@@ -406,6 +408,28 @@ impl MessageProcessor {
|
||||
thread_state_manager.clone(),
|
||||
state_db.clone(),
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
outgoing.clone(),
|
||||
analytics_events_client.clone(),
|
||||
arg0_paths.clone(),
|
||||
Arc::clone(&config),
|
||||
config_manager.clone(),
|
||||
Arc::clone(&pending_thread_unloads),
|
||||
thread_state_manager.clone(),
|
||||
state_db.clone(),
|
||||
thread_watch_manager.clone(),
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
Arc::clone(&skills_watcher),
|
||||
);
|
||||
let thread_queue_processor = ThreadQueueRequestProcessor::new(
|
||||
Arc::clone(&thread_manager),
|
||||
outgoing.clone(),
|
||||
state_db.clone(),
|
||||
thread_state_manager.clone(),
|
||||
turn_processor.clone(),
|
||||
);
|
||||
let thread_processor = ThreadRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
@@ -415,25 +439,12 @@ impl MessageProcessor {
|
||||
config_manager.clone(),
|
||||
Arc::clone(&thread_store),
|
||||
Arc::clone(&pending_thread_unloads),
|
||||
thread_state_manager.clone(),
|
||||
thread_watch_manager.clone(),
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
thread_goal_processor.clone(),
|
||||
state_db,
|
||||
Arc::clone(&skills_watcher),
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
outgoing.clone(),
|
||||
analytics_events_client.clone(),
|
||||
arg0_paths.clone(),
|
||||
Arc::clone(&config),
|
||||
config_manager.clone(),
|
||||
pending_thread_unloads,
|
||||
thread_state_manager,
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
thread_goal_processor.clone(),
|
||||
thread_queue_processor.clone(),
|
||||
state_db,
|
||||
Arc::clone(&skills_watcher),
|
||||
);
|
||||
if matches!(plugin_startup_tasks, crate::PluginStartupTasks::Start) {
|
||||
@@ -495,6 +506,7 @@ impl MessageProcessor {
|
||||
remote_control_processor,
|
||||
search_processor,
|
||||
thread_goal_processor,
|
||||
thread_queue_processor,
|
||||
thread_processor,
|
||||
turn_processor,
|
||||
windows_sandbox_processor,
|
||||
@@ -1030,6 +1042,24 @@ impl MessageProcessor {
|
||||
.thread_goal_clear(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadQueueAdd { params, .. } => {
|
||||
self.thread_queue_processor
|
||||
.thread_queue_add(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadQueueList { params, .. } => {
|
||||
self.thread_queue_processor.thread_queue_list(params).await
|
||||
}
|
||||
ClientRequest::ThreadQueueDelete { params, .. } => {
|
||||
self.thread_queue_processor
|
||||
.thread_queue_delete(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadQueueReorder { params, .. } => {
|
||||
self.thread_queue_processor
|
||||
.thread_queue_reorder(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadMetadataUpdate { params, .. } => {
|
||||
self.thread_processor.thread_metadata_update(params).await
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ use codex_app_server_protocol::PluginSource;
|
||||
use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::PluginUninstallParams;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_app_server_protocol::QueuedTurn;
|
||||
use codex_app_server_protocol::QueuedTurnStatus;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
@@ -196,6 +198,15 @@ use codex_app_server_protocol::ThreadMetadataGitInfoUpdateParams;
|
||||
use codex_app_server_protocol::ThreadMetadataUpdateParams;
|
||||
use codex_app_server_protocol::ThreadMetadataUpdateResponse;
|
||||
use codex_app_server_protocol::ThreadNameUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadQueueAddParams;
|
||||
use codex_app_server_protocol::ThreadQueueAddResponse;
|
||||
use codex_app_server_protocol::ThreadQueueChangedNotification;
|
||||
use codex_app_server_protocol::ThreadQueueDeleteParams;
|
||||
use codex_app_server_protocol::ThreadQueueDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadQueueListParams;
|
||||
use codex_app_server_protocol::ThreadQueueListResponse;
|
||||
use codex_app_server_protocol::ThreadQueueReorderParams;
|
||||
use codex_app_server_protocol::ThreadQueueReorderResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadRealtimeAppendAudioParams;
|
||||
@@ -494,6 +505,7 @@ mod config_errors;
|
||||
mod request_errors;
|
||||
mod thread_goal_processor;
|
||||
mod thread_lifecycle;
|
||||
mod thread_queue_processor;
|
||||
mod thread_resume_redaction;
|
||||
mod thread_summary;
|
||||
|
||||
@@ -506,6 +518,7 @@ use self::thread_summary::*;
|
||||
|
||||
pub(crate) use self::thread_lifecycle::populate_thread_turns_from_history;
|
||||
pub(crate) use self::thread_processor::thread_from_stored_thread;
|
||||
pub(crate) use self::thread_queue_processor::ThreadQueueRequestProcessor;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::read_summary_from_rollout;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(super) struct ListenerTaskContext {
|
||||
pub(super) fallback_model_provider: String,
|
||||
pub(super) codex_home: PathBuf,
|
||||
pub(super) skills_watcher: Arc<SkillsWatcher>,
|
||||
pub(super) thread_queue_processor: ThreadQueueRequestProcessor,
|
||||
}
|
||||
|
||||
struct UnloadingState {
|
||||
@@ -253,6 +254,7 @@ pub(super) async fn ensure_listener_task_running(
|
||||
thread_list_state_permit,
|
||||
fallback_model_provider,
|
||||
codex_home,
|
||||
thread_queue_processor,
|
||||
..
|
||||
} = listener_task_context;
|
||||
let outgoing_for_task = Arc::clone(&outgoing);
|
||||
@@ -277,6 +279,7 @@ pub(super) async fn ensure_listener_task_running(
|
||||
&thread_watch_manager,
|
||||
&outgoing_for_task,
|
||||
&pending_thread_unloads,
|
||||
&thread_queue_processor,
|
||||
listener_command,
|
||||
)
|
||||
.await;
|
||||
@@ -293,10 +296,14 @@ pub(super) async fn ensure_listener_task_running(
|
||||
// Track the event before emitting any typed translations
|
||||
// so thread-local state such as raw event opt-in stays
|
||||
// synchronized with the conversation.
|
||||
let raw_events_enabled = {
|
||||
let (raw_events_enabled, pending_turn_starts_cleared) = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.track_current_turn_event(&event.id, &event.msg);
|
||||
thread_state.experimental_raw_events
|
||||
let pending_turn_starts_cleared =
|
||||
thread_state.track_current_turn_event(&event.id, &event.msg);
|
||||
(
|
||||
thread_state.experimental_raw_events,
|
||||
pending_turn_starts_cleared,
|
||||
)
|
||||
};
|
||||
let subscribed_connection_ids = thread_state_manager
|
||||
.subscribed_connection_ids(conversation_id)
|
||||
@@ -332,6 +339,29 @@ pub(super) async fn ensure_listener_task_running(
|
||||
fallback_model_provider.clone(),
|
||||
)
|
||||
.await;
|
||||
if let EventMsg::TurnStarted(payload) = &event.msg {
|
||||
thread_queue_processor
|
||||
.complete_dispatch_after_turn_started(
|
||||
conversation_id,
|
||||
payload.turn_id.as_str(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if matches!(
|
||||
&event.msg,
|
||||
EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_)
|
||||
) {
|
||||
thread_queue_processor
|
||||
.drain_thread_queue_after_terminal_turn(conversation_id)
|
||||
.await;
|
||||
}
|
||||
if pending_turn_starts_cleared
|
||||
&& matches!(&event.msg, EventMsg::Error(_))
|
||||
{
|
||||
thread_queue_processor
|
||||
.drain_thread_queue_after_terminal_turn(conversation_id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
unloading_watchers_open = unloading_state.wait_for_unloading_trigger() => {
|
||||
if !unloading_watchers_open {
|
||||
@@ -447,6 +477,7 @@ pub(super) async fn handle_thread_listener_command(
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
pending_thread_unloads: &Arc<Mutex<HashSet<ThreadId>>>,
|
||||
thread_queue_processor: &ThreadQueueRequestProcessor,
|
||||
listener_command: ThreadListenerCommand,
|
||||
) {
|
||||
match listener_command {
|
||||
@@ -460,6 +491,7 @@ pub(super) async fn handle_thread_listener_command(
|
||||
thread_watch_manager,
|
||||
outgoing,
|
||||
pending_thread_unloads,
|
||||
thread_queue_processor,
|
||||
*resume_request,
|
||||
)
|
||||
.await;
|
||||
@@ -517,6 +549,7 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
pending_thread_unloads: &Arc<Mutex<HashSet<ThreadId>>>,
|
||||
thread_queue_processor: &ThreadQueueRequestProcessor,
|
||||
pending: crate::thread_state::PendingThreadResumeRequest,
|
||||
) {
|
||||
let active_turn = {
|
||||
@@ -669,6 +702,9 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
{
|
||||
tracing::warn!("failed to continue active goal after running-thread resume: {err}");
|
||||
}
|
||||
thread_queue_processor
|
||||
.emit_resume_queue_snapshot_and_drain(conversation_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn send_thread_goal_snapshot_notification(
|
||||
|
||||
@@ -336,6 +336,7 @@ pub(crate) struct ThreadRequestProcessor {
|
||||
pub(super) thread_watch_manager: ThreadWatchManager,
|
||||
pub(super) thread_list_state_permit: Arc<Semaphore>,
|
||||
pub(super) thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
pub(super) thread_queue_processor: ThreadQueueRequestProcessor,
|
||||
pub(super) state_db: Option<StateDbHandle>,
|
||||
pub(super) background_tasks: TaskTracker,
|
||||
pub(super) skills_watcher: Arc<SkillsWatcher>,
|
||||
@@ -356,6 +357,7 @@ impl ThreadRequestProcessor {
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
thread_queue_processor: ThreadQueueRequestProcessor,
|
||||
state_db: Option<StateDbHandle>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
) -> Self {
|
||||
@@ -372,6 +374,7 @@ impl ThreadRequestProcessor {
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
thread_goal_processor,
|
||||
thread_queue_processor,
|
||||
state_db,
|
||||
background_tasks: TaskTracker::new(),
|
||||
skills_watcher,
|
||||
@@ -777,6 +780,7 @@ impl ThreadRequestProcessor {
|
||||
fallback_model_provider: self.config.model_provider_id.clone(),
|
||||
codex_home: self.config.codex_home.to_path_buf(),
|
||||
skills_watcher: Arc::clone(&self.skills_watcher),
|
||||
thread_queue_processor: self.thread_queue_processor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,6 +881,7 @@ impl ThreadRequestProcessor {
|
||||
fallback_model_provider: self.config.model_provider_id.clone(),
|
||||
codex_home: self.config.codex_home.to_path_buf(),
|
||||
skills_watcher: Arc::clone(&self.skills_watcher),
|
||||
thread_queue_processor: self.thread_queue_processor.clone(),
|
||||
};
|
||||
let request_trace = request_context.request_trace();
|
||||
let config_manager = self.config_manager.clone();
|
||||
@@ -2591,6 +2596,9 @@ impl ThreadRequestProcessor {
|
||||
self.thread_goal_processor
|
||||
.emit_resume_goal_snapshot_and_continue(thread_id, codex_thread.as_ref())
|
||||
.await;
|
||||
self.thread_queue_processor
|
||||
.recover_resume_queue_snapshot_and_drain(thread_id)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
let error = internal_error(format!("error resuming thread: {err}"));
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
use super::*;
|
||||
|
||||
const THREAD_QUEUE_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_QUEUE_LIST_MAX_LIMIT: usize = 100;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadQueueRequestProcessor {
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
turn_processor: TurnRequestProcessor,
|
||||
}
|
||||
|
||||
impl ThreadQueueRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
turn_processor: TurnRequestProcessor,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_manager,
|
||||
outgoing,
|
||||
state_db,
|
||||
thread_state_manager,
|
||||
turn_processor,
|
||||
}
|
||||
}
|
||||
|
||||
fn state_db(&self) -> Result<&StateDbHandle, JSONRPCErrorError> {
|
||||
self.state_db
|
||||
.as_ref()
|
||||
.ok_or_else(|| internal_error("queued turns require the app-server state db"))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_queue_add(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadQueueAddParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let thread_id = parse_queue_thread_id(params.thread_id.as_str())?;
|
||||
if params.thread_id != params.turn_start_params.thread_id {
|
||||
return Err(invalid_request(
|
||||
"`threadId` must match `turnStartParams.threadId`",
|
||||
));
|
||||
}
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
if thread.config_snapshot().await.ephemeral {
|
||||
return Err(invalid_request(format!(
|
||||
"ephemeral thread does not support queued turns: {thread_id}"
|
||||
)));
|
||||
}
|
||||
TurnRequestProcessor::validate_v2_input_limit(¶ms.turn_start_params.input)?;
|
||||
let payload = serde_json::to_vec(¶ms.turn_start_params).map_err(|err| {
|
||||
internal_error(format!("failed to serialize queued turn payload: {err}"))
|
||||
})?;
|
||||
let record = self
|
||||
.state_db()?
|
||||
.append_thread_queued_turn(thread_id, payload.as_slice())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to add queued turn: {err}")))?;
|
||||
let queued_turn = queued_turn_from_state(record)?;
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
ThreadQueueAddResponse {
|
||||
queued_turn: queued_turn.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
self.emit_thread_queue_changed(thread_id).await;
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_queue_list(
|
||||
&self,
|
||||
params: ThreadQueueListParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let thread_id = parse_queue_thread_id(params.thread_id.as_str())?;
|
||||
let start = match params.cursor {
|
||||
Some(cursor) => cursor
|
||||
.parse::<usize>()
|
||||
.map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))?,
|
||||
None => 0,
|
||||
};
|
||||
let limit = params
|
||||
.limit
|
||||
.unwrap_or(THREAD_QUEUE_LIST_DEFAULT_LIMIT as u32)
|
||||
.clamp(1, THREAD_QUEUE_LIST_MAX_LIMIT as u32) as usize;
|
||||
let records = self
|
||||
.state_db()?
|
||||
.list_visible_thread_queued_turns_page(thread_id, start, limit.saturating_add(1))
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to read queued turns: {err}")))?;
|
||||
let has_next_page = records.len() > limit;
|
||||
let data = records
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(queued_turn_from_state)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let next_cursor = has_next_page.then(|| start.saturating_add(limit).to_string());
|
||||
Ok(Some(ThreadQueueListResponse { data, next_cursor }.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_queue_delete(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadQueueDeleteParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let thread_id = parse_queue_thread_id(params.thread_id.as_str())?;
|
||||
let deleted = self
|
||||
.state_db()?
|
||||
.delete_thread_queued_turn(thread_id, params.queued_turn_id.as_str())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to delete queued turn: {err}")))?;
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadQueueDeleteResponse { deleted })
|
||||
.await;
|
||||
if deleted {
|
||||
self.emit_thread_queue_changed(thread_id).await;
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_queue_reorder(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadQueueReorderParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let thread_id = parse_queue_thread_id(params.thread_id.as_str())?;
|
||||
let records = self
|
||||
.state_db()?
|
||||
.reorder_thread_queued_turns(thread_id, params.queued_turn_ids.as_slice())
|
||||
.await
|
||||
.map_err(|err| invalid_request(format!("failed to reorder queued turns: {err}")))?;
|
||||
let queued_turns = records
|
||||
.into_iter()
|
||||
.map(queued_turn_from_state)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
ThreadQueueReorderResponse {
|
||||
queued_turns: queued_turns.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
self.send_thread_queue_changed(thread_id, queued_turns)
|
||||
.await;
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn recover_resume_queue_snapshot_and_drain(&self, thread_id: ThreadId) {
|
||||
let Some(state_db) = self.state_db.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let failure = turn_error("queued turn dispatch was interrupted while app-server restarted");
|
||||
let failure_json = match serde_json::to_vec(&failure) {
|
||||
Ok(failure_json) => failure_json,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to serialize queued turn recovery failure: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match state_db
|
||||
.recover_dispatching_thread_queued_turns(thread_id, failure_json.as_slice())
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to recover queued turns for thread {thread_id}: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.emit_thread_queue_changed(thread_id).await;
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn emit_resume_queue_snapshot_and_drain(&self, thread_id: ThreadId) {
|
||||
self.emit_thread_queue_changed(thread_id).await;
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn drain_thread_queue_after_terminal_turn(&self, thread_id: ThreadId) {
|
||||
self.drain_thread_queue_if_idle(thread_id).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn complete_dispatch_after_turn_started(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
turn_id: &str,
|
||||
) {
|
||||
let Some(state_db) = self.state_db.as_ref() else {
|
||||
return;
|
||||
};
|
||||
match state_db
|
||||
.remove_dispatching_thread_queued_turn(thread_id, turn_id)
|
||||
.await
|
||||
{
|
||||
Ok(true) | Ok(false) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to clear queued dispatch claim for thread {thread_id}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn drain_thread_queue_if_idle(&self, thread_id: ThreadId) {
|
||||
let Some(state_db) = self.state_db.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
|
||||
return;
|
||||
};
|
||||
if matches!(thread.agent_status().await, AgentStatus::Running) {
|
||||
return;
|
||||
}
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
{
|
||||
let thread_state = thread_state.lock().await;
|
||||
if thread_state.active_turn_snapshot().is_some()
|
||||
|| !matches!(
|
||||
thread_state.pending_turn_starts,
|
||||
crate::thread_state::PendingTurnStarts::None
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
let record = match state_db.claim_head_thread_queued_turn(thread_id).await {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => return,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to claim queued turn for thread {thread_id}: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.emit_thread_queue_changed(thread_id).await;
|
||||
let params = match serde_json::from_slice::<TurnStartParams>(
|
||||
record.turn_start_params_jsonb.as_slice(),
|
||||
) {
|
||||
Ok(params) => params,
|
||||
Err(err) => {
|
||||
self.fail_dispatch(
|
||||
thread_id,
|
||||
record.queued_turn_id.as_str(),
|
||||
turn_error(format!("queued turn payload could not be read: {err}")),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
match self.turn_processor.queued_turn_start(params).await {
|
||||
Ok(response) => {
|
||||
let turn_id = response.turn.id;
|
||||
match state_db
|
||||
.set_dispatching_thread_queued_turn_turn_id(
|
||||
record.queued_turn_id.as_str(),
|
||||
turn_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
tracing::warn!(
|
||||
"queued turn {} lost its dispatch claim before turn {turn_id} was recorded",
|
||||
record.queued_turn_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"failed to record dispatch turn {turn_id} for queued turn {}: {err}",
|
||||
record.queued_turn_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
let should_complete_dispatch = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
thread_state
|
||||
.active_turn_snapshot()
|
||||
.is_some_and(|turn| turn.id == turn_id)
|
||||
|| thread_state.last_terminal_turn_id.as_deref() == Some(turn_id.as_str())
|
||||
};
|
||||
if should_complete_dispatch {
|
||||
self.complete_dispatch_after_turn_started(thread_id, turn_id.as_str())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.fail_dispatch(
|
||||
thread_id,
|
||||
record.queued_turn_id.as_str(),
|
||||
turn_error(format!(
|
||||
"queued turn could not start: {message}",
|
||||
message = err.message
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fail_dispatch(&self, thread_id: ThreadId, queued_turn_id: &str, error: TurnError) {
|
||||
let Some(state_db) = self.state_db.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let failure_json = match serde_json::to_vec(&error) {
|
||||
Ok(failure_json) => failure_json,
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to serialize queued turn failure: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match state_db
|
||||
.mark_thread_queued_turn_failed(queued_turn_id, failure_json.as_slice())
|
||||
.await
|
||||
{
|
||||
Ok(true) => self.emit_thread_queue_changed(thread_id).await,
|
||||
Ok(false) => tracing::warn!(
|
||||
"queued turn {queued_turn_id} could not be marked failed because its dispatch claim disappeared"
|
||||
),
|
||||
Err(err) => tracing::warn!("failed to mark queued turn {queued_turn_id} failed: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn emit_thread_queue_changed(&self, thread_id: ThreadId) {
|
||||
match self.list_visible_queued_turns(thread_id).await {
|
||||
Ok(queued_turns) => {
|
||||
self.send_thread_queue_changed(thread_id, queued_turns)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to read queue snapshot for thread {thread_id}: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_thread_queue_changed(&self, thread_id: ThreadId, queued_turns: Vec<QueuedTurn>) {
|
||||
let subscribed_connection_ids = self
|
||||
.thread_state_manager
|
||||
.subscribed_connection_ids(thread_id)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_server_notification_to_connections(
|
||||
subscribed_connection_ids.as_slice(),
|
||||
ServerNotification::ThreadQueueChanged(ThreadQueueChangedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
queued_turns,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn list_visible_queued_turns(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<Vec<QueuedTurn>, JSONRPCErrorError> {
|
||||
self.state_db()?
|
||||
.list_visible_thread_queued_turns(thread_id)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to read queued turns: {err}")))?
|
||||
.into_iter()
|
||||
.map(queued_turn_from_state)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_queue_thread_id(thread_id: &str) -> Result<ThreadId, JSONRPCErrorError> {
|
||||
ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))
|
||||
}
|
||||
|
||||
fn queued_turn_from_state(
|
||||
record: codex_state::ThreadQueuedTurn,
|
||||
) -> Result<QueuedTurn, JSONRPCErrorError> {
|
||||
let turn_start_params = serde_json::from_slice(record.turn_start_params_jsonb.as_slice())
|
||||
.map_err(|err| internal_error(format!("failed to read queued turn payload: {err}")))?;
|
||||
let status = match record.state {
|
||||
codex_state::ThreadQueuedTurnState::Pending => QueuedTurnStatus::Pending,
|
||||
codex_state::ThreadQueuedTurnState::Failed => {
|
||||
let error = record
|
||||
.failure_jsonb
|
||||
.as_deref()
|
||||
.map(serde_json::from_slice)
|
||||
.transpose()
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to read queued turn failure: {err}"))
|
||||
})?
|
||||
.unwrap_or_else(|| turn_error("queued turn dispatch failed"));
|
||||
QueuedTurnStatus::Failed { error }
|
||||
}
|
||||
codex_state::ThreadQueuedTurnState::Dispatching => {
|
||||
return Err(internal_error(
|
||||
"dispatching queued turns are not client-visible",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(QueuedTurn {
|
||||
id: record.queued_turn_id,
|
||||
turn_start_params,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
fn turn_error(message: impl Into<String>) -> TurnError {
|
||||
TurnError {
|
||||
message: message.into(),
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub(crate) struct TurnRequestProcessor {
|
||||
config_manager: ConfigManager,
|
||||
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
@@ -42,6 +43,7 @@ impl TurnRequestProcessor {
|
||||
config_manager: ConfigManager,
|
||||
pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
thread_list_state_permit: Arc<Semaphore>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
@@ -56,6 +58,7 @@ impl TurnRequestProcessor {
|
||||
config_manager,
|
||||
pending_thread_unloads,
|
||||
thread_state_manager,
|
||||
state_db,
|
||||
thread_watch_manager,
|
||||
thread_list_state_permit,
|
||||
skills_watcher,
|
||||
@@ -79,6 +82,16 @@ impl TurnRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn queued_turn_start(
|
||||
&self,
|
||||
params: TurnStartParams,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
Self::validate_v2_input_limit(¶ms.input)?;
|
||||
let (thread_id, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
self.start_turn_from_params(thread_id, thread, params, /*trace_context*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_inject_items(
|
||||
&self,
|
||||
params: ThreadInjectItemsParams,
|
||||
@@ -318,7 +331,7 @@ impl TurnRequestProcessor {
|
||||
error
|
||||
}
|
||||
|
||||
fn validate_v2_input_limit(items: &[V2UserInput]) -> Result<(), JSONRPCErrorError> {
|
||||
pub(crate) fn validate_v2_input_limit(items: &[V2UserInput]) -> Result<(), JSONRPCErrorError> {
|
||||
let actual_chars: usize = items.iter().map(V2UserInput::text_char_count).sum();
|
||||
if actual_chars > MAX_USER_INPUT_TEXT_CHARS {
|
||||
return Err(Self::input_too_large_error(actual_chars));
|
||||
@@ -356,7 +369,48 @@ impl TurnRequestProcessor {
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
let response = self
|
||||
.start_turn_from_params(
|
||||
thread_id,
|
||||
thread,
|
||||
params,
|
||||
self.request_trace_context(&request_id).await,
|
||||
)
|
||||
.await?;
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_id).await;
|
||||
{
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
let turn_lifecycle_already_visible = thread_state
|
||||
.active_turn_snapshot()
|
||||
.is_some_and(|turn| turn.id == response.turn.id)
|
||||
|| thread_state.last_terminal_turn_id.as_deref() == Some(response.turn.id.as_str());
|
||||
if !turn_lifecycle_already_visible {
|
||||
match &mut thread_state.pending_turn_starts {
|
||||
crate::thread_state::PendingTurnStarts::None => {
|
||||
thread_state.pending_turn_starts =
|
||||
crate::thread_state::PendingTurnStarts::WaitingForLifecycle {
|
||||
turn_ids: HashSet::from([response.turn.id.clone()]),
|
||||
};
|
||||
}
|
||||
crate::thread_state::PendingTurnStarts::WaitingForLifecycle { turn_ids } => {
|
||||
turn_ids.insert(response.turn.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &response.turn.id)
|
||||
.await;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn start_turn_from_params(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
thread: Arc<CodexThread>,
|
||||
params: TurnStartParams,
|
||||
trace_context: Option<W3cTraceContext>,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
let collaboration_mode = params
|
||||
.collaboration_mode
|
||||
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
|
||||
@@ -528,14 +582,10 @@ impl TurnRequestProcessor {
|
||||
responsesapi_client_metadata: params.responsesapi_client_metadata,
|
||||
thread_settings,
|
||||
};
|
||||
let turn_id = self
|
||||
.submit_core_op(&request_id, thread.as_ref(), turn_op)
|
||||
let turn_id = thread
|
||||
.submit_with_trace(turn_op, trace_context)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let error = internal_error(format!("failed to start turn: {err}"));
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
error
|
||||
})?;
|
||||
.map_err(|err| internal_error(format!("failed to start turn: {err}")))?;
|
||||
|
||||
if turn_has_input {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
@@ -549,9 +599,6 @@ impl TurnRequestProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &turn_id)
|
||||
.await;
|
||||
let turn = Turn {
|
||||
id: turn_id,
|
||||
items: vec![],
|
||||
@@ -1141,6 +1188,13 @@ impl TurnRequestProcessor {
|
||||
fallback_model_provider: self.config.model_provider_id.clone(),
|
||||
codex_home: self.config.codex_home.to_path_buf(),
|
||||
skills_watcher: Arc::clone(&self.skills_watcher),
|
||||
thread_queue_processor: ThreadQueueRequestProcessor::new(
|
||||
Arc::clone(&self.thread_manager),
|
||||
Arc::clone(&self.outgoing),
|
||||
self.state_db.clone(),
|
||||
self.thread_state_manager.clone(),
|
||||
self.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,15 @@ use tracing::error;
|
||||
|
||||
type PendingInterruptQueue = Vec<ConnectionRequestId>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) enum PendingTurnStarts {
|
||||
#[default]
|
||||
None,
|
||||
WaitingForLifecycle {
|
||||
turn_ids: HashSet<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) struct PendingThreadResumeRequest {
|
||||
pub(crate) request_id: ConnectionRequestId,
|
||||
pub(crate) history_items: Vec<RolloutItem>,
|
||||
@@ -71,6 +80,7 @@ pub(crate) struct TurnSummary {
|
||||
pub(crate) struct ThreadState {
|
||||
pub(crate) pending_interrupts: PendingInterruptQueue,
|
||||
pub(crate) pending_rollbacks: Option<ConnectionRequestId>,
|
||||
pub(crate) pending_turn_starts: PendingTurnStarts,
|
||||
pub(crate) turn_summary: TurnSummary,
|
||||
pub(crate) last_terminal_turn_id: Option<String>,
|
||||
pub(crate) cancel_tx: Option<oneshot::Sender<()>>,
|
||||
@@ -112,6 +122,7 @@ impl ThreadState {
|
||||
let _ = cancel_tx.send(());
|
||||
}
|
||||
self.listener_command_tx = None;
|
||||
self.pending_turn_starts = PendingTurnStarts::None;
|
||||
self.current_turn_history.reset();
|
||||
self.listener_thread = None;
|
||||
self.watch_registration = WatchRegistration::default();
|
||||
@@ -131,17 +142,38 @@ impl ThreadState {
|
||||
self.current_turn_history.active_turn_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn track_current_turn_event(&mut self, event_turn_id: &str, event: &EventMsg) {
|
||||
pub(crate) fn track_current_turn_event(
|
||||
&mut self,
|
||||
event_turn_id: &str,
|
||||
event: &EventMsg,
|
||||
) -> bool {
|
||||
if let EventMsg::TurnStarted(payload) = event {
|
||||
self.turn_summary.started_at = payload.started_at;
|
||||
}
|
||||
self.current_turn_history.handle_event(event);
|
||||
let pending_turn_start_resolved = matches!(
|
||||
event,
|
||||
EventMsg::TurnStarted(_)
|
||||
| EventMsg::TurnAborted(_)
|
||||
| EventMsg::TurnComplete(_)
|
||||
| EventMsg::Error(_)
|
||||
);
|
||||
let pending_turn_starts_cleared = match &mut self.pending_turn_starts {
|
||||
PendingTurnStarts::None => false,
|
||||
PendingTurnStarts::WaitingForLifecycle { turn_ids } => {
|
||||
pending_turn_start_resolved && turn_ids.remove(event_turn_id) && turn_ids.is_empty()
|
||||
}
|
||||
};
|
||||
if pending_turn_starts_cleared {
|
||||
self.pending_turn_starts = PendingTurnStarts::None;
|
||||
}
|
||||
if matches!(event, EventMsg::TurnAborted(_) | EventMsg::TurnComplete(_))
|
||||
&& !self.current_turn_history.has_active_turn()
|
||||
{
|
||||
self.last_terminal_turn_id = Some(event_turn_id.to_string());
|
||||
self.current_turn_history.reset();
|
||||
}
|
||||
pending_turn_starts_cleared
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ mod thread_loaded_list;
|
||||
mod thread_memory_mode_set;
|
||||
mod thread_metadata_update;
|
||||
mod thread_name_websocket;
|
||||
mod thread_queue;
|
||||
mod thread_read;
|
||||
mod thread_resume;
|
||||
mod thread_rollback;
|
||||
|
||||
645
codex-rs/app-server/tests/suite/v2/thread_queue.rs
Normal file
645
codex-rs/app-server/tests/suite/v2/thread_queue.rs
Normal file
@@ -0,0 +1,645 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::QueuedTurnStatus;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadQueueAddParams;
|
||||
use codex_app_server_protocol::ThreadQueueAddResponse;
|
||||
use codex_app_server_protocol::ThreadQueueChangedNotification;
|
||||
use codex_app_server_protocol::ThreadQueueDeleteParams;
|
||||
use codex_app_server_protocol::ThreadQueueDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadQueueListParams;
|
||||
use codex_app_server_protocol::ThreadQueueListResponse;
|
||||
use codex_app_server_protocol::ThreadQueueReorderParams;
|
||||
use codex_app_server_protocol::ThreadQueueReorderResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
|
||||
#[tokio::test]
|
||||
async fn idle_queue_add_dispatches_serialized_turn_and_drains_visible_queue() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("queued done")?,
|
||||
create_final_assistant_message_sse_response("second queued done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let thread = start_thread(&mut mcp).await?;
|
||||
|
||||
let add_request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/add",
|
||||
Some(serde_json::to_value(ThreadQueueAddParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_start_params: text_turn(&thread.id, "queued serialized input"),
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let add_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(add_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadQueueAddResponse { queued_turn } = to_response(add_response)?;
|
||||
assert!(matches!(queued_turn.status, QueuedTurnStatus::Pending));
|
||||
let add_notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/queue/changed"),
|
||||
)
|
||||
.await??;
|
||||
let add_notification: ThreadQueueChangedNotification = serde_json::from_value(
|
||||
add_notification
|
||||
.params
|
||||
.expect("thread/queue/changed params"),
|
||||
)?;
|
||||
assert_eq!(add_notification.thread_id, thread.id);
|
||||
assert_eq!(add_notification.queued_turns, vec![queued_turn]);
|
||||
|
||||
let drain_notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/queue/changed"),
|
||||
)
|
||||
.await??;
|
||||
let drain_notification: ThreadQueueChangedNotification = serde_json::from_value(
|
||||
drain_notification
|
||||
.params
|
||||
.expect("thread/queue/changed params"),
|
||||
)?;
|
||||
assert_eq!(drain_notification.thread_id, thread.id);
|
||||
assert!(drain_notification.queued_turns.is_empty());
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let list_request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/list",
|
||||
Some(serde_json::to_value(ThreadQueueListParams {
|
||||
thread_id: thread.id.clone(),
|
||||
cursor: None,
|
||||
limit: None,
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let list_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(list_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadQueueListResponse { data, next_cursor } = to_response(list_response)?;
|
||||
assert!(data.is_empty());
|
||||
assert_eq!(next_cursor, None);
|
||||
|
||||
queue_turn(&mut mcp, &thread.id, "second queued serialized input").await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert!(list_queue_ids(&mut mcp, &thread.id).await?.is_empty());
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&requests[0].body).contains("queued serialized input"),
|
||||
"queued turn payload should reach the model request after state round-trip"
|
||||
);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&requests[1].body).contains("second queued serialized input"),
|
||||
"a later queued turn should still drain after a fast terminal dispatch"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queue_add_rejects_ephemeral_threads() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(vec![
|
||||
create_final_assistant_message_sse_response("unused")?,
|
||||
])
|
||||
.await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let start_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
ephemeral: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(start_response)?;
|
||||
|
||||
let add_request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/add",
|
||||
Some(serde_json::to_value(ThreadQueueAddParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_start_params: text_turn(&thread.id, "ephemeral queued turn"),
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let add_error: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(add_request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
assert_eq!(add_error.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert_eq!(
|
||||
add_error.error.message,
|
||||
format!(
|
||||
"ephemeral thread does not support queued turns: {}",
|
||||
thread.id
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn busy_thread_queue_rows_support_list_reorder_and_delete_before_drain() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
/*workdir*/ None,
|
||||
Some(5000),
|
||||
"queue-blocker",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("active turn done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "untrusted")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let thread = start_thread(&mut mcp).await?;
|
||||
|
||||
let active_turn_request_id = mcp
|
||||
.send_turn_start_request(text_turn(&thread.id, "keep the thread running"))
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(active_turn_request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let approval_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = approval_request else {
|
||||
panic!("expected command approval to keep the active turn open");
|
||||
};
|
||||
|
||||
let first = queue_turn(&mut mcp, &thread.id, "first queued").await?;
|
||||
let second = queue_turn(&mut mcp, &thread.id, "second queued").await?;
|
||||
assert_eq!(
|
||||
list_queue_ids(&mut mcp, &thread.id).await?,
|
||||
vec![first.clone(), second.clone()]
|
||||
);
|
||||
let first_page = list_queue_page(&mut mcp, &thread.id, /*cursor*/ None, Some(1)).await?;
|
||||
assert_eq!(
|
||||
first_page
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|queued_turn| queued_turn.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![first.clone()]
|
||||
);
|
||||
let second_page =
|
||||
list_queue_page(&mut mcp, &thread.id, first_page.next_cursor, Some(1)).await?;
|
||||
assert_eq!(
|
||||
second_page
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|queued_turn| queued_turn.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![second.clone()]
|
||||
);
|
||||
assert_eq!(second_page.next_cursor, None);
|
||||
|
||||
let reorder_request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/reorder",
|
||||
Some(serde_json::to_value(ThreadQueueReorderParams {
|
||||
thread_id: thread.id.clone(),
|
||||
queued_turn_ids: vec![second.clone(), first.clone()],
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let reorder_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(reorder_request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadQueueReorderResponse { queued_turns } = to_response(reorder_response)?;
|
||||
assert_eq!(
|
||||
queued_turns
|
||||
.into_iter()
|
||||
.map(|queued_turn| queued_turn.id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![second.clone(), first.clone()]
|
||||
);
|
||||
|
||||
delete_queue_turn(&mut mcp, &thread.id, &second).await?;
|
||||
delete_queue_turn(&mut mcp, &thread.id, &first).await?;
|
||||
assert!(list_queue_ids(&mut mcp, &thread.id).await?.is_empty());
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Accept,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_turns_stay_serial_after_the_first_dispatch_starts() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
/*workdir*/ None,
|
||||
Some(5000),
|
||||
"queued-serial-blocker",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("first queued turn done")?,
|
||||
create_final_assistant_message_sse_response("second queued turn done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "untrusted")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let thread = start_thread(&mut mcp).await?;
|
||||
|
||||
queue_turn(&mut mcp, &thread.id, "first queued").await?;
|
||||
let approval_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = approval_request else {
|
||||
panic!("expected queued turn approval request to keep the first dispatch active");
|
||||
};
|
||||
|
||||
let second = queue_turn(&mut mcp, &thread.id, "second queued").await?;
|
||||
assert_eq!(list_queue_ids(&mut mcp, &thread.id).await?, vec![second]);
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Accept,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert!(list_queue_ids(&mut mcp, &thread.id).await?.is_empty());
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert_eq!(requests.len(), 3);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&requests[2].body).contains("second queued"),
|
||||
"second queued follow-up should become its own later model request"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_turns_wait_for_a_just_accepted_direct_turn_to_become_visible() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
/*workdir*/ None,
|
||||
Some(5000),
|
||||
"direct-turn-blocker",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("direct turn done")?,
|
||||
create_final_assistant_message_sse_response("queued follow-up done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "untrusted")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let thread = start_thread(&mut mcp).await?;
|
||||
|
||||
let direct_turn_request_id = mcp
|
||||
.send_turn_start_request(text_turn(&thread.id, "direct turn first"))
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(direct_turn_request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let queued_turn_id = queue_turn(&mut mcp, &thread.id, "queued turn after direct").await?;
|
||||
assert_eq!(
|
||||
list_queue_ids(&mut mcp, &thread.id).await?,
|
||||
vec![queued_turn_id]
|
||||
);
|
||||
|
||||
let approval_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = approval_request else {
|
||||
panic!("expected direct turn approval request to keep the direct turn open");
|
||||
};
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Accept,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert!(list_queue_ids(&mut mcp, &thread.id).await?.is_empty());
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert_eq!(requests.len(), 3);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&requests[2].body).contains("queued turn after direct"),
|
||||
"queued follow-up should become its own later model request"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_turns_drain_after_a_direct_turn_has_already_completed() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("direct turn done")?,
|
||||
create_final_assistant_message_sse_response("queued follow-up done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
write_queue_test_config(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
initialize_experimental(&mut mcp).await?;
|
||||
let thread = start_thread(&mut mcp).await?;
|
||||
|
||||
let direct_turn_request_id = mcp
|
||||
.send_turn_start_request(text_turn(&thread.id, "direct turn first"))
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(direct_turn_request_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
queue_turn(&mut mcp, &thread.id, "queued turn after completion").await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert!(list_queue_ids(&mut mcp, &thread.id).await?.is_empty());
|
||||
|
||||
let requests = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("failed to fetch received requests");
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&requests[1].body).contains("queued turn after completion"),
|
||||
"queued follow-up should drain after an already completed direct turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn initialize_experimental(mcp: &mut McpProcess) -> Result<()> {
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_capabilities(
|
||||
ClientInfo {
|
||||
name: "thread-queue-tests".to_string(),
|
||||
title: None,
|
||||
version: "0.0.0".to_string(),
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
opt_out_notification_methods: None,
|
||||
request_attestation: false,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_thread(mcp: &mut McpProcess) -> Result<codex_app_server_protocol::Thread> {
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(response)?;
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
async fn queue_turn(mcp: &mut McpProcess, thread_id: &str, text: &str) -> Result<String> {
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/add",
|
||||
Some(serde_json::to_value(ThreadQueueAddParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_start_params: text_turn(thread_id, text),
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadQueueAddResponse { queued_turn } = to_response(response)?;
|
||||
Ok(queued_turn.id)
|
||||
}
|
||||
|
||||
async fn list_queue_ids(mcp: &mut McpProcess, thread_id: &str) -> Result<Vec<String>> {
|
||||
let ThreadQueueListResponse { data, .. } =
|
||||
list_queue_page(mcp, thread_id, /*cursor*/ None, /*limit*/ None).await?;
|
||||
Ok(data.into_iter().map(|queued_turn| queued_turn.id).collect())
|
||||
}
|
||||
|
||||
async fn list_queue_page(
|
||||
mcp: &mut McpProcess,
|
||||
thread_id: &str,
|
||||
cursor: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<ThreadQueueListResponse> {
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/list",
|
||||
Some(serde_json::to_value(ThreadQueueListParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
cursor,
|
||||
limit,
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
to_response(response)
|
||||
}
|
||||
|
||||
async fn delete_queue_turn(
|
||||
mcp: &mut McpProcess,
|
||||
thread_id: &str,
|
||||
queued_turn_id: &str,
|
||||
) -> Result<()> {
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"thread/queue/delete",
|
||||
Some(serde_json::to_value(ThreadQueueDeleteParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
queued_turn_id: queued_turn_id.to_string(),
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadQueueDeleteResponse { deleted } = to_response(response)?;
|
||||
assert!(deleted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn text_turn(thread_id: &str, text: &str) -> TurnStartParams {
|
||||
TurnStartParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: text.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn write_queue_test_config(
|
||||
codex_home: &std::path::Path,
|
||||
server_uri: &str,
|
||||
approval_policy: &str,
|
||||
) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "{approval_policy}"
|
||||
sandbox_mode = "read-only"
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
15
codex-rs/state/migrations/0035_thread_queued_turns.sql
Normal file
15
codex-rs/state/migrations/0035_thread_queued_turns.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE thread_queued_turns (
|
||||
queued_turn_id TEXT PRIMARY KEY NOT NULL,
|
||||
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
||||
turn_start_params_jsonb BLOB NOT NULL,
|
||||
queue_order INTEGER NOT NULL,
|
||||
state TEXT NOT NULL CHECK(state IN ('pending', 'dispatching', 'failed')),
|
||||
dispatch_turn_id TEXT,
|
||||
failure_jsonb BLOB,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
UNIQUE(thread_id, queue_order)
|
||||
);
|
||||
|
||||
CREATE INDEX thread_queued_turns_thread_state_order_idx
|
||||
ON thread_queued_turns(thread_id, state, queue_order);
|
||||
@@ -47,6 +47,8 @@ pub use model::ThreadGoal;
|
||||
pub use model::ThreadGoalStatus;
|
||||
pub use model::ThreadMetadata;
|
||||
pub use model::ThreadMetadataBuilder;
|
||||
pub use model::ThreadQueuedTurn;
|
||||
pub use model::ThreadQueuedTurnState;
|
||||
pub use model::ThreadsPage;
|
||||
pub use runtime::GoalStore;
|
||||
pub use runtime::RemoteControlEnrollmentRecord;
|
||||
|
||||
@@ -5,6 +5,7 @@ mod log;
|
||||
mod memories;
|
||||
mod thread_goal;
|
||||
mod thread_metadata;
|
||||
mod thread_queued_turn;
|
||||
|
||||
pub use agent_job::AgentJob;
|
||||
pub use agent_job::AgentJobCreateParams;
|
||||
@@ -34,6 +35,8 @@ pub use thread_metadata::SortKey;
|
||||
pub use thread_metadata::ThreadMetadata;
|
||||
pub use thread_metadata::ThreadMetadataBuilder;
|
||||
pub use thread_metadata::ThreadsPage;
|
||||
pub use thread_queued_turn::ThreadQueuedTurn;
|
||||
pub use thread_queued_turn::ThreadQueuedTurnState;
|
||||
|
||||
pub(crate) use agent_job::AgentJobItemRow;
|
||||
pub(crate) use agent_job::AgentJobRow;
|
||||
@@ -44,3 +47,4 @@ pub(crate) use thread_metadata::anchor_from_item;
|
||||
pub(crate) use thread_metadata::datetime_to_epoch_millis;
|
||||
pub(crate) use thread_metadata::datetime_to_epoch_seconds;
|
||||
pub(crate) use thread_metadata::epoch_millis_to_datetime;
|
||||
pub(crate) use thread_queued_turn::ThreadQueuedTurnRow;
|
||||
|
||||
98
codex-rs/state/src/model/thread_queued_turn.rs
Normal file
98
codex-rs/state/src/model/thread_queued_turn.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use sqlx::Row;
|
||||
use sqlx::sqlite::SqliteRow;
|
||||
|
||||
use super::epoch_millis_to_datetime;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThreadQueuedTurnState {
|
||||
Pending,
|
||||
Dispatching,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl ThreadQueuedTurnState {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pending => "pending",
|
||||
Self::Dispatching => "dispatching",
|
||||
Self::Failed => "failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ThreadQueuedTurnState {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self> {
|
||||
match value {
|
||||
"pending" => Ok(Self::Pending),
|
||||
"dispatching" => Ok(Self::Dispatching),
|
||||
"failed" => Ok(Self::Failed),
|
||||
other => Err(anyhow!("unknown thread queued turn state `{other}`")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ThreadQueuedTurn {
|
||||
pub queued_turn_id: String,
|
||||
pub thread_id: ThreadId,
|
||||
pub turn_start_params_jsonb: Vec<u8>,
|
||||
pub queue_order: i64,
|
||||
pub state: ThreadQueuedTurnState,
|
||||
pub dispatch_turn_id: Option<String>,
|
||||
pub failure_jsonb: Option<Vec<u8>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub(crate) struct ThreadQueuedTurnRow {
|
||||
pub queued_turn_id: String,
|
||||
pub thread_id: String,
|
||||
pub turn_start_params_jsonb: Vec<u8>,
|
||||
pub queue_order: i64,
|
||||
pub state: String,
|
||||
pub dispatch_turn_id: Option<String>,
|
||||
pub failure_jsonb: Option<Vec<u8>>,
|
||||
pub created_at_ms: i64,
|
||||
pub updated_at_ms: i64,
|
||||
}
|
||||
|
||||
impl ThreadQueuedTurnRow {
|
||||
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
|
||||
Ok(Self {
|
||||
queued_turn_id: row.try_get("queued_turn_id")?,
|
||||
thread_id: row.try_get("thread_id")?,
|
||||
turn_start_params_jsonb: row.try_get("turn_start_params_jsonb")?,
|
||||
queue_order: row.try_get("queue_order")?,
|
||||
state: row.try_get("state")?,
|
||||
dispatch_turn_id: row.try_get("dispatch_turn_id")?,
|
||||
failure_jsonb: row.try_get("failure_jsonb")?,
|
||||
created_at_ms: row.try_get("created_at_ms")?,
|
||||
updated_at_ms: row.try_get("updated_at_ms")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ThreadQueuedTurnRow> for ThreadQueuedTurn {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(row: ThreadQueuedTurnRow) -> Result<Self> {
|
||||
Ok(Self {
|
||||
queued_turn_id: row.queued_turn_id,
|
||||
thread_id: ThreadId::try_from(row.thread_id)?,
|
||||
turn_start_params_jsonb: row.turn_start_params_jsonb,
|
||||
queue_order: row.queue_order,
|
||||
state: ThreadQueuedTurnState::try_from(row.state.as_str())?,
|
||||
dispatch_turn_id: row.dispatch_turn_id,
|
||||
failure_jsonb: row.failure_jsonb,
|
||||
created_at: epoch_millis_to_datetime(row.created_at_ms)?,
|
||||
updated_at: epoch_millis_to_datetime(row.updated_at_ms)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ mod backfill;
|
||||
mod goals;
|
||||
mod logs;
|
||||
mod memories;
|
||||
mod queued_turns;
|
||||
mod remote_control;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
|
||||
626
codex-rs/state/src/runtime/queued_turns.rs
Normal file
626
codex-rs/state/src/runtime/queued_turns.rs
Normal file
@@ -0,0 +1,626 @@
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
impl StateRuntime {
|
||||
pub async fn append_thread_queued_turn(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
turn_start_params_json: &[u8],
|
||||
) -> anyhow::Result<crate::ThreadQueuedTurn> {
|
||||
let queued_turn_id = Uuid::now_v7().to_string();
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO thread_queued_turns (
|
||||
queued_turn_id,
|
||||
thread_id,
|
||||
turn_start_params_jsonb,
|
||||
queue_order,
|
||||
state,
|
||||
dispatch_turn_id,
|
||||
failure_jsonb,
|
||||
created_at_ms,
|
||||
updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
?,
|
||||
?,
|
||||
jsonb(?),
|
||||
COALESCE(MAX(queue_order), -1) + 1,
|
||||
'pending',
|
||||
NULL,
|
||||
NULL,
|
||||
?,
|
||||
?
|
||||
FROM thread_queued_turns
|
||||
WHERE thread_id = ?
|
||||
RETURNING
|
||||
queued_turn_id,
|
||||
thread_id,
|
||||
CAST(json(turn_start_params_jsonb) AS BLOB) AS turn_start_params_jsonb,
|
||||
queue_order,
|
||||
state,
|
||||
dispatch_turn_id,
|
||||
CASE
|
||||
WHEN failure_jsonb IS NULL THEN NULL
|
||||
ELSE CAST(json(failure_jsonb) AS BLOB)
|
||||
END AS failure_jsonb,
|
||||
created_at_ms,
|
||||
updated_at_ms
|
||||
"#,
|
||||
)
|
||||
.bind(queued_turn_id)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(turn_start_params_json)
|
||||
.bind(now_ms)
|
||||
.bind(now_ms)
|
||||
.bind(thread_id.to_string())
|
||||
.fetch_one(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
thread_queued_turn_from_row(&row)
|
||||
}
|
||||
|
||||
pub async fn list_visible_thread_queued_turns(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> anyhow::Result<Vec<crate::ThreadQueuedTurn>> {
|
||||
self.list_visible_thread_queued_turns_page(thread_id, /*offset*/ 0, i64::MAX as usize)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_visible_thread_queued_turns_page(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> anyhow::Result<Vec<crate::ThreadQueuedTurn>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
queued_turn_id,
|
||||
thread_id,
|
||||
CAST(json(turn_start_params_jsonb) AS BLOB) AS turn_start_params_jsonb,
|
||||
queue_order,
|
||||
state,
|
||||
dispatch_turn_id,
|
||||
CASE
|
||||
WHEN failure_jsonb IS NULL THEN NULL
|
||||
ELSE CAST(json(failure_jsonb) AS BLOB)
|
||||
END AS failure_jsonb,
|
||||
created_at_ms,
|
||||
updated_at_ms
|
||||
FROM thread_queued_turns
|
||||
WHERE thread_id = ?
|
||||
AND state IN ('pending', 'failed')
|
||||
ORDER BY queue_order ASC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(i64::try_from(limit)?)
|
||||
.bind(i64::try_from(offset)?)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
rows.iter().map(thread_queued_turn_from_row).collect()
|
||||
}
|
||||
|
||||
pub async fn delete_thread_queued_turn(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
queued_turn_id: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM thread_queued_turns
|
||||
WHERE thread_id = ?
|
||||
AND queued_turn_id = ?
|
||||
AND state IN ('pending', 'failed')
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(queued_turn_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn reorder_thread_queued_turns(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
ordered_ids: &[String],
|
||||
) -> anyhow::Result<Vec<crate::ThreadQueuedTurn>> {
|
||||
let mut transaction = self.pool.begin().await?;
|
||||
let visible_rows: Vec<(String, i64)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT queued_turn_id, queue_order
|
||||
FROM thread_queued_turns
|
||||
WHERE thread_id = ?
|
||||
AND state IN ('pending', 'failed')
|
||||
ORDER BY queue_order ASC
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id.to_string())
|
||||
.fetch_all(transaction.as_mut())
|
||||
.await?;
|
||||
|
||||
let visible_ids = visible_rows
|
||||
.iter()
|
||||
.map(|(queued_turn_id, _)| queued_turn_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let visible_queue_orders = visible_rows
|
||||
.into_iter()
|
||||
.map(|(_, queue_order)| queue_order)
|
||||
.collect::<Vec<_>>();
|
||||
let mut expected_ids = visible_ids.clone();
|
||||
expected_ids.sort();
|
||||
let mut requested_ids = ordered_ids.to_vec();
|
||||
requested_ids.sort();
|
||||
if expected_ids != requested_ids {
|
||||
anyhow::bail!("queue reorder must include every visible queued turn exactly once");
|
||||
}
|
||||
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
for (temporary_order, queued_turn_id) in ordered_ids.iter().enumerate() {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET queue_order = ?, updated_at_ms = ?
|
||||
WHERE thread_id = ?
|
||||
AND queued_turn_id = ?
|
||||
AND state IN ('pending', 'failed')
|
||||
"#,
|
||||
)
|
||||
.bind(-((temporary_order as i64) + 1))
|
||||
.bind(now_ms)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(queued_turn_id)
|
||||
.execute(transaction.as_mut())
|
||||
.await?;
|
||||
}
|
||||
for (queue_order, queued_turn_id) in visible_queue_orders.into_iter().zip(ordered_ids) {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET queue_order = ?, updated_at_ms = ?
|
||||
WHERE thread_id = ?
|
||||
AND queued_turn_id = ?
|
||||
AND state IN ('pending', 'failed')
|
||||
"#,
|
||||
)
|
||||
.bind(queue_order)
|
||||
.bind(now_ms)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(queued_turn_id)
|
||||
.execute(transaction.as_mut())
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
|
||||
self.list_visible_thread_queued_turns(thread_id).await
|
||||
}
|
||||
|
||||
pub async fn claim_head_thread_queued_turn(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> anyhow::Result<Option<crate::ThreadQueuedTurn>> {
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET state = 'dispatching', updated_at_ms = ?
|
||||
WHERE queued_turn_id = (
|
||||
SELECT head.queued_turn_id
|
||||
FROM thread_queued_turns AS head
|
||||
WHERE head.thread_id = ?
|
||||
AND head.state IN ('pending', 'failed')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM thread_queued_turns AS active
|
||||
WHERE active.thread_id = head.thread_id
|
||||
AND active.state = 'dispatching'
|
||||
)
|
||||
ORDER BY head.queue_order ASC
|
||||
LIMIT 1
|
||||
)
|
||||
AND state = 'pending'
|
||||
RETURNING
|
||||
queued_turn_id,
|
||||
thread_id,
|
||||
CAST(json(turn_start_params_jsonb) AS BLOB) AS turn_start_params_jsonb,
|
||||
queue_order,
|
||||
state,
|
||||
dispatch_turn_id,
|
||||
CASE
|
||||
WHEN failure_jsonb IS NULL THEN NULL
|
||||
ELSE CAST(json(failure_jsonb) AS BLOB)
|
||||
END AS failure_jsonb,
|
||||
created_at_ms,
|
||||
updated_at_ms
|
||||
"#,
|
||||
)
|
||||
.bind(now_ms)
|
||||
.bind(thread_id.to_string())
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
row.map(|row| thread_queued_turn_from_row(&row)).transpose()
|
||||
}
|
||||
|
||||
pub async fn set_dispatching_thread_queued_turn_turn_id(
|
||||
&self,
|
||||
queued_turn_id: &str,
|
||||
turn_id: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET dispatch_turn_id = ?, updated_at_ms = ?
|
||||
WHERE queued_turn_id = ?
|
||||
AND state = 'dispatching'
|
||||
"#,
|
||||
)
|
||||
.bind(turn_id)
|
||||
.bind(now_ms)
|
||||
.bind(queued_turn_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn remove_dispatching_thread_queued_turn(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
turn_id: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM thread_queued_turns
|
||||
WHERE thread_id = ?
|
||||
AND state = 'dispatching'
|
||||
AND dispatch_turn_id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id.to_string())
|
||||
.bind(turn_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn mark_thread_queued_turn_failed(
|
||||
&self,
|
||||
queued_turn_id: &str,
|
||||
failure_json: &[u8],
|
||||
) -> anyhow::Result<bool> {
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET
|
||||
state = 'failed',
|
||||
failure_jsonb = jsonb(?),
|
||||
updated_at_ms = ?
|
||||
WHERE queued_turn_id = ?
|
||||
AND state = 'dispatching'
|
||||
"#,
|
||||
)
|
||||
.bind(failure_json)
|
||||
.bind(now_ms)
|
||||
.bind(queued_turn_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn recover_dispatching_thread_queued_turns(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
failure_json: &[u8],
|
||||
) -> anyhow::Result<u64> {
|
||||
let now_ms = datetime_to_epoch_millis(Utc::now());
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_queued_turns
|
||||
SET
|
||||
state = 'failed',
|
||||
failure_jsonb = jsonb(?),
|
||||
updated_at_ms = ?
|
||||
WHERE thread_id = ?
|
||||
AND state = 'dispatching'
|
||||
"#,
|
||||
)
|
||||
.bind(failure_json)
|
||||
.bind(now_ms)
|
||||
.bind(thread_id.to_string())
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_queued_turn_from_row(
|
||||
row: &sqlx::sqlite::SqliteRow,
|
||||
) -> anyhow::Result<crate::ThreadQueuedTurn> {
|
||||
crate::model::ThreadQueuedTurnRow::try_from_row(row)?.try_into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::test_support::test_thread_metadata;
|
||||
use crate::runtime::test_support::unique_temp_dir;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
async fn runtime_with_thread() -> (Arc<StateRuntime>, ThreadId) {
|
||||
let codex_home = unique_temp_dir();
|
||||
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string())
|
||||
.await
|
||||
.expect("state runtime");
|
||||
let thread_id = ThreadId::new();
|
||||
runtime
|
||||
.upsert_thread(&test_thread_metadata(
|
||||
codex_home.as_path(),
|
||||
thread_id,
|
||||
codex_home.clone(),
|
||||
))
|
||||
.await
|
||||
.expect("insert thread");
|
||||
(runtime, thread_id)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_turn_claim_is_single_winner_and_hides_dispatching_row() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append queued turn");
|
||||
|
||||
let (first, second) = tokio::join!(
|
||||
runtime.claim_head_thread_queued_turn(thread_id),
|
||||
runtime.claim_head_thread_queued_turn(thread_id),
|
||||
);
|
||||
let claimed = [first.expect("first claim"), second.expect("second claim")]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.count();
|
||||
assert_eq!(claimed, 1);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.list_visible_thread_queued_turns(thread_id)
|
||||
.await
|
||||
.expect("list visible queued turns"),
|
||||
Vec::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_turn_added_during_dispatch_claim_waits_for_existing_claim() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append first");
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim first")
|
||||
.expect("claimed row");
|
||||
|
||||
let second = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append second");
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim blocked by dispatch"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.list_visible_thread_queued_turns(thread_id)
|
||||
.await
|
||||
.expect("list visible queued turns"),
|
||||
vec![second]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_claim_rejects_stale_mutations_and_keeps_later_rows_reorderable() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
let first = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append first");
|
||||
let second = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append second");
|
||||
let third = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append third");
|
||||
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim first")
|
||||
.expect("claimed row");
|
||||
|
||||
assert!(
|
||||
!runtime
|
||||
.delete_thread_queued_turn(thread_id, &first.queued_turn_id)
|
||||
.await
|
||||
.expect("dispatching row is not deletable")
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.reorder_thread_queued_turns(
|
||||
thread_id,
|
||||
&[
|
||||
first.queued_turn_id.clone(),
|
||||
third.queued_turn_id.clone(),
|
||||
second.queued_turn_id.clone(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let reordered_ids = runtime
|
||||
.reorder_thread_queued_turns(
|
||||
thread_id,
|
||||
&[third.queued_turn_id.clone(), second.queued_turn_id.clone()],
|
||||
)
|
||||
.await
|
||||
.expect("reorder visible rows")
|
||||
.into_iter()
|
||||
.map(|queued_turn| queued_turn.queued_turn_id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
reordered_ids,
|
||||
vec![third.queued_turn_id, second.queued_turn_id]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn abandoned_dispatch_claim_recovers_as_failed_and_blocks_fifo() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
let first = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append first");
|
||||
runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append second");
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim first")
|
||||
.expect("claimed row");
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.recover_dispatching_thread_queued_turns(
|
||||
thread_id,
|
||||
br#"{"message":"dispatch interrupted"}"#,
|
||||
)
|
||||
.await
|
||||
.expect("recover dispatching rows"),
|
||||
1
|
||||
);
|
||||
|
||||
let visible = runtime
|
||||
.list_visible_thread_queued_turns(thread_id)
|
||||
.await
|
||||
.expect("list recovered queue");
|
||||
assert_eq!(visible[0].queued_turn_id, first.queued_turn_id);
|
||||
assert_eq!(visible[0].state, crate::ThreadQueuedTurnState::Failed);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("failed head blocks claim"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_head_blocks_later_pending_work_until_removed() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
let first = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append first");
|
||||
runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append second");
|
||||
|
||||
let claimed = runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim first")
|
||||
.expect("claimed row");
|
||||
assert_eq!(claimed.queued_turn_id, first.queued_turn_id);
|
||||
runtime
|
||||
.mark_thread_queued_turn_failed(&claimed.queued_turn_id, br#"{"message":"nope"}"#)
|
||||
.await
|
||||
.expect("mark failed");
|
||||
|
||||
assert_eq!(
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("blocked claim"),
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.delete_thread_queued_turn(thread_id, &first.queued_turn_id)
|
||||
.await
|
||||
.expect("delete failed head")
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim next")
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_claim_clears_only_for_its_submitted_turn() {
|
||||
let (runtime, thread_id) = runtime_with_thread().await;
|
||||
let queued_turn = runtime
|
||||
.append_thread_queued_turn(thread_id, br#"{"threadId":"t","input":[]}"#)
|
||||
.await
|
||||
.expect("append queued turn");
|
||||
runtime
|
||||
.claim_head_thread_queued_turn(thread_id)
|
||||
.await
|
||||
.expect("claim queued turn")
|
||||
.expect("claimed row");
|
||||
|
||||
assert!(
|
||||
!runtime
|
||||
.remove_dispatching_thread_queued_turn(thread_id, "regular-turn")
|
||||
.await
|
||||
.expect("unmatched turn must not clear claim")
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.set_dispatching_thread_queued_turn_turn_id(
|
||||
&queued_turn.queued_turn_id,
|
||||
"queued-turn",
|
||||
)
|
||||
.await
|
||||
.expect("record submitted queued turn id")
|
||||
);
|
||||
assert!(
|
||||
!runtime
|
||||
.remove_dispatching_thread_queued_turn(thread_id, "regular-turn")
|
||||
.await
|
||||
.expect("different started turn must not clear claim")
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.remove_dispatching_thread_queued_turn(thread_id, "queued-turn")
|
||||
.await
|
||||
.expect("matching queued turn clears claim")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ pub(super) fn server_notification_thread_target(
|
||||
ServerNotification::ThreadGoalCleared(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::ThreadQueueChanged(notification) => {
|
||||
Some(notification.thread_id.as_str())
|
||||
}
|
||||
ServerNotification::TurnStarted(notification) => Some(notification.thread_id.as_str()),
|
||||
ServerNotification::HookStarted(notification) => Some(notification.thread_id.as_str()),
|
||||
ServerNotification::TurnCompleted(notification) => Some(notification.thread_id.as_str()),
|
||||
|
||||
@@ -219,6 +219,7 @@ impl ChatWidget {
|
||||
| ServerNotification::ThreadStatusChanged(_)
|
||||
| ServerNotification::ThreadArchived(_)
|
||||
| ServerNotification::ThreadUnarchived(_)
|
||||
| ServerNotification::ThreadQueueChanged(_)
|
||||
| ServerNotification::RawResponseItemCompleted(_)
|
||||
| ServerNotification::CommandExecOutputDelta(_)
|
||||
| ServerNotification::ProcessOutputDelta(_)
|
||||
|
||||
Reference in New Issue
Block a user