mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # codex-rs/core/src/tools/handlers/unified_exec.rs
This commit is contained in:
@@ -183,5 +183,15 @@ common:ci-v8 --build_metadata=TAG_os=linux
|
||||
common:ci-v8 --config=remote
|
||||
common:ci-v8 --strategy=remote
|
||||
|
||||
# Source-built Bazel V8 artifacts use the in-process sandbox by default. This
|
||||
# does not affect Cargo's default prebuilt rusty_v8 path.
|
||||
common --@v8//:v8_enable_pointer_compression=True
|
||||
common --@v8//:v8_enable_sandbox=True
|
||||
|
||||
# Keep currently published rusty_v8 release artifacts non-sandboxed until the
|
||||
# artifact migration ships matching Rust feature selection for Cargo consumers.
|
||||
common:v8-release-compat --@v8//:v8_enable_pointer_compression=False
|
||||
common:v8-release-compat --@v8//:v8_enable_sandbox=False
|
||||
|
||||
# Optional per-user local overrides.
|
||||
try-import %workspace%/user.bazelrc
|
||||
|
||||
@@ -63,8 +63,10 @@ def bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> list[Path]:
|
||||
expression = "set(" + " ".join(labels) + ")"
|
||||
bazel_configs = bazel_configs or []
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
@@ -72,6 +74,7 @@ def bazel_output_files(
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*[f"--config={config}" for config in bazel_configs],
|
||||
"--output=files",
|
||||
expression,
|
||||
],
|
||||
@@ -87,7 +90,9 @@ def bazel_build(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> None:
|
||||
bazel_configs = bazel_configs or []
|
||||
subprocess.run(
|
||||
[
|
||||
"bazel",
|
||||
@@ -95,6 +100,7 @@ def bazel_build(
|
||||
"-c",
|
||||
compilation_mode,
|
||||
f"--platforms=@llvm//platforms:{platform}",
|
||||
*[f"--config={config}" for config in bazel_configs],
|
||||
*labels,
|
||||
],
|
||||
cwd=ROOT,
|
||||
@@ -106,13 +112,14 @@ def ensure_bazel_output_files(
|
||||
platform: str,
|
||||
labels: list[str],
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> list[Path]:
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode, bazel_configs)
|
||||
if all(path.exists() for path in outputs):
|
||||
return outputs
|
||||
|
||||
bazel_build(platform, labels, compilation_mode)
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode)
|
||||
bazel_build(platform, labels, compilation_mode, bazel_configs)
|
||||
outputs = bazel_output_files(platform, labels, compilation_mode, bazel_configs)
|
||||
missing = [str(path) for path in outputs if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing built outputs for {labels}: {missing}")
|
||||
@@ -187,8 +194,9 @@ def single_bazel_output_file(
|
||||
platform: str,
|
||||
label: str,
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> Path:
|
||||
outputs = ensure_bazel_output_files(platform, [label], compilation_mode)
|
||||
outputs = ensure_bazel_output_files(platform, [label], compilation_mode, bazel_configs)
|
||||
if len(outputs) != 1:
|
||||
raise SystemExit(f"expected exactly one output for {label}, found {outputs}")
|
||||
return outputs[0]
|
||||
@@ -198,11 +206,17 @@ def merged_musl_archive(
|
||||
platform: str,
|
||||
lib_path: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> Path:
|
||||
llvm_ar = single_bazel_output_file(platform, LLVM_AR_LABEL, compilation_mode)
|
||||
llvm_ranlib = single_bazel_output_file(platform, LLVM_RANLIB_LABEL, compilation_mode)
|
||||
llvm_ar = single_bazel_output_file(platform, LLVM_AR_LABEL, compilation_mode, bazel_configs)
|
||||
llvm_ranlib = single_bazel_output_file(
|
||||
platform,
|
||||
LLVM_RANLIB_LABEL,
|
||||
compilation_mode,
|
||||
bazel_configs,
|
||||
)
|
||||
runtime_archives = [
|
||||
single_bazel_output_file(platform, label, compilation_mode)
|
||||
single_bazel_output_file(platform, label, compilation_mode, bazel_configs)
|
||||
for label in MUSL_RUNTIME_ARCHIVE_LABELS
|
||||
]
|
||||
|
||||
@@ -233,11 +247,13 @@ def stage_release_pair(
|
||||
target: str,
|
||||
output_dir: Path,
|
||||
compilation_mode: str = "fastbuild",
|
||||
bazel_configs: list[str] | None = None,
|
||||
) -> None:
|
||||
outputs = ensure_bazel_output_files(
|
||||
platform,
|
||||
[release_pair_label(target)],
|
||||
compilation_mode,
|
||||
bazel_configs,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -254,7 +270,7 @@ def stage_release_pair(
|
||||
staged_library = output_dir / staged_archive_name(target, lib_path)
|
||||
staged_binding = output_dir / f"src_binding_release_{target}.rs"
|
||||
source_archive = (
|
||||
merged_musl_archive(platform, lib_path, compilation_mode)
|
||||
merged_musl_archive(platform, lib_path, compilation_mode, bazel_configs)
|
||||
if is_musl_archive_target(target, lib_path)
|
||||
else lib_path
|
||||
)
|
||||
@@ -293,6 +309,12 @@ def parse_args() -> argparse.Namespace:
|
||||
stage_release_pair_parser.add_argument("--platform", required=True)
|
||||
stage_release_pair_parser.add_argument("--target", required=True)
|
||||
stage_release_pair_parser.add_argument("--output-dir", required=True)
|
||||
stage_release_pair_parser.add_argument(
|
||||
"--bazel-config",
|
||||
action="append",
|
||||
default=[],
|
||||
dest="bazel_configs",
|
||||
)
|
||||
stage_release_pair_parser.add_argument(
|
||||
"--compilation-mode",
|
||||
default="fastbuild",
|
||||
@@ -330,6 +352,7 @@ def main() -> int:
|
||||
target=args.target,
|
||||
output_dir=Path(args.output_dir),
|
||||
compilation_mode=args.compilation_mode,
|
||||
bazel_configs=args.bazel_configs,
|
||||
)
|
||||
return 0
|
||||
if args.command == "resolved-v8-crate-version":
|
||||
|
||||
@@ -25,7 +25,10 @@ TOP_LEVEL_NAME_EXCEPTIONS = {
|
||||
UTILITY_NAME_EXCEPTIONS = {
|
||||
"path-utils": "codex-utils-path",
|
||||
}
|
||||
MANIFEST_FEATURE_EXCEPTIONS = {}
|
||||
MANIFEST_FEATURE_EXCEPTIONS = {
|
||||
"codex-rs/code-mode/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
|
||||
"codex-rs/v8-poc/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
|
||||
}
|
||||
OPTIONAL_DEPENDENCY_EXCEPTIONS = set()
|
||||
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS = {}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ jobs:
|
||||
-c
|
||||
opt
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
--config=v8-release-compat
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
@@ -134,6 +135,7 @@ jobs:
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--compilation-mode opt \
|
||||
--bazel-config v8-release-compat \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
|
||||
@@ -105,6 +105,7 @@ jobs:
|
||||
bazel_args=(
|
||||
build
|
||||
"--platforms=@llvm//platforms:${PLATFORM}"
|
||||
--config=v8-release-compat
|
||||
"${pair_target}"
|
||||
"${extra_targets[@]}"
|
||||
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
|
||||
@@ -127,6 +128,7 @@ jobs:
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
|
||||
--platform "${PLATFORM}" \
|
||||
--target "${TARGET}" \
|
||||
--bazel-config v8-release-compat \
|
||||
--output-dir "dist/${TARGET}"
|
||||
|
||||
- name: Upload staged musl artifacts
|
||||
|
||||
@@ -327,6 +327,18 @@ crate.annotation(
|
||||
"RUSTY_V8_SRC_BINDING_PATH": "$(execpath @v8_targets//:rusty_v8_binding_for_target)",
|
||||
},
|
||||
crate = "v8",
|
||||
# Keep the Rust feature aligned with the source-built Bazel artifacts.
|
||||
# Windows MSVC still consumes upstream non-sandboxed prebuilts.
|
||||
crate_features_select = {
|
||||
"aarch64-apple-darwin": ["v8_enable_sandbox"],
|
||||
"aarch64-pc-windows-gnullvm": ["v8_enable_sandbox"],
|
||||
"aarch64-unknown-linux-gnu": ["v8_enable_sandbox"],
|
||||
"aarch64-unknown-linux-musl": ["v8_enable_sandbox"],
|
||||
"x86_64-apple-darwin": ["v8_enable_sandbox"],
|
||||
"x86_64-pc-windows-gnullvm": ["v8_enable_sandbox"],
|
||||
"x86_64-unknown-linux-gnu": ["v8_enable_sandbox"],
|
||||
"x86_64-unknown-linux-musl": ["v8_enable_sandbox"],
|
||||
},
|
||||
gen_build_script = "on",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
|
||||
Generated
+4
@@ -2411,6 +2411,7 @@ dependencies = [
|
||||
"bm25",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-agent-graph-store",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
@@ -2700,6 +2701,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.18",
|
||||
@@ -2708,6 +2710,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4219,6 +4222,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-features",
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
|
||||
@@ -3,12 +3,16 @@ use crate::events::AppServerRpcTransport;
|
||||
use crate::events::CodexAppMentionedEventRequest;
|
||||
use crate::events::CodexAppServerClientMetadata;
|
||||
use crate::events::CodexAppUsedEventRequest;
|
||||
use crate::events::CodexCommandExecutionEventParams;
|
||||
use crate::events::CodexCommandExecutionEventRequest;
|
||||
use crate::events::CodexCompactionEventRequest;
|
||||
use crate::events::CodexHookRunEventRequest;
|
||||
use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::CodexToolItemEventBase;
|
||||
use crate::events::CodexTurnEventRequest;
|
||||
use crate::events::CommandExecutionSource;
|
||||
use crate::events::GuardianApprovalRequestSource;
|
||||
use crate::events::GuardianReviewDecision;
|
||||
use crate::events::GuardianReviewEventParams;
|
||||
@@ -17,6 +21,8 @@ use crate::events::GuardianReviewTerminalStatus;
|
||||
use crate::events::GuardianReviewedAction;
|
||||
use crate::events::ThreadInitializedEvent;
|
||||
use crate::events::ThreadInitializedEventParams;
|
||||
use crate::events::ToolItemFinalApprovalOutcome;
|
||||
use crate::events::ToolItemTerminalStatus;
|
||||
use crate::events::TrackEventRequest;
|
||||
use crate::events::codex_app_metadata;
|
||||
use crate::events::codex_hook_run_metadata;
|
||||
@@ -240,6 +246,7 @@ fn sample_turn_start_response(turn_id: &str) -> ClientResponsePayload {
|
||||
ClientResponsePayload::TurnStart(codex_app_server_protocol::TurnStartResponse {
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
@@ -255,6 +262,7 @@ fn sample_turn_started_notification(thread_id: &str, turn_id: &str) -> ServerNot
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
@@ -289,6 +297,7 @@ fn sample_turn_completed_notification(
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status,
|
||||
error: codex_error_info.map(|codex_error_info| AppServerTurnError {
|
||||
@@ -884,6 +893,103 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest {
|
||||
event_type: "codex_command_execution_event",
|
||||
event_params: CodexCommandExecutionEventParams {
|
||||
base: CodexToolItemEventBase {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
app_server_client: CodexAppServerClientMetadata {
|
||||
product_client_id: "codex_tui".to_string(),
|
||||
client_name: Some("codex-tui".to_string()),
|
||||
client_version: Some("1.2.3".to_string()),
|
||||
rpc_transport: AppServerRpcTransport::Websocket,
|
||||
experimental_api_enabled: Some(true),
|
||||
},
|
||||
runtime: CodexRuntimeMetadata {
|
||||
codex_rs_version: "0.99.0".to_string(),
|
||||
runtime_os: "macos".to_string(),
|
||||
runtime_os_version: "15.3.1".to_string(),
|
||||
runtime_arch: "aarch64".to_string(),
|
||||
},
|
||||
thread_source: Some("user"),
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
tool_name: "shell".to_string(),
|
||||
started_at_ms: 123_000,
|
||||
completed_at_ms: 125_000,
|
||||
duration_ms: Some(2000),
|
||||
review_count: 0,
|
||||
guardian_review_count: 0,
|
||||
user_review_count: 0,
|
||||
final_approval_outcome: ToolItemFinalApprovalOutcome::NotNeeded,
|
||||
terminal_status: ToolItemTerminalStatus::Completed,
|
||||
failure_kind: None,
|
||||
requested_additional_permissions: false,
|
||||
requested_network_access: false,
|
||||
},
|
||||
command_execution_source: CommandExecutionSource::Agent,
|
||||
exit_code: Some(0),
|
||||
command_total_action_count: 4,
|
||||
command_read_action_count: 1,
|
||||
command_list_files_action_count: 1,
|
||||
command_search_action_count: 1,
|
||||
command_unknown_action_count: 1,
|
||||
},
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize command execution event");
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_command_execution_event",
|
||||
"event_params": {
|
||||
"thread_id": "thread-1",
|
||||
"turn_id": "turn-1",
|
||||
"item_id": "item-1",
|
||||
"app_server_client": {
|
||||
"product_client_id": "codex_tui",
|
||||
"client_name": "codex-tui",
|
||||
"client_version": "1.2.3",
|
||||
"rpc_transport": "websocket",
|
||||
"experimental_api_enabled": true
|
||||
},
|
||||
"runtime": {
|
||||
"codex_rs_version": "0.99.0",
|
||||
"runtime_os": "macos",
|
||||
"runtime_os_version": "15.3.1",
|
||||
"runtime_arch": "aarch64"
|
||||
},
|
||||
"thread_source": "user",
|
||||
"subagent_source": null,
|
||||
"parent_thread_id": null,
|
||||
"tool_name": "shell",
|
||||
"started_at_ms": 123000,
|
||||
"completed_at_ms": 125000,
|
||||
"duration_ms": 2000,
|
||||
"review_count": 0,
|
||||
"guardian_review_count": 0,
|
||||
"user_review_count": 0,
|
||||
"final_approval_outcome": "not_needed",
|
||||
"terminal_status": "completed",
|
||||
"failure_kind": null,
|
||||
"requested_additional_permissions": false,
|
||||
"requested_network_access": false,
|
||||
"command_execution_source": "agent",
|
||||
"exit_code": 0,
|
||||
"command_total_action_count": 4,
|
||||
"command_read_action_count": 1,
|
||||
"command_list_files_action_count": 1,
|
||||
"command_search_action_count": 1,
|
||||
"command_unknown_action_count": 1
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialized() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
|
||||
@@ -154,6 +154,7 @@ fn sample_turn_start_response() -> ClientResponsePayload {
|
||||
ClientResponsePayload::TurnStart(TurnStartResponse {
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
|
||||
@@ -61,6 +61,20 @@ pub(crate) enum TrackEventRequest {
|
||||
Compaction(Box<CodexCompactionEventRequest>),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
TurnSteer(CodexTurnSteerEventRequest),
|
||||
#[allow(dead_code)]
|
||||
CommandExecution(CodexCommandExecutionEventRequest),
|
||||
#[allow(dead_code)]
|
||||
FileChange(CodexFileChangeEventRequest),
|
||||
#[allow(dead_code)]
|
||||
McpToolCall(CodexMcpToolCallEventRequest),
|
||||
#[allow(dead_code)]
|
||||
DynamicToolCall(CodexDynamicToolCallEventRequest),
|
||||
#[allow(dead_code)]
|
||||
CollabAgentToolCall(CodexCollabAgentToolCallEventRequest),
|
||||
#[allow(dead_code)]
|
||||
WebSearch(CodexWebSearchEventRequest),
|
||||
#[allow(dead_code)]
|
||||
ImageGeneration(CodexImageGenerationEventRequest),
|
||||
PluginUsed(CodexPluginUsedEventRequest),
|
||||
PluginInstalled(CodexPluginEventRequest),
|
||||
PluginUninstalled(CodexPluginEventRequest),
|
||||
@@ -386,6 +400,208 @@ pub(crate) struct GuardianReviewEventPayload {
|
||||
pub(crate) guardian_review: GuardianReviewEventParams,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ToolItemFinalApprovalOutcome {
|
||||
Unknown,
|
||||
NotNeeded,
|
||||
ConfigAllowed,
|
||||
PolicyForbidden,
|
||||
GuardianApproved,
|
||||
GuardianDenied,
|
||||
GuardianAborted,
|
||||
UserApproved,
|
||||
UserApprovedForSession,
|
||||
UserDenied,
|
||||
UserAborted,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ToolItemTerminalStatus {
|
||||
Completed,
|
||||
Failed,
|
||||
Rejected,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum ToolItemFailureKind {
|
||||
ToolError,
|
||||
ApprovalDenied,
|
||||
ApprovalAborted,
|
||||
SandboxDenied,
|
||||
PolicyForbidden,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexToolItemEventBase {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
/// App-server ThreadItem.id. For tool-originated items this generally
|
||||
/// corresponds to the originating core call_id.
|
||||
pub(crate) item_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<&'static str>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) started_at_ms: u64,
|
||||
pub(crate) completed_at_ms: u64,
|
||||
pub(crate) duration_ms: Option<u64>,
|
||||
pub(crate) review_count: u64,
|
||||
pub(crate) guardian_review_count: u64,
|
||||
pub(crate) user_review_count: u64,
|
||||
pub(crate) final_approval_outcome: ToolItemFinalApprovalOutcome,
|
||||
pub(crate) terminal_status: ToolItemTerminalStatus,
|
||||
pub(crate) failure_kind: Option<ToolItemFailureKind>,
|
||||
pub(crate) requested_additional_permissions: bool,
|
||||
pub(crate) requested_network_access: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum CommandExecutionSource {
|
||||
Agent,
|
||||
UserShell,
|
||||
UnifiedExecStartup,
|
||||
UnifiedExecInteraction,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum WebSearchActionKind {
|
||||
Search,
|
||||
OpenPage,
|
||||
FindInPage,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCommandExecutionEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) command_execution_source: CommandExecutionSource,
|
||||
pub(crate) exit_code: Option<i32>,
|
||||
pub(crate) command_total_action_count: u64,
|
||||
pub(crate) command_read_action_count: u64,
|
||||
pub(crate) command_list_files_action_count: u64,
|
||||
pub(crate) command_search_action_count: u64,
|
||||
pub(crate) command_unknown_action_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCommandExecutionEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexCommandExecutionEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexFileChangeEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) file_change_count: u64,
|
||||
pub(crate) file_add_count: u64,
|
||||
pub(crate) file_update_count: u64,
|
||||
pub(crate) file_delete_count: u64,
|
||||
pub(crate) file_move_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexFileChangeEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexFileChangeEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexMcpToolCallEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) mcp_server_name: String,
|
||||
pub(crate) mcp_tool_name: String,
|
||||
pub(crate) mcp_error_present: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexMcpToolCallEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexMcpToolCallEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexDynamicToolCallEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) dynamic_tool_name: String,
|
||||
pub(crate) success: Option<bool>,
|
||||
pub(crate) output_content_item_count: Option<u64>,
|
||||
pub(crate) output_text_item_count: Option<u64>,
|
||||
pub(crate) output_image_item_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexDynamicToolCallEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexDynamicToolCallEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCollabAgentToolCallEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) sender_thread_id: String,
|
||||
pub(crate) receiver_thread_count: u64,
|
||||
pub(crate) receiver_thread_ids: Option<Vec<String>>,
|
||||
pub(crate) requested_model: Option<String>,
|
||||
pub(crate) requested_reasoning_effort: Option<String>,
|
||||
pub(crate) agent_state_count: Option<u64>,
|
||||
pub(crate) completed_agent_count: Option<u64>,
|
||||
pub(crate) failed_agent_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexCollabAgentToolCallEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexCollabAgentToolCallEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexWebSearchEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) web_search_action: Option<WebSearchActionKind>,
|
||||
pub(crate) query_present: bool,
|
||||
pub(crate) query_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexWebSearchEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexWebSearchEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexImageGenerationEventParams {
|
||||
#[serde(flatten)]
|
||||
pub(crate) base: CodexToolItemEventBase,
|
||||
pub(crate) image_generation_status: String,
|
||||
pub(crate) revised_prompt_present: bool,
|
||||
pub(crate) saved_path_present: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexImageGenerationEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexImageGenerationEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexAppMetadata {
|
||||
pub(crate) connector_id: Option<String>,
|
||||
|
||||
@@ -29,7 +29,6 @@ pub use codex_app_server::in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
|
||||
pub use codex_app_server::in_process::InProcessServerEvent;
|
||||
use codex_app_server::in_process::InProcessStartArgs;
|
||||
use codex_app_server::in_process::LogDbLayer;
|
||||
pub use codex_app_server::in_process::StateDbHandle;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
@@ -47,6 +46,7 @@ use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
pub use codex_core::StateDbHandle;
|
||||
use codex_core::config::Config;
|
||||
pub use codex_exec_server::EnvironmentManager;
|
||||
pub use codex_exec_server::EnvironmentManagerArgs;
|
||||
@@ -1154,6 +1154,7 @@ mod tests {
|
||||
thread_id: "thread".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn".to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1984,6 +1985,7 @@ mod tests {
|
||||
thread_id: "thread".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn".to_string(),
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -4312,12 +4312,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -4400,6 +4409,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
|
||||
+53
-2
@@ -9833,6 +9833,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayOrder": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -9880,9 +9883,13 @@
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"trustStatus": {
|
||||
"$ref": "#/definitions/v2/HookTrustStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"currentHash",
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
@@ -9891,7 +9898,8 @@
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
"timeoutSec",
|
||||
"trustStatus"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -10081,6 +10089,15 @@
|
||||
"title": "HookStartedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HookTrustStatus": {
|
||||
"enum": [
|
||||
"managed",
|
||||
"untrusted",
|
||||
"trusted",
|
||||
"modified"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -17666,12 +17683,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -17795,6 +17821,31 @@
|
||||
"title": "TurnInterruptResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
|
||||
+53
-2
@@ -6400,6 +6400,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayOrder": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -6447,9 +6450,13 @@
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"trustStatus": {
|
||||
"$ref": "#/definitions/HookTrustStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"currentHash",
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
@@ -6458,7 +6465,8 @@
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
"timeoutSec",
|
||||
"trustStatus"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -6648,6 +6656,15 @@
|
||||
"title": "HookStartedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HookTrustStatus": {
|
||||
"enum": [
|
||||
"managed",
|
||||
"untrusted",
|
||||
"trusted",
|
||||
"modified"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
@@ -15552,12 +15569,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -15681,6 +15707,31 @@
|
||||
"title": "TurnInterruptResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"properties": {
|
||||
"status": {
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"currentHash": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayOrder": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -94,9 +97,13 @@
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"trustStatus": {
|
||||
"$ref": "#/definitions/HookTrustStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"currentHash",
|
||||
"displayOrder",
|
||||
"enabled",
|
||||
"eventName",
|
||||
@@ -105,7 +112,8 @@
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath",
|
||||
"timeoutSec"
|
||||
"timeoutSec",
|
||||
"trustStatus"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -124,6 +132,15 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookTrustStatus": {
|
||||
"enum": [
|
||||
"managed",
|
||||
"untrusted",
|
||||
"trusted",
|
||||
"modified"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
|
||||
@@ -1324,12 +1324,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1377,6 +1386,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -2225,12 +2225,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -2278,6 +2287,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
+35
-1
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -2225,12 +2225,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -2278,6 +2287,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -2225,12 +2225,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -2278,6 +2287,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
+35
-1
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1675,12 +1675,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1728,6 +1737,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
+35
-1
@@ -1324,12 +1324,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1377,6 +1386,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1324,12 +1324,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1377,6 +1386,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -1324,12 +1324,21 @@
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"description": "Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.",
|
||||
"description": "Thread items currently included in this turn payload.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ThreadItem"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"itemsView": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TurnItemsView"
|
||||
}
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -1377,6 +1386,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnItemsView": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "`items` was not loaded for this turn. The field is intentionally empty.",
|
||||
"enum": [
|
||||
"notLoaded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains only a display summary for this turn.",
|
||||
"enum": [
|
||||
"summary"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "`items` contains every ThreadItem available from persisted app-server history for this turn.",
|
||||
"enum": [
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TurnStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
|
||||
@@ -5,5 +5,6 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { HookEventName } from "./HookEventName";
|
||||
import type { HookHandlerType } from "./HookHandlerType";
|
||||
import type { HookSource } from "./HookSource";
|
||||
import type { HookTrustStatus } from "./HookTrustStatus";
|
||||
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, };
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, currentHash: string, trustStatus: HookTrustStatus, };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
export type HookTrustStatus = "managed" | "untrusted" | "trusted" | "modified";
|
||||
+7
-4
@@ -3,15 +3,18 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ThreadItem } from "./ThreadItem";
|
||||
import type { TurnError } from "./TurnError";
|
||||
import type { TurnItemsView } from "./TurnItemsView";
|
||||
import type { TurnStatus } from "./TurnStatus";
|
||||
|
||||
export type Turn = { id: string,
|
||||
/**
|
||||
* Only populated on a `thread/resume` or `thread/fork` response.
|
||||
* For all other responses and notifications returning a Turn,
|
||||
* the items field will be an empty list.
|
||||
* Thread items currently included in this turn payload.
|
||||
*/
|
||||
items: Array<ThreadItem>, status: TurnStatus,
|
||||
items: Array<ThreadItem>,
|
||||
/**
|
||||
* Describes how much of `items` has been loaded for this turn.
|
||||
*/
|
||||
itemsView: TurnItemsView, status: TurnStatus,
|
||||
/**
|
||||
* Only populated when the Turn's status is failed.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
export type TurnItemsView = "notLoaded" | "summary" | "full";
|
||||
@@ -168,6 +168,7 @@ export type { HookRunSummary } from "./HookRunSummary";
|
||||
export type { HookScope } from "./HookScope";
|
||||
export type { HookSource } from "./HookSource";
|
||||
export type { HookStartedNotification } from "./HookStartedNotification";
|
||||
export type { HookTrustStatus } from "./HookTrustStatus";
|
||||
export type { HooksListEntry } from "./HooksListEntry";
|
||||
export type { HooksListParams } from "./HooksListParams";
|
||||
export type { HooksListResponse } from "./HooksListResponse";
|
||||
@@ -424,6 +425,7 @@ export type { TurnEnvironmentParams } from "./TurnEnvironmentParams";
|
||||
export type { TurnError } from "./TurnError";
|
||||
export type { TurnInterruptParams } from "./TurnInterruptParams";
|
||||
export type { TurnInterruptResponse } from "./TurnInterruptResponse";
|
||||
export type { TurnItemsView } from "./TurnItemsView";
|
||||
export type { TurnPlanStep } from "./TurnPlanStep";
|
||||
export type { TurnPlanStepStatus } from "./TurnPlanStepStatus";
|
||||
export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification";
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::Turn;
|
||||
use crate::protocol::v2::TurnError as V2TurnError;
|
||||
use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnItemsView;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
@@ -1166,6 +1167,7 @@ impl From<PendingTurn> for Turn {
|
||||
Self {
|
||||
id: value.id,
|
||||
items: value.items,
|
||||
items_view: TurnItemsView::Full,
|
||||
error: value.error,
|
||||
status: value.status,
|
||||
started_at: value.started_at,
|
||||
@@ -1180,6 +1182,7 @@ impl From<&PendingTurn> for Turn {
|
||||
Self {
|
||||
id: value.id.clone(),
|
||||
items: value.items.clone(),
|
||||
items_view: TurnItemsView::Full,
|
||||
error: value.error.clone(),
|
||||
status: value.status.clone(),
|
||||
started_at: value.started_at,
|
||||
@@ -1453,6 +1456,7 @@ mod tests {
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
@@ -2723,6 +2727,7 @@ mod tests {
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
}]
|
||||
);
|
||||
@@ -2982,6 +2987,7 @@ mod tests {
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
|
||||
@@ -80,6 +80,7 @@ use codex_protocol::protocol::HookRunStatus as CoreHookRunStatus;
|
||||
use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary;
|
||||
use codex_protocol::protocol::HookScope as CoreHookScope;
|
||||
use codex_protocol::protocol::HookSource as CoreHookSource;
|
||||
use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus;
|
||||
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
|
||||
use codex_protocol::protocol::ModelVerification as CoreModelVerification;
|
||||
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||
@@ -482,6 +483,12 @@ v2_enum_from_core!(
|
||||
}
|
||||
);
|
||||
|
||||
v2_enum_from_core!(
|
||||
pub enum HookTrustStatus from CoreHookTrustStatus {
|
||||
Managed, Untrusted, Trusted, Modified
|
||||
}
|
||||
);
|
||||
|
||||
fn default_hook_source() -> HookSource {
|
||||
HookSource::Unknown
|
||||
}
|
||||
@@ -3841,9 +3848,9 @@ pub struct ThreadStartParams {
|
||||
#[experimental("thread/start.experimentalRawEvents")]
|
||||
#[serde(default)]
|
||||
pub experimental_raw_events: bool,
|
||||
/// If true, persist additional EventMsg variants to the rollout file.
|
||||
/// However, `thread/read`, `thread/resume`, and `thread/fork` still only
|
||||
/// return the limited form of thread history for scalability reasons.
|
||||
/// Deprecated and ignored by app-server. Kept only so older clients can
|
||||
/// continue sending the field while rollout persistence always uses the
|
||||
/// limited history policy.
|
||||
#[experimental("thread/start.persistFullHistory")]
|
||||
#[serde(default)]
|
||||
pub persist_extended_history: bool,
|
||||
@@ -3973,9 +3980,9 @@ pub struct ThreadResumeParams {
|
||||
#[experimental("thread/resume.excludeTurns")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub exclude_turns: bool,
|
||||
/// If true, persist additional EventMsg variants to the rollout file.
|
||||
/// However, `thread/read`, `thread/resume`, and `thread/fork` still only
|
||||
/// return the limited form of thread history for scalability reasons.
|
||||
/// Deprecated and ignored by app-server. Kept only so older clients can
|
||||
/// continue sending the field while rollout persistence always uses the
|
||||
/// limited history policy.
|
||||
#[experimental("thread/resume.persistFullHistory")]
|
||||
#[serde(default)]
|
||||
pub persist_extended_history: bool,
|
||||
@@ -4079,9 +4086,9 @@ pub struct ThreadForkParams {
|
||||
#[experimental("thread/fork.excludeTurns")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub exclude_turns: bool,
|
||||
/// If true, persist additional EventMsg variants to the rollout file.
|
||||
/// However, `thread/read`, `thread/resume`, and `thread/fork` still only
|
||||
/// return the limited form of thread history for scalability reasons.
|
||||
/// Deprecated and ignored by app-server. Kept only so older clients can
|
||||
/// continue sending the field while rollout persistence always uses the
|
||||
/// limited history policy.
|
||||
#[experimental("thread/fork.persistFullHistory")]
|
||||
#[serde(default)]
|
||||
pub persist_extended_history: bool,
|
||||
@@ -5032,6 +5039,8 @@ pub struct HookMetadata {
|
||||
pub display_order: i64,
|
||||
pub enabled: bool,
|
||||
pub is_managed: bool,
|
||||
pub current_hash: String,
|
||||
pub trust_status: HookTrustStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -5425,10 +5434,11 @@ impl From<CoreTokenUsage> for TokenUsageBreakdown {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Turn {
|
||||
pub id: String,
|
||||
/// Only populated on a `thread/resume` or `thread/fork` response.
|
||||
/// For all other responses and notifications returning a Turn,
|
||||
/// the items field will be an empty list.
|
||||
/// Thread items currently included in this turn payload.
|
||||
pub items: Vec<ThreadItem>,
|
||||
/// Describes how much of `items` has been loaded for this turn.
|
||||
#[serde(default)]
|
||||
pub items_view: TurnItemsView,
|
||||
pub status: TurnStatus,
|
||||
/// Only populated when the Turn's status is failed.
|
||||
pub error: Option<TurnError>,
|
||||
@@ -5443,6 +5453,19 @@ pub struct Turn {
|
||||
pub duration_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum TurnItemsView {
|
||||
/// `items` was not loaded for this turn. The field is intentionally empty.
|
||||
NotLoaded,
|
||||
/// `items` contains only a display summary for this turn.
|
||||
Summary,
|
||||
/// `items` contains every ThreadItem available from persisted app-server history for this turn.
|
||||
#[default]
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -8432,6 +8455,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_defaults_legacy_missing_items_view_to_full() {
|
||||
let turn: Turn = serde_json::from_value(json!({
|
||||
"id": "turn_123",
|
||||
"items": [],
|
||||
"status": "completed",
|
||||
"error": null,
|
||||
"startedAt": null,
|
||||
"completedAt": null,
|
||||
"durationMs": null,
|
||||
}))
|
||||
.expect("legacy turn should deserialize");
|
||||
|
||||
assert_eq!(turn.items_view, TurnItemsView::Full);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_list_params_accepts_single_cwd() {
|
||||
let params = serde_json::from_value::<ThreadListParams>(json!({
|
||||
|
||||
@@ -427,6 +427,8 @@ Use `thread/read` to fetch a stored thread by id without resuming it. Pass `incl
|
||||
|
||||
Use `thread/turns/list` with `capabilities.experimentalApi = true` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page.
|
||||
|
||||
Every returned `Turn` includes `itemsView`, which tells clients whether the `items` array was omitted intentionally (`notLoaded`), contains only summary items (`summary`), or contains every item available from persisted app-server history (`full`). Current `thread/turns/list` responses return `full` turns.
|
||||
|
||||
```json
|
||||
{ "method": "thread/turns/list", "id": 24, "params": {
|
||||
"threadId": "thr_123",
|
||||
@@ -1539,7 +1541,11 @@ To enable or disable a skill by name:
|
||||
}
|
||||
```
|
||||
|
||||
Use `hooks/list` to fetch the discovered hooks for one or more `cwds`. Each entry is evaluated using that `cwd`'s effective config, so feature gating and discovered config layers can differ across entries in the same request. Disabled hooks are still returned with `"enabled": false` so clients can render and re-enable them. Hook state is stored under `hooks.state`; clients should treat hooks from managed sources as non-configurable, and user config entries for those keys are ignored during loading. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional.
|
||||
Use `hooks/list` to fetch discovered hooks for one or more `cwds`. Each result is evaluated with that `cwd`'s effective config, so feature gates and discovered config layers can differ within a single response.
|
||||
|
||||
Hooks are returned even when disabled so clients can render and re-enable them. User-controlled state lives under `hooks.state`. Managed hooks are non-configurable, and user entries for managed hook keys are ignored during loading.
|
||||
|
||||
For unmanaged hooks, `currentHash` and `trustStatus` describe whether the current definition is first-seen, approved, or changed since approval. Only trusted unmanaged hooks become runnable. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1570,7 +1576,9 @@ Use `hooks/list` to fetch the discovered hooks for one or more `cwds`. Each entr
|
||||
"source": "user",
|
||||
"pluginId": null,
|
||||
"displayOrder": 0,
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"currentHash": "sha256:...",
|
||||
"trustStatus": "untrusted"
|
||||
}],
|
||||
"warnings": [],
|
||||
"errors": []
|
||||
|
||||
@@ -75,6 +75,7 @@ use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnDiffUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnPlanStep;
|
||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
@@ -157,15 +158,19 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
let turn = {
|
||||
let state = thread_state.lock().await;
|
||||
state.active_turn_snapshot().unwrap_or_else(|| Turn {
|
||||
let mut turn = state.active_turn_snapshot().unwrap_or_else(|| Turn {
|
||||
id: payload.turn_id.clone(),
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
started_at: payload.started_at,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
})
|
||||
});
|
||||
turn.items.clear();
|
||||
turn.items_view = TurnItemsView::NotLoaded;
|
||||
turn
|
||||
};
|
||||
let notification = TurnStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
@@ -1305,6 +1310,7 @@ async fn emit_turn_completed_with_status(
|
||||
turn: Turn {
|
||||
id: event_turn_id,
|
||||
items: vec![],
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: turn_completion_metadata.error,
|
||||
status: turn_completion_metadata.status,
|
||||
started_at: turn_completion_metadata.started_at,
|
||||
@@ -2623,7 +2629,8 @@ mod tests {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
let codex_core::NewThread {
|
||||
thread_id: conversation_id,
|
||||
@@ -3198,6 +3205,92 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_started_omits_active_snapshot_items() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = load_default_config_for_test(&codex_home).await;
|
||||
let thread_manager = Arc::new(
|
||||
codex_core::test_support::thread_manager_with_models_provider_and_home(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
let codex_core::NewThread {
|
||||
thread_id: conversation_id,
|
||||
thread: conversation,
|
||||
..
|
||||
} = thread_manager.start_thread(config.clone()).await?;
|
||||
let thread_state = new_thread_state();
|
||||
{
|
||||
let mut state = thread_state.lock().await;
|
||||
state.track_current_turn_event(
|
||||
"turn-1",
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
);
|
||||
state.track_current_turn_event(
|
||||
"turn-1",
|
||||
&EventMsg::UserMessage(codex_protocol::protocol::UserMessageEvent {
|
||||
message: "already tracked".to_string(),
|
||||
images: None,
|
||||
local_images: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
let thread_watch_manager = ThreadWatchManager::new();
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
|
||||
apply_bespoke_event_handling(
|
||||
Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: Some(42),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
},
|
||||
conversation_id,
|
||||
conversation,
|
||||
thread_manager,
|
||||
/*analytics_events_client*/ None,
|
||||
outgoing,
|
||||
thread_state,
|
||||
thread_watch_manager,
|
||||
Arc::new(tokio::sync::Semaphore::new(/*permits*/ 1)),
|
||||
"test-provider".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
match msg {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::TurnStarted(n)) => {
|
||||
assert_eq!(n.turn.id, "turn-1");
|
||||
assert_eq!(n.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(n.turn.items.is_empty());
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()> {
|
||||
let conversation_id = ThreadId::new();
|
||||
@@ -3245,6 +3338,8 @@ mod tests {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
|
||||
assert_eq!(n.turn.id, event_turn_id);
|
||||
assert_eq!(n.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(n.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(n.turn.items.is_empty());
|
||||
assert_eq!(n.turn.error, None);
|
||||
assert_eq!(n.turn.started_at, Some(42));
|
||||
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
|
||||
|
||||
@@ -52,9 +52,9 @@ use std::time::Duration;
|
||||
|
||||
use crate::analytics_utils::analytics_events_client_from_config;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::OVERLOADED_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::message_processor::ConnectionSessionState;
|
||||
use crate::message_processor::MessageProcessor;
|
||||
use crate::message_processor::MessageProcessorArgs;
|
||||
@@ -82,11 +82,12 @@ use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
pub use codex_rollout::StateDbHandle;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
pub use codex_state::log_db::LogDbLayer;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -127,7 +128,7 @@ pub struct InProcessStartArgs {
|
||||
pub feedback: CodexFeedback,
|
||||
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
|
||||
pub log_db: Option<LogDbLayer>,
|
||||
/// Process-wide SQLite state handle shared with embedded app-server consumers.
|
||||
/// Optional state DB handle to use for the in-process runtime.
|
||||
pub state_db: Option<StateDbHandle>,
|
||||
/// Environment manager used by core execution and filesystem operations.
|
||||
pub environment_manager: Arc<EnvironmentManager>,
|
||||
@@ -344,7 +345,7 @@ impl InProcessClientHandle {
|
||||
/// the runtime is shut down and an `InvalidData` error is returned.
|
||||
pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
|
||||
let initialize = args.initialize.clone();
|
||||
let client = start_uninitialized(args);
|
||||
let client = start_uninitialized(args).await;
|
||||
|
||||
let initialize_response = client
|
||||
.request(ClientRequest::Initialize {
|
||||
@@ -364,8 +365,12 @@ pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle>
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
async fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
let channel_capacity = args.channel_capacity.max(1);
|
||||
let state_db = match args.state_db.clone() {
|
||||
Some(state_db) => Some(state_db),
|
||||
None => init_state_db_from_config(args.config.as_ref()).await,
|
||||
};
|
||||
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
|
||||
let (event_tx, event_rx) = mpsc::channel::<InProcessServerEvent>(channel_capacity);
|
||||
|
||||
@@ -414,6 +419,12 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
);
|
||||
let (processor_tx, mut processor_rx) = mpsc::channel::<ProcessorCommand>(channel_capacity);
|
||||
let mut processor_handle = tokio::spawn(async move {
|
||||
let Some(state_db) = state_db else {
|
||||
warn!(
|
||||
"in-process app-server state db initialization failed; shutting down processor task"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
|
||||
outgoing: Arc::clone(&processor_outgoing),
|
||||
analytics_events_client,
|
||||
@@ -423,7 +434,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
environment_manager: args.environment_manager,
|
||||
feedback: args.feedback,
|
||||
log_db: args.log_db,
|
||||
state_db: args.state_db,
|
||||
state_db,
|
||||
config_warnings: args.config_warnings,
|
||||
session_source: args.session_source,
|
||||
auth_manager,
|
||||
@@ -526,11 +537,9 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
entry.insert(response_tx);
|
||||
}
|
||||
Entry::Occupied(_) => {
|
||||
let _ = response_tx.send(Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("duplicate request id: {request_id:?}"),
|
||||
data: None,
|
||||
}));
|
||||
let _ = response_tx.send(Err(invalid_request(format!(
|
||||
"duplicate request id: {request_id:?}"
|
||||
))));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -553,13 +562,9 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
if let Some(response_tx) =
|
||||
pending_request_responses.remove(&request_id)
|
||||
{
|
||||
let _ = response_tx.send(Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message:
|
||||
"in-process app-server request processor is closed"
|
||||
.to_string(),
|
||||
data: None,
|
||||
}));
|
||||
let _ = response_tx.send(Err(internal_error(
|
||||
"in-process app-server request processor is closed",
|
||||
)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -627,15 +632,20 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
if let Err(send_error) = event_tx
|
||||
.try_send(InProcessServerEvent::ServerRequest(request))
|
||||
{
|
||||
let (code, message, inner) = match send_error {
|
||||
let (error, inner) = match send_error {
|
||||
mpsc::error::TrySendError::Full(inner) => (
|
||||
OVERLOADED_ERROR_CODE,
|
||||
"in-process server request queue is full",
|
||||
JSONRPCErrorError {
|
||||
code: OVERLOADED_ERROR_CODE,
|
||||
message:
|
||||
"in-process server request queue is full".to_string(),
|
||||
data: None,
|
||||
},
|
||||
inner,
|
||||
),
|
||||
mpsc::error::TrySendError::Closed(inner) => (
|
||||
INTERNAL_ERROR_CODE,
|
||||
"in-process server request consumer is closed",
|
||||
internal_error(
|
||||
"in-process server request consumer is closed",
|
||||
),
|
||||
inner,
|
||||
),
|
||||
};
|
||||
@@ -644,14 +654,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
_ => unreachable!("we just sent a ServerRequest variant"),
|
||||
};
|
||||
outgoing_message_sender
|
||||
.notify_client_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code,
|
||||
message: message.to_string(),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.notify_client_error(request_id, error)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -688,21 +691,17 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
drop(writer_rx);
|
||||
drop(processor_tx);
|
||||
outgoing_message_sender
|
||||
.cancel_all_requests(Some(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "in-process app-server runtime is shutting down".to_string(),
|
||||
data: None,
|
||||
}))
|
||||
.cancel_all_requests(Some(internal_error(
|
||||
"in-process app-server runtime is shutting down",
|
||||
)))
|
||||
.await;
|
||||
// Drop the runtime's last sender before awaiting the router task so
|
||||
// `outgoing_rx.recv()` can observe channel closure and exit cleanly.
|
||||
drop(outgoing_message_sender);
|
||||
for (_, response_tx) in pending_request_responses {
|
||||
let _ = response_tx.send(Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "in-process app-server runtime is shutting down".to_string(),
|
||||
data: None,
|
||||
}));
|
||||
let _ = response_tx.send(Err(internal_error(
|
||||
"in-process app-server runtime is shutting down",
|
||||
)));
|
||||
}
|
||||
|
||||
if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut processor_handle).await {
|
||||
@@ -731,6 +730,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
||||
use codex_app_server_protocol::DeviceKeyPublicParams;
|
||||
@@ -743,6 +743,7 @@ mod tests {
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -771,7 +772,7 @@ mod tests {
|
||||
) -> InProcessClientHandle {
|
||||
let codex_home = TempDir::new().expect("temp dir");
|
||||
let config = Arc::new(build_test_config(codex_home.path()).await);
|
||||
let state_db = codex_rollout::state_db::try_init(config.as_ref())
|
||||
let state_db = init_state_db_from_config(config.as_ref())
|
||||
.await
|
||||
.expect("state db should initialize for in-process test");
|
||||
let args = InProcessStartArgs {
|
||||
@@ -829,7 +830,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_process_allows_device_key_requests_to_reach_device_key_processor() {
|
||||
async fn in_process_allows_device_key_requests_to_reach_device_key_api() {
|
||||
let client = start_test_client(SessionSource::Cli).await;
|
||||
const MALFORMED_KEY_ID_MESSAGE: &str = concat!(
|
||||
"invalid device key payload: keyId must be dk_hse_, dk_tpm_, or dk_osn_ ",
|
||||
@@ -972,6 +973,7 @@ mod tests {
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
|
||||
@@ -50,11 +50,11 @@ use codex_config::TextRange as CoreTextRange;
|
||||
use codex_core::ExecPolicyError;
|
||||
use codex_core::check_execpolicy_for_warnings;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::state_db as rollout_state_db;
|
||||
use codex_state::log_db;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -487,9 +487,9 @@ pub async fn run_main_with_transport_options(
|
||||
}
|
||||
};
|
||||
|
||||
let state_db_result = rollout_state_db::try_init(&config).await;
|
||||
let state_db_init_error = state_db_result.as_ref().err().map(ToString::to_string);
|
||||
let state_db = state_db_result.ok();
|
||||
let state_db = init_state_db_from_config(&config)
|
||||
.await
|
||||
.ok_or_else(|| std::io::Error::other("failed to initialize sqlite state db"))?;
|
||||
|
||||
if should_run_personality_migration {
|
||||
let effective_toml = config.config_layer_stack.effective_config();
|
||||
@@ -598,10 +598,12 @@ pub async fn run_main_with_transport_options(
|
||||
|
||||
let feedback_layer = feedback.logger_layer();
|
||||
let feedback_metadata_layer = feedback.metadata_layer();
|
||||
let log_db = state_db.clone().map(log_db::start);
|
||||
let log_db_layer = log_db
|
||||
.clone()
|
||||
.map(|layer| layer.with_filter(Targets::new().with_default(Level::TRACE)));
|
||||
let log_db = log_db::start(state_db.clone());
|
||||
let log_db_layer = Some(
|
||||
log_db
|
||||
.clone()
|
||||
.with_filter(Targets::new().with_default(Level::TRACE)),
|
||||
);
|
||||
let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
|
||||
let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
|
||||
let _ = tracing_subscriber::registry()
|
||||
@@ -618,10 +620,6 @@ pub async fn run_main_with_transport_options(
|
||||
None => error!("{}", warning.summary),
|
||||
}
|
||||
}
|
||||
if let Some(err) = &state_db_init_error {
|
||||
error!("failed to initialize sqlite state db: {err}");
|
||||
}
|
||||
|
||||
let transport_shutdown_token = CancellationToken::new();
|
||||
let mut transport_accept_handles = Vec::<JoinHandle<()>>::new();
|
||||
|
||||
@@ -666,25 +664,17 @@ pub async fn run_main_with_transport_options(
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
let remote_control_config_enabled = config.features.enabled(Feature::RemoteControl);
|
||||
let remote_control_enabled = remote_control_config_enabled && state_db.is_some();
|
||||
if remote_control_config_enabled && state_db.is_none() {
|
||||
error!("remote control disabled because sqlite state db is unavailable");
|
||||
}
|
||||
let remote_control_enabled = config.features.enabled(Feature::RemoteControl);
|
||||
if transport_accept_handles.is_empty() && !remote_control_enabled {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
if remote_control_config_enabled && state_db.is_none() {
|
||||
"no transport configured; remote control disabled because sqlite state db is unavailable"
|
||||
} else {
|
||||
"no transport configured; use --listen or enable remote control"
|
||||
},
|
||||
"no transport configured; use --listen or enable remote control",
|
||||
));
|
||||
}
|
||||
|
||||
let (remote_control_accept_handle, remote_control_handle) = start_remote_control(
|
||||
config.chatgpt_base_url.clone(),
|
||||
state_db.clone(),
|
||||
Some(state_db.clone()),
|
||||
auth_manager.clone(),
|
||||
transport_event_tx.clone(),
|
||||
transport_shutdown_token.clone(),
|
||||
@@ -768,7 +758,7 @@ pub async fn run_main_with_transport_options(
|
||||
config_manager,
|
||||
environment_manager,
|
||||
feedback: feedback.clone(),
|
||||
log_db,
|
||||
log_db: Some(log_db),
|
||||
state_db: state_db.clone(),
|
||||
config_warnings,
|
||||
session_source,
|
||||
|
||||
@@ -61,6 +61,7 @@ use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::agent_graph_store_from_state_db;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::thread_store_from_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
@@ -254,7 +255,7 @@ pub(crate) struct MessageProcessorArgs {
|
||||
pub(crate) environment_manager: Arc<EnvironmentManager>,
|
||||
pub(crate) feedback: CodexFeedback,
|
||||
pub(crate) log_db: Option<LogDbLayer>,
|
||||
pub(crate) state_db: Option<StateDbHandle>,
|
||||
pub(crate) state_db: StateDbHandle,
|
||||
pub(crate) config_warnings: Vec<ConfigWarningNotification>,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
@@ -291,14 +292,16 @@ impl MessageProcessor {
|
||||
// affect per-thread behavior, but they must not move newly started,
|
||||
// resumed, or forked threads to a different persistence backend/root.
|
||||
let thread_store = thread_store_from_config(config.as_ref(), state_db.clone());
|
||||
let agent_graph_store = agent_graph_store_from_state_db(state_db.clone());
|
||||
let thread_manager = Arc::new(ThreadManager::new(
|
||||
config.as_ref(),
|
||||
auth_manager.clone(),
|
||||
session_source,
|
||||
environment_manager,
|
||||
Some(analytics_events_client.clone()),
|
||||
Arc::clone(&thread_store),
|
||||
state_db.clone(),
|
||||
Arc::clone(&thread_store),
|
||||
agent_graph_store.clone(),
|
||||
));
|
||||
thread_manager
|
||||
.plugins_manager()
|
||||
@@ -344,7 +347,7 @@ impl MessageProcessor {
|
||||
Arc::clone(&config),
|
||||
feedback,
|
||||
log_db,
|
||||
state_db.clone(),
|
||||
Some(state_db.clone()),
|
||||
);
|
||||
let git_processor = GitRequestProcessor::new();
|
||||
let initialize_processor = InitializeRequestProcessor::new(
|
||||
@@ -395,7 +398,7 @@ impl MessageProcessor {
|
||||
thread_watch_manager.clone(),
|
||||
Arc::clone(&thread_list_state_permit),
|
||||
thread_goal_processor.clone(),
|
||||
state_db.clone(),
|
||||
Some(state_db.clone()),
|
||||
);
|
||||
let turn_processor = TurnRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
@@ -954,12 +957,22 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::ThreadResume { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_resume(request_id.clone(), params)
|
||||
.thread_resume(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadFork { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_fork(request_id.clone(), params)
|
||||
.thread_fork(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadArchive { params, .. } => {
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::init_state_db_from_config;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
@@ -281,6 +282,9 @@ async fn build_test_processor(
|
||||
outgoing_tx,
|
||||
analytics_events_client.clone(),
|
||||
));
|
||||
let state_db = init_state_db_from_config(config.as_ref())
|
||||
.await
|
||||
.expect("tracing test processor requires state db");
|
||||
let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
|
||||
outgoing,
|
||||
analytics_events_client,
|
||||
@@ -290,7 +294,7 @@ async fn build_test_processor(
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
state_db,
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::VSCode,
|
||||
auth_manager,
|
||||
|
||||
@@ -21,7 +21,6 @@ use tracing::Instrument;
|
||||
use tracing::Span;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::server_request_error::TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON;
|
||||
pub(crate) use codex_app_server_transport::ConnectionId;
|
||||
@@ -157,11 +156,14 @@ impl ThreadScopedOutgoingMessageSender {
|
||||
self.outgoing
|
||||
.cancel_requests_for_thread(
|
||||
self.thread_id,
|
||||
Some(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "client request resolved because the turn state was changed"
|
||||
.to_string(),
|
||||
data: Some(serde_json::json!({ "reason": TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON })),
|
||||
Some({
|
||||
let mut error = internal_error(
|
||||
"client request resolved because the turn state was changed",
|
||||
);
|
||||
error.data = Some(serde_json::json!({
|
||||
"reason": TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON,
|
||||
}));
|
||||
error
|
||||
}),
|
||||
)
|
||||
.await
|
||||
@@ -1011,11 +1013,7 @@ mod tests {
|
||||
connection_id: ConnectionId(9),
|
||||
request_id: RequestId::Integer(3),
|
||||
};
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "boom".to_string(),
|
||||
data: None,
|
||||
};
|
||||
let error = internal_error("boom");
|
||||
|
||||
outgoing.send_error(request_id.clone(), error.clone()).await;
|
||||
|
||||
@@ -1139,11 +1137,7 @@ mod tests {
|
||||
))
|
||||
.await;
|
||||
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "refresh failed".to_string(),
|
||||
data: None,
|
||||
};
|
||||
let error = internal_error("refresh failed");
|
||||
|
||||
outgoing
|
||||
.notify_client_error(request_id, error.clone())
|
||||
@@ -1253,11 +1247,7 @@ mod tests {
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "tracked request cancelled".to_string(),
|
||||
data: None,
|
||||
};
|
||||
let error = internal_error("tracked request cancelled");
|
||||
|
||||
outgoing
|
||||
.cancel_requests_for_thread(thread_id, Some(error.clone()))
|
||||
|
||||
@@ -4,9 +4,6 @@ use crate::command_exec::CommandExecManager;
|
||||
use crate::command_exec::StartCommandExecParams;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_PARAMS_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::invalid_params;
|
||||
use crate::models::supported_models;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
@@ -52,6 +49,7 @@ use codex_app_server_protocol::CommandExecWriteParams;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::ConversationGitInfo;
|
||||
use codex_app_server_protocol::ConversationSummary;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
use codex_app_server_protocol::DynamicToolSpec as ApiDynamicToolSpec;
|
||||
use codex_app_server_protocol::ExperimentalFeature as ApiExperimentalFeature;
|
||||
use codex_app_server_protocol::ExperimentalFeatureListParams;
|
||||
@@ -224,6 +222,7 @@ use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
|
||||
@@ -243,12 +243,9 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
|
||||
fn external_auth_active_error(&self) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "External auth is active. Use account/login/start (chatgptAuthTokens) to update it or account/logout to clear it."
|
||||
.to_string(),
|
||||
data: None,
|
||||
}
|
||||
invalid_request(
|
||||
"External auth is active. Use account/login/start (chatgptAuthTokens) to update it or account/logout to clear it.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn login_api_key_common(
|
||||
@@ -263,11 +260,9 @@ impl AccountRequestProcessor {
|
||||
self.config.forced_login_method,
|
||||
Some(ForcedLoginMethod::Chatgpt)
|
||||
) {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "API key login is disabled. Use ChatGPT login instead.".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"API key login is disabled. Use ChatGPT login instead.",
|
||||
));
|
||||
}
|
||||
|
||||
// Cancel any active login attempt.
|
||||
@@ -287,11 +282,7 @@ impl AccountRequestProcessor {
|
||||
self.auth_manager.reload().await;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to save api key: {err}"),
|
||||
data: None,
|
||||
}),
|
||||
Err(err) => Err(internal_error(format!("failed to save api key: {err}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,11 +312,9 @@ impl AccountRequestProcessor {
|
||||
}
|
||||
|
||||
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "ChatGPT login is disabled. Use API key login instead.".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"ChatGPT login is disabled. Use API key login instead.",
|
||||
));
|
||||
}
|
||||
|
||||
let opts = LoginServerOptions {
|
||||
@@ -354,18 +343,10 @@ impl AccountRequestProcessor {
|
||||
|
||||
fn login_chatgpt_device_code_start_error(err: IoError) -> JSONRPCErrorError {
|
||||
let is_not_found = err.kind() == std::io::ErrorKind::NotFound;
|
||||
JSONRPCErrorError {
|
||||
code: if is_not_found {
|
||||
INVALID_REQUEST_ERROR_CODE
|
||||
} else {
|
||||
INTERNAL_ERROR_CODE
|
||||
},
|
||||
message: if is_not_found {
|
||||
err.to_string()
|
||||
} else {
|
||||
format!("failed to request device code: {err}")
|
||||
},
|
||||
data: None,
|
||||
if is_not_found {
|
||||
invalid_request(err.to_string())
|
||||
} else {
|
||||
internal_error(format!("failed to request device code: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,11 +679,7 @@ impl AccountRequestProcessor {
|
||||
match self.auth_manager.logout_with_revoke().await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("logout failed: {err}"),
|
||||
data: None,
|
||||
});
|
||||
return Err(internal_error(format!("logout failed: {err}")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -885,28 +862,19 @@ impl AccountRequestProcessor {
|
||||
params: SendAddCreditsNudgeEmailParams,
|
||||
) -> Result<AddCreditsNudgeEmailStatus, JSONRPCErrorError> {
|
||||
let Some(auth) = self.auth_manager.auth().await else {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "codex account authentication required to notify workspace owner"
|
||||
.to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"codex account authentication required to notify workspace owner",
|
||||
));
|
||||
};
|
||||
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "chatgpt authentication required to notify workspace owner".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"chatgpt authentication required to notify workspace owner",
|
||||
));
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to construct backend client: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
|
||||
|
||||
match client
|
||||
.send_add_credits_nudge_email(Self::backend_credit_type(params.credit_type))
|
||||
@@ -916,11 +884,9 @@ impl AccountRequestProcessor {
|
||||
Err(err) if err.status().is_some_and(|status| status.as_u16() == 429) => {
|
||||
Ok(AddCreditsNudgeEmailStatus::CooldownActive)
|
||||
}
|
||||
Err(err) => Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to notify workspace owner: {err}"),
|
||||
data: None,
|
||||
}),
|
||||
Err(err) => Err(internal_error(format!(
|
||||
"failed to notify workspace owner: {err}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,42 +907,28 @@ impl AccountRequestProcessor {
|
||||
JSONRPCErrorError,
|
||||
> {
|
||||
let Some(auth) = self.auth_manager.auth().await else {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "codex account authentication required to read rate limits".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"codex account authentication required to read rate limits",
|
||||
));
|
||||
};
|
||||
|
||||
if !auth.uses_codex_backend() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "chatgpt authentication required to read rate limits".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"chatgpt authentication required to read rate limits",
|
||||
));
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth)
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to construct backend client: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|err| internal_error(format!("failed to construct backend client: {err}")))?;
|
||||
|
||||
let snapshots = client
|
||||
.get_rate_limits_many()
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to fetch codex rate limits: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|err| internal_error(format!("failed to fetch codex rate limits: {err}")))?;
|
||||
if snapshots.is_empty() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "failed to fetch codex rate limits: no snapshots returned".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(internal_error(
|
||||
"failed to fetch codex rate limits: no snapshots returned",
|
||||
));
|
||||
}
|
||||
|
||||
let rate_limits_by_limit_id: HashMap<String, CoreRateLimitSnapshot> = snapshots
|
||||
|
||||
@@ -231,21 +231,14 @@ impl AppsRequestProcessor {
|
||||
&self,
|
||||
thread_id: &str,
|
||||
) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {
|
||||
let thread_id = ThreadId::from_string(thread_id).map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid thread id: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
@@ -257,11 +250,7 @@ impl AppsRequestProcessor {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to reload config: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
|
||||
}
|
||||
|
||||
async fn workspace_codex_plugins_enabled(
|
||||
@@ -319,11 +308,9 @@ fn paginate_apps(
|
||||
) -> Result<AppsListResponse, JSONRPCErrorError> {
|
||||
let total = connectors.len();
|
||||
if start > total {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("cursor {start} exceeds total apps {total}"),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"cursor {start} exceeds total apps {total}"
|
||||
)));
|
||||
}
|
||||
|
||||
let effective_limit = limit.unwrap_or(total as u32).max(1) as usize;
|
||||
|
||||
@@ -72,6 +72,8 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
|
||||
display_order: hook.display_order,
|
||||
enabled: hook.enabled,
|
||||
is_managed: hook.is_managed,
|
||||
current_hash: hook.current_hash.clone(),
|
||||
trust_status: hook.trust_status.into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -190,11 +192,7 @@ impl CatalogRequestProcessor {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to reload config: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
|
||||
}
|
||||
|
||||
async fn workspace_codex_plugins_enabled(
|
||||
|
||||
@@ -29,9 +29,7 @@ pub(super) fn config_load_error(err: &std::io::Error) -> JSONRPCErrorError {
|
||||
data
|
||||
});
|
||||
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("failed to load configuration: {err}"),
|
||||
data,
|
||||
}
|
||||
let mut error = invalid_request(format!("failed to load configuration: {err}"));
|
||||
error.data = data;
|
||||
error
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::config_manager_service::ConfigManagerError;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
@@ -612,11 +611,9 @@ fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
||||
}
|
||||
|
||||
fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: Some(json!({
|
||||
"config_write_error_code": code,
|
||||
})),
|
||||
}
|
||||
let mut error = invalid_request(message);
|
||||
error.data = Some(json!({
|
||||
"config_write_error_code": code,
|
||||
}));
|
||||
error
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ use codex_device_key::RemoteControlClientConnectionAudience;
|
||||
use codex_device_key::RemoteControlClientConnectionSignPayload;
|
||||
use codex_device_key::RemoteControlClientEnrollmentAudience;
|
||||
use codex_device_key::RemoteControlClientEnrollmentSignPayload;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_state::DeviceKeyBindingRecord;
|
||||
use codex_state::StateRuntime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DeviceKeyRequestProcessor {
|
||||
@@ -43,10 +43,7 @@ pub(crate) struct DeviceKeyRequestProcessor {
|
||||
}
|
||||
|
||||
impl DeviceKeyRequestProcessor {
|
||||
pub(crate) fn new(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
) -> Self {
|
||||
pub(crate) fn new(outgoing: Arc<OutgoingMessageSender>, state_db: StateDbHandle) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(state_db))),
|
||||
@@ -170,25 +167,18 @@ async fn sign_device_key(
|
||||
}
|
||||
|
||||
struct StateDeviceKeyBindingStore {
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
state_db: StateDbHandle,
|
||||
}
|
||||
|
||||
impl StateDeviceKeyBindingStore {
|
||||
fn new(state_db: Option<Arc<StateRuntime>>) -> Self {
|
||||
fn new(state_db: StateDbHandle) -> Self {
|
||||
Self { state_db }
|
||||
}
|
||||
|
||||
async fn state_db(&self) -> Result<Arc<StateRuntime>, DeviceKeyError> {
|
||||
self.state_db
|
||||
.clone()
|
||||
.ok_or_else(|| DeviceKeyError::Platform("sqlite state db unavailable".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for StateDeviceKeyBindingStore {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StateDeviceKeyBindingStore")
|
||||
.field("has_state_db", &self.state_db.is_some())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
@@ -196,7 +186,7 @@ impl fmt::Debug for StateDeviceKeyBindingStore {
|
||||
#[async_trait]
|
||||
impl DeviceKeyBindingStore for StateDeviceKeyBindingStore {
|
||||
async fn get_binding(&self, key_id: &str) -> Result<Option<DeviceKeyBinding>, DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
let state_db = self.state_db.clone();
|
||||
state_db
|
||||
.get_device_key_binding(key_id)
|
||||
.await
|
||||
@@ -214,7 +204,7 @@ impl DeviceKeyBindingStore for StateDeviceKeyBindingStore {
|
||||
key_id: &str,
|
||||
binding: &DeviceKeyBinding,
|
||||
) -> Result<(), DeviceKeyError> {
|
||||
let state_db = self.state_db().await?;
|
||||
let state_db = self.state_db.clone();
|
||||
state_db
|
||||
.upsert_device_key_binding(&DeviceKeyBindingRecord {
|
||||
key_id: key_id.to_string(),
|
||||
|
||||
@@ -132,10 +132,6 @@ impl MarketplaceRequestProcessor {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to reload config: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,32 +89,21 @@ impl McpRequestProcessor {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to reload config: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
&self,
|
||||
thread_id: &str,
|
||||
) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {
|
||||
let thread_id = ThreadId::from_string(thread_id).map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid thread id: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
@@ -130,11 +119,9 @@ impl McpRequestProcessor {
|
||||
let mcp_servers = match serde_json::to_value(configured_servers) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to serialize MCP servers: {err}"),
|
||||
data: None,
|
||||
});
|
||||
return Err(internal_error(format!(
|
||||
"failed to serialize MCP servers: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,13 +129,9 @@ impl McpRequestProcessor {
|
||||
match serde_json::to_value(config.mcp_oauth_credentials_store_mode) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!(
|
||||
"failed to serialize MCP OAuth credentials store mode: {err}"
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
return Err(internal_error(format!(
|
||||
"failed to serialize MCP OAuth credentials store mode: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -219,11 +219,7 @@ impl PluginRequestProcessor {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to reload config: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))
|
||||
}
|
||||
|
||||
async fn workspace_codex_plugins_enabled(
|
||||
@@ -1307,16 +1303,17 @@ fn remote_plugin_catalog_error_to_jsonrpc(
|
||||
err: RemotePluginCatalogError,
|
||||
context: &str,
|
||||
) -> JSONRPCErrorError {
|
||||
let code = match &err {
|
||||
let message = format!("{context}: {err}");
|
||||
match &err {
|
||||
RemotePluginCatalogError::AuthRequired | RemotePluginCatalogError::UnsupportedAuthMode => {
|
||||
INVALID_REQUEST_ERROR_CODE
|
||||
invalid_request(message)
|
||||
}
|
||||
RemotePluginCatalogError::UnexpectedStatus { status, .. } if status.as_u16() == 404 => {
|
||||
INVALID_REQUEST_ERROR_CODE
|
||||
invalid_request(message)
|
||||
}
|
||||
RemotePluginCatalogError::InvalidPluginPath { .. }
|
||||
| RemotePluginCatalogError::ArchiveTooLarge { .. }
|
||||
| RemotePluginCatalogError::UnknownMarketplace { .. } => INVALID_REQUEST_ERROR_CODE,
|
||||
| RemotePluginCatalogError::UnknownMarketplace { .. } => invalid_request(message),
|
||||
RemotePluginCatalogError::AuthToken(_)
|
||||
| RemotePluginCatalogError::Request { .. }
|
||||
| RemotePluginCatalogError::UnexpectedStatus { .. }
|
||||
@@ -1330,12 +1327,7 @@ fn remote_plugin_catalog_error_to_jsonrpc(
|
||||
| RemotePluginCatalogError::ArchiveJoin(_)
|
||||
| RemotePluginCatalogError::MissingUploadEtag
|
||||
| RemotePluginCatalogError::UnexpectedResponse(_)
|
||||
| RemotePluginCatalogError::CacheRemove(_) => INTERNAL_ERROR_CODE,
|
||||
};
|
||||
JSONRPCErrorError {
|
||||
code,
|
||||
message: format!("{context}: {err}"),
|
||||
data: None,
|
||||
| RemotePluginCatalogError::CacheRemove(_) => internal_error(message),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::fuzzy_file_search::FuzzyFileSearchSession;
|
||||
use crate::fuzzy_file_search::run_fuzzy_file_search;
|
||||
use crate::fuzzy_file_search::start_fuzzy_file_search_session;
|
||||
@@ -132,19 +132,3 @@ impl SearchRequestProcessor {
|
||||
Ok(FuzzyFileSearchSessionStopResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_error(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ pub(crate) struct ThreadGoalRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
config: Arc<Config>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
state_db: StateDbHandle,
|
||||
}
|
||||
|
||||
impl ThreadGoalRequestProcessor {
|
||||
@@ -16,7 +16,7 @@ impl ThreadGoalRequestProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
config: Arc<Config>,
|
||||
thread_state_manager: ThreadStateManager,
|
||||
state_db: Option<StateDbHandle>,
|
||||
state_db: StateDbHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_manager,
|
||||
@@ -72,23 +72,6 @@ impl ThreadGoalRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn pending_resume_goal_state(
|
||||
&self,
|
||||
thread: &CodexThread,
|
||||
) -> (bool, Option<StateDbHandle>) {
|
||||
let emit_thread_goal_update = self.config.features.enabled(Feature::Goals);
|
||||
let thread_goal_state_db = if emit_thread_goal_update {
|
||||
if let Some(state_db) = thread.state_db() {
|
||||
Some(state_db)
|
||||
} else {
|
||||
self.state_db.clone()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(emit_thread_goal_update, thread_goal_state_db)
|
||||
}
|
||||
|
||||
async fn thread_goal_set_inner(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -110,7 +93,7 @@ impl ThreadGoalRequestProcessor {
|
||||
None => find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
Some(self.state_db.as_ref()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -275,7 +258,7 @@ impl ThreadGoalRequestProcessor {
|
||||
None => find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
Some(self.state_db.as_ref()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -339,7 +322,7 @@ impl ThreadGoalRequestProcessor {
|
||||
find_thread_path_by_id_str(
|
||||
&self.config.codex_home,
|
||||
&thread_id.to_string(),
|
||||
self.state_db.as_deref(),
|
||||
Some(self.state_db.as_ref()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -348,9 +331,7 @@ impl ThreadGoalRequestProcessor {
|
||||
.ok_or_else(|| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
}
|
||||
|
||||
self.state_db
|
||||
.clone()
|
||||
.ok_or_else(|| internal_error("sqlite state db unavailable for thread goals"))
|
||||
Ok(self.state_db.clone())
|
||||
}
|
||||
|
||||
async fn emit_thread_goal_snapshot(&self, thread_id: ThreadId) {
|
||||
|
||||
@@ -147,23 +147,17 @@ pub(super) async fn ensure_conversation_listener(
|
||||
{
|
||||
Ok(conv) => conv,
|
||||
Err(_) => {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {conversation_id}"),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"thread not found: {conversation_id}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let thread_state = {
|
||||
let pending_thread_unloads = listener_task_context.pending_thread_unloads.lock().await;
|
||||
if pending_thread_unloads.contains(&conversation_id) {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"thread {conversation_id} is closing; retry after the thread is closed"
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"thread {conversation_id} is closing; retry after the thread is closed"
|
||||
)));
|
||||
}
|
||||
let Some(thread_state) = listener_task_context
|
||||
.thread_state_manager
|
||||
@@ -229,13 +223,9 @@ pub(super) async fn ensure_listener_task_running(
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"thread {conversation_id} is closing; retry after the thread is closed"
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"thread {conversation_id} is closing; retry after the thread is closed"
|
||||
)));
|
||||
};
|
||||
let (mut listener_command_rx, listener_generation) = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
|
||||
@@ -2,6 +2,10 @@ use super::*;
|
||||
|
||||
const THREAD_LIST_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_LIST_MAX_LIMIT: usize = 100;
|
||||
const PERSIST_EXTENDED_HISTORY_DEPRECATION_SUMMARY: &str =
|
||||
"persistExtendedHistory is deprecated and ignored";
|
||||
const PERSIST_EXTENDED_HISTORY_DEPRECATION_DETAILS: &str =
|
||||
"Remove this parameter. App-server always uses limited history persistence.";
|
||||
|
||||
struct ThreadListFilters {
|
||||
model_providers: Option<Vec<String>>,
|
||||
@@ -121,12 +125,6 @@ fn collect_resume_override_mismatches(
|
||||
"developerInstructions override was provided and ignored while running".to_string(),
|
||||
);
|
||||
}
|
||||
if request.persist_extended_history {
|
||||
mismatch_details.push(
|
||||
"persistExtendedHistory override was provided and ignored while running".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
mismatch_details
|
||||
}
|
||||
|
||||
@@ -165,10 +163,8 @@ fn normalize_thread_list_cwd_filters(
|
||||
for cwd in cwds {
|
||||
let cwd = AbsolutePathBuf::relative_to_current_dir(cwd.as_str())
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_PARAMS_ERROR_CODE,
|
||||
message: format!("invalid thread/list cwd filter `{cwd}`: {err}"),
|
||||
data: None,
|
||||
.map_err(|err| {
|
||||
invalid_params(format!("invalid thread/list cwd filter `{cwd}`: {err}"))
|
||||
})?;
|
||||
normalized_cwds.push(cwd);
|
||||
}
|
||||
@@ -333,20 +329,34 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_resume_inner(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
self.thread_resume_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_fork(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_fork_inner(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
self.thread_fork_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_archive(
|
||||
@@ -567,21 +577,14 @@ impl ThreadRequestProcessor {
|
||||
thread_id: &str,
|
||||
) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {
|
||||
// Resolve the core conversation handle from a v2 thread id string.
|
||||
let thread_id = ThreadId::from_string(thread_id).map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid thread id: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
@@ -601,14 +604,18 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
|
||||
app_server_client_name.as_deref(),
|
||||
app_server_client_version.as_deref(),
|
||||
);
|
||||
thread
|
||||
.set_app_server_client_info(app_server_client_name, app_server_client_version)
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to set app server client info: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
|
||||
}
|
||||
|
||||
async fn finalize_thread_teardown(&self, thread_id: ThreadId) {
|
||||
@@ -750,6 +757,10 @@ impl ThreadRequestProcessor {
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
));
|
||||
}
|
||||
if persist_extended_history {
|
||||
self.send_persist_extended_history_deprecation_notice(request_id.connection_id)
|
||||
.await;
|
||||
}
|
||||
let environment_selections = self.parse_environment_selections(environments)?;
|
||||
let mut typesafe_overrides = self.build_thread_config_overrides(
|
||||
model,
|
||||
@@ -792,7 +803,6 @@ impl ThreadRequestProcessor {
|
||||
dynamic_tools,
|
||||
session_start_source,
|
||||
environment_selections,
|
||||
persist_extended_history,
|
||||
service_name,
|
||||
experimental_raw_events,
|
||||
request_trace,
|
||||
@@ -841,6 +851,18 @@ impl ThreadRequestProcessor {
|
||||
self.outgoing.request_trace_context(request_id).await
|
||||
}
|
||||
|
||||
async fn send_persist_extended_history_deprecation_notice(&self, connection_id: ConnectionId) {
|
||||
self.outgoing
|
||||
.send_server_notification_to_connections(
|
||||
&[connection_id],
|
||||
ServerNotification::DeprecationNotice(DeprecationNoticeNotification {
|
||||
summary: PERSIST_EXTENDED_HISTORY_DEPRECATION_SUMMARY.to_string(),
|
||||
details: Some(PERSIST_EXTENDED_HISTORY_DEPRECATION_DETAILS.to_string()),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn submit_core_op(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -864,7 +886,6 @@ impl ThreadRequestProcessor {
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
session_start_source: Option<codex_app_server_protocol::ThreadStartSource>,
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
persist_extended_history: bool,
|
||||
service_name: Option<String>,
|
||||
experimental_raw_events: bool,
|
||||
request_trace: Option<W3cTraceContext>,
|
||||
@@ -981,7 +1002,7 @@ impl ThreadRequestProcessor {
|
||||
},
|
||||
session_source: None,
|
||||
dynamic_tools: core_dynamic_tools,
|
||||
persist_extended_history,
|
||||
persist_extended_history: false,
|
||||
metrics_service_name: service_name,
|
||||
parent_trace: request_trace,
|
||||
environments,
|
||||
@@ -990,7 +1011,7 @@ impl ThreadRequestProcessor {
|
||||
"app_server.thread_start.create_thread",
|
||||
otel.name = "app_server.thread_start.create_thread",
|
||||
thread_start.dynamic_tool_count = core_dynamic_tool_count,
|
||||
thread_start.persist_extended_history = persist_extended_history,
|
||||
thread_start.persist_extended_history = false,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
@@ -1536,10 +1557,8 @@ impl ThreadRequestProcessor {
|
||||
.and_then(|stored_thread| {
|
||||
summary_from_stored_thread(stored_thread, fallback_provider.as_str())
|
||||
.map(|summary| summary_to_thread(summary, &self.config.cwd))
|
||||
.ok_or_else(|| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to read unarchived thread {thread_id}"),
|
||||
data: None,
|
||||
.ok_or_else(|| {
|
||||
internal_error(format!("failed to read unarchived thread {thread_id}"))
|
||||
})
|
||||
})?;
|
||||
|
||||
@@ -2174,6 +2193,8 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2202,6 +2223,10 @@ impl ThreadRequestProcessor {
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
if params.persist_extended_history {
|
||||
self.send_persist_extended_history_deprecation_notice(request_id.connection_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
let _thread_list_state_permit = match self.acquire_thread_list_state_permit().await {
|
||||
Ok(permit) => permit,
|
||||
@@ -2210,7 +2235,15 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
match self.resume_running_thread(&request_id, ¶ms).await {
|
||||
match self
|
||||
.resume_running_thread(
|
||||
&request_id,
|
||||
¶ms,
|
||||
app_server_client_name.clone(),
|
||||
app_server_client_version.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
@@ -2236,7 +2269,7 @@ impl ThreadRequestProcessor {
|
||||
developer_instructions,
|
||||
personality,
|
||||
exclude_turns,
|
||||
persist_extended_history,
|
||||
persist_extended_history: _persist_extended_history,
|
||||
} = params;
|
||||
let include_turns = !exclude_turns;
|
||||
|
||||
@@ -2300,7 +2333,7 @@ impl ThreadRequestProcessor {
|
||||
config.clone(),
|
||||
thread_history,
|
||||
self.auth_manager.clone(),
|
||||
persist_extended_history,
|
||||
/*persist_extended_history*/ false,
|
||||
self.request_trace_context(&request_id).await,
|
||||
)
|
||||
.await
|
||||
@@ -2311,6 +2344,16 @@ impl ThreadRequestProcessor {
|
||||
session_configured,
|
||||
..
|
||||
}) => {
|
||||
if let Err(err) = Self::set_app_server_client_info(
|
||||
codex_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return Ok(());
|
||||
}
|
||||
let SessionConfiguredEvent { rollout_path, .. } = session_configured;
|
||||
let Some(rollout_path) = rollout_path else {
|
||||
let error =
|
||||
@@ -2447,6 +2490,8 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: &ThreadResumeParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<bool, JSONRPCErrorError> {
|
||||
let running_thread = if params.history.is_some() {
|
||||
if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
@@ -2528,6 +2573,12 @@ impl ThreadRequestProcessor {
|
||||
thread_state.clone(),
|
||||
)
|
||||
.await?;
|
||||
Self::set_app_server_client_info(
|
||||
existing_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let config_snapshot = existing_thread.config_snapshot().await;
|
||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||
@@ -2560,10 +2611,10 @@ impl ThreadRequestProcessor {
|
||||
)));
|
||||
};
|
||||
|
||||
let (emit_thread_goal_update, thread_goal_state_db) = self
|
||||
.thread_goal_processor
|
||||
.pending_resume_goal_state(existing_thread.as_ref())
|
||||
.await;
|
||||
let emit_thread_goal_update = self.config.features.enabled(Feature::Goals);
|
||||
let thread_goal_state_db = emit_thread_goal_update
|
||||
.then(|| self.state_db.clone())
|
||||
.flatten();
|
||||
|
||||
let command = crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||
Box::new(crate::thread_state::PendingThreadResumeRequest {
|
||||
@@ -2811,6 +2862,8 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadForkParams {
|
||||
thread_id,
|
||||
@@ -2836,6 +2889,10 @@ impl ThreadRequestProcessor {
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
));
|
||||
}
|
||||
if persist_extended_history {
|
||||
self.send_persist_extended_history_deprecation_notice(request_id.connection_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
let source_thread = self
|
||||
.read_stored_thread_for_resume(&thread_id, path.as_ref(), /*include_history*/ true)
|
||||
@@ -2913,7 +2970,7 @@ impl ThreadRequestProcessor {
|
||||
history: history_items.clone(),
|
||||
rollout_path: source_thread.rollout_path.clone(),
|
||||
}),
|
||||
persist_extended_history,
|
||||
/*persist_extended_history*/ false,
|
||||
self.request_trace_context(&request_id).await,
|
||||
)
|
||||
.await
|
||||
@@ -2925,6 +2982,13 @@ impl ThreadRequestProcessor {
|
||||
err => internal_error(format!("error forking thread: {err}")),
|
||||
})?;
|
||||
|
||||
Self::set_app_server_client_info(
|
||||
forked_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Auto-attach a conversation listener when forking a thread.
|
||||
log_listener_attach_result(
|
||||
self.ensure_conversation_listener(
|
||||
@@ -3181,6 +3245,17 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn xcode_26_4_mcp_elicitations_auto_deny(
|
||||
client_name: Option<&str>,
|
||||
client_version: Option<&str>,
|
||||
) -> bool {
|
||||
// Xcode 26.4 shipped before app-server MCP elicitation requests were
|
||||
// client-visible. Keep elicitations auto-denied for that client line.
|
||||
// TODO: Remove this compatibility hack once Xcode 26.4 ages out.
|
||||
client_name == Some("Xcode")
|
||||
&& client_version.is_some_and(|version| version.starts_with("26.4"))
|
||||
}
|
||||
|
||||
const THREAD_TURNS_DEFAULT_LIMIT: usize = 25;
|
||||
const THREAD_TURNS_MAX_LIMIT: usize = 100;
|
||||
|
||||
@@ -3239,11 +3314,9 @@ fn paginate_thread_turns(
|
||||
.as_ref()
|
||||
.and_then(|anchor| turns.iter().position(|turn| turn.id == anchor.turn_id));
|
||||
if anchor.is_some() && anchor_index.is_none() {
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "invalid cursor: anchor turn is no longer present".to_string(),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(
|
||||
"invalid cursor: anchor turn is no longer present",
|
||||
));
|
||||
}
|
||||
|
||||
let mut keyed_turns: Vec<_> = turns.into_iter().enumerate().collect();
|
||||
@@ -3304,19 +3377,11 @@ fn serialize_thread_turns_cursor(
|
||||
turn_id: turn_id.to_string(),
|
||||
include_anchor,
|
||||
})
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to serialize cursor: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to serialize cursor: {err}")))
|
||||
}
|
||||
|
||||
fn parse_thread_turns_cursor(cursor: &str) -> Result<ThreadTurnsCursor, JSONRPCErrorError> {
|
||||
serde_json::from_str(cursor).map_err(|_| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid cursor: {cursor}"),
|
||||
data: None,
|
||||
})
|
||||
serde_json::from_str(cursor).map_err(|_| invalid_request(format!("invalid cursor: {cursor}")))
|
||||
}
|
||||
|
||||
fn reconstruct_thread_turns_for_turns_list(
|
||||
@@ -3367,36 +3432,18 @@ fn thread_read_view_error(err: ThreadReadViewError) -> JSONRPCErrorError {
|
||||
|
||||
fn thread_store_list_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to list threads: {err}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
err => internal_error(format!("failed to list threads: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_resume_read_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("no rollout found for thread id {thread_id}"),
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to read thread: {err}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => {
|
||||
invalid_request(format!("no rollout found for thread id {thread_id}"))
|
||||
}
|
||||
err => internal_error(format!("failed to read thread: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3459,25 +3506,17 @@ fn conversation_summary_thread_id_read_error(
|
||||
ThreadStoreError::ThreadNotFound { thread_id } if thread_id == conversation_id => {
|
||||
conversation_summary_not_found_error(conversation_id)
|
||||
}
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to load conversation summary for {conversation_id}: {err}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
err => internal_error(format!(
|
||||
"failed to load conversation summary for {conversation_id}: {err}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn conversation_summary_not_found_error(conversation_id: ThreadId) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("no rollout found for conversation id {conversation_id}"),
|
||||
data: None,
|
||||
}
|
||||
invalid_request(format!(
|
||||
"no rollout found for conversation id {conversation_id}"
|
||||
))
|
||||
}
|
||||
|
||||
fn conversation_summary_rollout_path_read_error(
|
||||
@@ -3485,55 +3524,29 @@ fn conversation_summary_rollout_path_read_error(
|
||||
err: ThreadStoreError,
|
||||
) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!(
|
||||
"failed to load conversation summary from {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
err => internal_error(format!(
|
||||
"failed to load conversation summary from {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_write_error(operation: &str, err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to {operation}: {err}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::ThreadNotFound { thread_id } => {
|
||||
invalid_request(format!("thread not found: {thread_id}"))
|
||||
}
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
err => internal_error(format!("failed to {operation}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_archive_error(operation: &str, err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
match err {
|
||||
ThreadStoreError::InvalidRequest { message } => JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
},
|
||||
err => JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to {operation} thread: {err}"),
|
||||
data: None,
|
||||
},
|
||||
ThreadStoreError::InvalidRequest { message } => invalid_request(message),
|
||||
err => internal_error(format!("failed to {operation} thread: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ mod thread_processor_behavior_tests {
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -205,6 +206,7 @@ mod thread_processor_behavior_tests {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}],
|
||||
items_view: TurnItemsView::Full,
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
started_at: None,
|
||||
|
||||
@@ -171,21 +171,14 @@ impl TurnRequestProcessor {
|
||||
thread_id: &str,
|
||||
) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {
|
||||
// Resolve the core conversation handle from a v2 thread id string.
|
||||
let thread_id = ThreadId::from_string(thread_id).map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid thread id: {err}"),
|
||||
data: None,
|
||||
})?;
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;
|
||||
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("thread not found: {thread_id}"),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
@@ -209,14 +202,6 @@ impl TurnRequestProcessor {
|
||||
fn review_request_from_target(
|
||||
target: ApiReviewTarget,
|
||||
) -> Result<(ReviewRequest, String), JSONRPCErrorError> {
|
||||
fn invalid_request(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
let cleaned_target = match target {
|
||||
ApiReviewTarget::UncommittedChanges => ApiReviewTarget::UncommittedChanges,
|
||||
ApiReviewTarget::BaseBranch { branch } => {
|
||||
@@ -305,17 +290,15 @@ impl TurnRequestProcessor {
|
||||
}
|
||||
|
||||
fn input_too_large_error(actual_chars: usize) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_PARAMS_ERROR_CODE,
|
||||
message: format!(
|
||||
"Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters."
|
||||
),
|
||||
data: Some(serde_json::json!({
|
||||
"input_error_code": INPUT_TOO_LARGE_ERROR_CODE,
|
||||
"max_chars": MAX_USER_INPUT_TEXT_CHARS,
|
||||
"actual_chars": actual_chars,
|
||||
})),
|
||||
}
|
||||
let mut error = invalid_params(format!(
|
||||
"Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters."
|
||||
));
|
||||
error.data = Some(serde_json::json!({
|
||||
"input_error_code": INPUT_TOO_LARGE_ERROR_CODE,
|
||||
"max_chars": MAX_USER_INPUT_TEXT_CHARS,
|
||||
"actual_chars": actual_chars,
|
||||
}));
|
||||
error
|
||||
}
|
||||
|
||||
fn validate_v2_input_limit(items: &[V2UserInput]) -> Result<(), JSONRPCErrorError> {
|
||||
@@ -519,6 +502,7 @@ impl TurnRequestProcessor {
|
||||
let turn = Turn {
|
||||
id: turn_id,
|
||||
items: vec![],
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
started_at: None,
|
||||
@@ -561,14 +545,18 @@ impl TurnRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
|
||||
app_server_client_name.as_deref(),
|
||||
app_server_client_version.as_deref(),
|
||||
);
|
||||
thread
|
||||
.set_app_server_client_info(app_server_client_name, app_server_client_version)
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to set app server client info: {err}"),
|
||||
data: None,
|
||||
})
|
||||
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
|
||||
}
|
||||
|
||||
async fn turn_steer_inner(
|
||||
@@ -612,9 +600,8 @@ impl TurnRequestProcessor {
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let (code, message, data, error_type) = match err {
|
||||
let (message, data, error_type) = match err {
|
||||
SteerInputError::NoActiveTurn(_) => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"no active turn to steer".to_string(),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(
|
||||
@@ -622,7 +609,6 @@ impl TurnRequestProcessor {
|
||||
)),
|
||||
),
|
||||
SteerInputError::ExpectedTurnMismatch { expected, actual } => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
format!("expected active turn id `{expected}` but found `{actual}`"),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(
|
||||
@@ -658,24 +644,19 @@ impl TurnRequestProcessor {
|
||||
}
|
||||
};
|
||||
(
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(turn_steer_error)),
|
||||
)
|
||||
}
|
||||
SteerInputError::EmptyInput => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"input must not be empty".to_string(),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::Input(InputError::Empty)),
|
||||
),
|
||||
};
|
||||
let error = JSONRPCErrorError {
|
||||
code,
|
||||
message,
|
||||
data,
|
||||
};
|
||||
let mut error = invalid_request(message);
|
||||
error.data = data;
|
||||
self.track_error_response(request_id, &error, error_type);
|
||||
error
|
||||
})?;
|
||||
@@ -835,6 +816,7 @@ impl TurnRequestProcessor {
|
||||
Turn {
|
||||
id: turn_id,
|
||||
items,
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
started_at: None,
|
||||
@@ -1009,7 +991,7 @@ impl TurnRequestProcessor {
|
||||
request_id,
|
||||
parent_thread,
|
||||
review_request,
|
||||
display_text.as_str(),
|
||||
&display_text,
|
||||
thread_id,
|
||||
)
|
||||
.await?;
|
||||
@@ -1020,7 +1002,7 @@ impl TurnRequestProcessor {
|
||||
parent_thread_id,
|
||||
parent_thread,
|
||||
review_request,
|
||||
display_text.as_str(),
|
||||
&display_text,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1122,3 +1104,14 @@ impl TurnRequestProcessor {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn xcode_26_4_mcp_elicitations_auto_deny(
|
||||
client_name: Option<&str>,
|
||||
client_version: Option<&str>,
|
||||
) -> bool {
|
||||
// Xcode 26.4 shipped before app-server MCP elicitation requests were
|
||||
// client-visible. Keep elicitations auto-denied for that client line.
|
||||
// TODO: Remove this compatibility hack once Xcode 26.4 ages out.
|
||||
client_name == Some("Xcode")
|
||||
&& client_version.is_some_and(|version| version.starts_with("26.4"))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookHandlerType;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
use codex_app_server_protocol::HookTrustStatus;
|
||||
use codex_app_server_protocol::HooksListEntry;
|
||||
use codex_app_server_protocol::HooksListParams;
|
||||
use codex_app_server_protocol::HooksListResponse;
|
||||
@@ -26,11 +27,44 @@ use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_windows;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Serialize;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NormalizedHookIdentity {
|
||||
event_name: &'static str,
|
||||
#[serde(flatten)]
|
||||
group: codex_config::MatcherGroup,
|
||||
}
|
||||
|
||||
fn command_hook_hash(
|
||||
event_name: &'static str,
|
||||
matcher: Option<&str>,
|
||||
command: &str,
|
||||
timeout_sec: u64,
|
||||
status_message: Option<&str>,
|
||||
) -> String {
|
||||
let identity = NormalizedHookIdentity {
|
||||
event_name,
|
||||
group: codex_config::MatcherGroup {
|
||||
matcher: matcher.map(ToOwned::to_owned),
|
||||
hooks: vec![codex_config::HookHandlerConfig::Command {
|
||||
command: command.to_string(),
|
||||
timeout_sec: Some(timeout_sec),
|
||||
r#async: false,
|
||||
status_message: status_message.map(ToOwned::to_owned),
|
||||
}],
|
||||
},
|
||||
};
|
||||
let Ok(value) = codex_config::TomlValue::try_from(identity) else {
|
||||
unreachable!("normalized hook identity should serialize to TOML");
|
||||
};
|
||||
codex_config::version_for_toml(&value)
|
||||
}
|
||||
|
||||
fn write_user_hook_config(codex_home: &std::path::Path) -> Result<()> {
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
@@ -113,6 +147,14 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
display_order: 0,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: command_hook_hash(
|
||||
"pre_tool_use",
|
||||
Some("Bash"),
|
||||
"python3 /tmp/listed-hook.py",
|
||||
/*timeout_sec*/ 5,
|
||||
Some("running listed hook"),
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
}],
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
@@ -183,6 +225,14 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
display_order: 0,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: command_hook_hash(
|
||||
"pre_tool_use",
|
||||
Some("Bash"),
|
||||
"echo plugin hook",
|
||||
/*timeout_sec*/ 7,
|
||||
Some("running plugin hook"),
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
}],
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
@@ -300,6 +350,14 @@ timeout = 5
|
||||
display_order: 0,
|
||||
enabled: true,
|
||||
is_managed: false,
|
||||
current_hash: command_hook_hash(
|
||||
"pre_tool_use",
|
||||
Some("Bash"),
|
||||
"echo project hook",
|
||||
/*timeout_sec*/ 5,
|
||||
/*status_message*/ None,
|
||||
),
|
||||
trust_status: HookTrustStatus::Untrusted,
|
||||
}],
|
||||
warnings: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
@@ -408,6 +466,254 @@ async fn config_batch_write_toggles_user_hook() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_updates_hook_trust_for_loaded_session() -> Result<()> {
|
||||
skip_if_windows!(Ok(()));
|
||||
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Warmup")?,
|
||||
create_final_assistant_message_sse_response("Untrusted turn")?,
|
||||
create_final_assistant_message_sse_response("Trusted turn")?,
|
||||
create_final_assistant_message_sse_response("Modified turn")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let hook_script_path = codex_home.path().join("user_prompt_submit_hook.py");
|
||||
let hook_log_path = codex_home.path().join("user_prompt_submit_hook_log.jsonl");
|
||||
std::fs::write(
|
||||
&hook_script_path,
|
||||
format!(
|
||||
r#"import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
payload = json.load(sys.stdin)
|
||||
with Path(r"{hook_log_path}").open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
"#,
|
||||
hook_log_path = hook_log_path.display(),
|
||||
),
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "never"
|
||||
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
|
||||
|
||||
[hooks]
|
||||
|
||||
[[hooks.UserPromptSubmit]]
|
||||
|
||||
[[hooks.UserPromptSubmit.hooks]]
|
||||
type = "command"
|
||||
command = "python3 {hook_script_path}"
|
||||
"#,
|
||||
server_uri = server.uri(),
|
||||
hook_script_path = hook_script_path.display(),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let hook_list_id = mcp
|
||||
.send_hooks_list_request(HooksListParams {
|
||||
cwds: vec![codex_home.path().to_path_buf()],
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(hook_list_id)),
|
||||
)
|
||||
.await??;
|
||||
let HooksListResponse { data } = to_response(response)?;
|
||||
let hook = data[0].hooks[0].clone();
|
||||
assert_eq!(hook.trust_status, HookTrustStatus::Untrusted);
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(response)?;
|
||||
|
||||
let first_turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "first turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(first_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert!(!std::fs::exists(&hook_log_path)?);
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_batch_write_request(ConfigBatchWriteParams {
|
||||
edits: vec![ConfigEdit {
|
||||
key_path: "hooks.state".to_string(),
|
||||
value: serde_json::json!({
|
||||
hook.key.clone(): {
|
||||
"trusted_hash": hook.current_hash.clone()
|
||||
}
|
||||
}),
|
||||
merge_strategy: MergeStrategy::Upsert,
|
||||
}],
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: true,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: codex_app_server_protocol::ConfigWriteResponse = to_response(response)?;
|
||||
|
||||
let hook_list_id = mcp
|
||||
.send_hooks_list_request(HooksListParams {
|
||||
cwds: vec![codex_home.path().to_path_buf()],
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(hook_list_id)),
|
||||
)
|
||||
.await??;
|
||||
let HooksListResponse { data } = to_response(response)?;
|
||||
let trusted_hook = &data[0].hooks[0];
|
||||
assert_eq!(trusted_hook.key, hook.key);
|
||||
assert_eq!(trusted_hook.current_hash, hook.current_hash);
|
||||
assert_eq!(trusted_hook.trust_status, HookTrustStatus::Trusted);
|
||||
|
||||
let second_turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "second turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(second_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&hook_log_path)?
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty())
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_batch_write_request(ConfigBatchWriteParams {
|
||||
edits: vec![ConfigEdit {
|
||||
key_path: "hooks.UserPromptSubmit".to_string(),
|
||||
value: serde_json::json!([{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": format!("python3 {}", hook_script_path.display()),
|
||||
"statusMessage": "modified hook",
|
||||
}],
|
||||
}]),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
}],
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: true,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: codex_app_server_protocol::ConfigWriteResponse = to_response(response)?;
|
||||
|
||||
let hook_list_id = mcp
|
||||
.send_hooks_list_request(HooksListParams {
|
||||
cwds: vec![codex_home.path().to_path_buf()],
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(hook_list_id)),
|
||||
)
|
||||
.await??;
|
||||
let HooksListResponse { data } = to_response(response)?;
|
||||
let modified_hook = &data[0].hooks[0];
|
||||
assert_eq!(modified_hook.key, hook.key);
|
||||
assert_ne!(modified_hook.current_hash, hook.current_hash);
|
||||
assert_eq!(modified_hook.trust_status, HookTrustStatus::Modified);
|
||||
|
||||
let third_turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "third turn".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(third_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&hook_log_path)?
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty())
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_disables_hook_for_loaded_session() -> Result<()> {
|
||||
skip_if_windows!(Ok(()));
|
||||
@@ -482,6 +788,29 @@ command = "python3 {hook_script_path}"
|
||||
let hook = &data[0].hooks[0];
|
||||
assert_eq!(hook.enabled, true);
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_batch_write_request(ConfigBatchWriteParams {
|
||||
edits: vec![ConfigEdit {
|
||||
key_path: "hooks.state".to_string(),
|
||||
value: serde_json::json!({
|
||||
hook.key.clone(): {
|
||||
"trusted_hash": hook.current_hash.clone()
|
||||
}
|
||||
}),
|
||||
merge_strategy: MergeStrategy::Upsert,
|
||||
}],
|
||||
file_path: None,
|
||||
expected_version: None,
|
||||
reload_user_config: true,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: codex_app_server_protocol::ConfigWriteResponse = to_response(response)?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
|
||||
@@ -42,6 +42,7 @@ use codex_core::config::ConfigBuilder;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
@@ -67,6 +68,13 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste
|
||||
.loader_overrides(loader_overrides.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let sqlite_home = TempDir::new()?;
|
||||
let state_db = StateRuntime::init(
|
||||
sqlite_home.path().to_path_buf(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("remote thread store regression test should initialize state db");
|
||||
|
||||
let thread_store = InMemoryThreadStore::for_id(store_id.clone());
|
||||
let _in_memory_store = InMemoryThreadStoreId { store_id };
|
||||
@@ -80,7 +88,7 @@ async fn thread_start_with_non_local_thread_store_does_not_create_local_persiste
|
||||
thread_config_loader: Arc::new(NoopThreadConfigLoader),
|
||||
feedback: CodexFeedback::new(),
|
||||
log_db: None,
|
||||
state_db: None,
|
||||
state_db: Some(state_db),
|
||||
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
||||
config_warnings: Vec::new(),
|
||||
session_source: SessionSource::Cli,
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
@@ -85,6 +86,17 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<()
|
||||
assert_eq!(review_thread_id, thread_id.clone());
|
||||
let turn_id = turn.id.clone();
|
||||
assert_eq!(turn.status, TurnStatus::InProgress);
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
id: turn_id.clone(),
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "commit 1234567: Tidy UI colors".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
);
|
||||
|
||||
// Confirm we see the EnteredReviewMode marker on the main thread.
|
||||
let mut saw_entered_review_mode = false;
|
||||
@@ -182,6 +194,17 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() ->
|
||||
.await??;
|
||||
let ReviewStartResponse { turn, .. } = to_response::<ReviewStartResponse>(review_resp)?;
|
||||
let turn_id = turn.id.clone();
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
id: turn_id.clone(),
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "commit 1234567: Check review approvals".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
);
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -300,6 +323,17 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<(
|
||||
} = to_response::<ReviewStartResponse>(review_resp)?;
|
||||
|
||||
assert_eq!(turn.status, TurnStatus::InProgress);
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
id: turn.id.clone(),
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "detached review".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
);
|
||||
assert_ne!(
|
||||
review_thread_id, thread_id,
|
||||
"detached review should run on a different thread"
|
||||
|
||||
@@ -33,6 +33,7 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListResponse;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -174,6 +175,7 @@ async fn thread_read_can_include_turns() -> Result<()> {
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
let turn = &thread.turns[0];
|
||||
assert_eq!(turn.status, TurnStatus::Completed);
|
||||
assert_eq!(turn.items_view, TurnItemsView::Full);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
@@ -234,6 +236,10 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
backwards_cursor,
|
||||
} = to_response::<ThreadTurnsListResponse>(read_resp)?;
|
||||
assert_eq!(turn_user_texts(&data), vec!["third", "second"]);
|
||||
assert!(
|
||||
data.iter()
|
||||
.all(|turn| turn.items_view == TurnItemsView::Full)
|
||||
);
|
||||
let next_cursor = next_cursor.expect("expected nextCursor for older turns");
|
||||
let backwards_cursor = backwards_cursor.expect("expected backwardsCursor for newest turn");
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -1629,6 +1630,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
|
||||
.await??;
|
||||
let TurnStartResponse { turn: running_turn } =
|
||||
to_response::<TurnStartResponse>(running_turn_resp)?;
|
||||
assert_eq!(running_turn.items_view, TurnItemsView::NotLoaded);
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("turn/started"),
|
||||
|
||||
@@ -6,6 +6,7 @@ use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
@@ -50,6 +51,46 @@ use super::analytics::wait_for_analytics_payload;
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_deprecates_persist_extended_history_true() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
persist_extended_history: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("deprecationNotice"),
|
||||
)
|
||||
.await??;
|
||||
let notice: DeprecationNoticeNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.expect("deprecationNotice params should be present"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
notice.summary,
|
||||
"persistExtendedHistory is deprecated and ignored"
|
||||
);
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
// Provide a mock server and config so model wiring is valid.
|
||||
|
||||
@@ -44,6 +44,7 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
@@ -868,6 +869,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
codex_app_server_protocol::TurnStatus::InProgress
|
||||
);
|
||||
assert_eq!(started.turn.id, turn.id);
|
||||
assert_eq!(started.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(started.turn.items.is_empty());
|
||||
|
||||
let completed_notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -882,6 +885,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.id, turn.id);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(completed.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(completed.turn.items.is_empty());
|
||||
|
||||
// Send a second turn that exercises the overrides path: change the model.
|
||||
let turn_req2 = mcp
|
||||
@@ -915,6 +920,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
assert_eq!(started2.thread_id, thread.id);
|
||||
assert_eq!(started2.turn.id, turn2.id);
|
||||
assert_eq!(started2.turn.status, TurnStatus::InProgress);
|
||||
assert_eq!(started2.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(started2.turn.items.is_empty());
|
||||
|
||||
let completed_notif2: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -929,6 +936,8 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
assert_eq!(completed2.thread_id, thread.id);
|
||||
assert_eq!(completed2.turn.id, turn2.id);
|
||||
assert_eq!(completed2.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(completed2.turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert!(completed2.turn.items.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -447,12 +447,20 @@ struct AppServerCommand {
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExecServerCommand {
|
||||
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
|
||||
#[arg(
|
||||
long = "listen",
|
||||
value_name = "URL",
|
||||
default_value = "ws://127.0.0.1:0"
|
||||
)]
|
||||
listen: String,
|
||||
#[arg(long = "listen", value_name = "URL", conflicts_with = "remote")]
|
||||
listen: Option<String>,
|
||||
|
||||
/// Register this exec-server as a remote executor using the given base URL.
|
||||
#[arg(long = "remote", value_name = "URL", requires = "executor_id")]
|
||||
remote: Option<String>,
|
||||
|
||||
/// Executor id to attach to when registering remotely.
|
||||
#[arg(long = "executor-id", value_name = "ID")]
|
||||
executor_id: Option<String>,
|
||||
|
||||
/// Human-readable executor name.
|
||||
#[arg(long = "name", value_name = "NAME")]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
@@ -1264,7 +1272,23 @@ async fn run_exec_server_command(
|
||||
codex_self_exe,
|
||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
)?;
|
||||
codex_exec_server::run_main(&cmd.listen, runtime_paths)
|
||||
if let Some(base_url) = cmd.remote {
|
||||
let executor_id = cmd
|
||||
.executor_id
|
||||
.ok_or_else(|| anyhow::anyhow!("--executor-id is required when --remote is set"))?;
|
||||
let mut remote_config =
|
||||
codex_exec_server::RemoteExecutorConfig::new(base_url, executor_id)?;
|
||||
if let Some(name) = cmd.name {
|
||||
remote_config.name = name;
|
||||
}
|
||||
codex_exec_server::run_remote_executor(remote_config, runtime_paths).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let listen_url = cmd
|
||||
.listen
|
||||
.as_deref()
|
||||
.unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);
|
||||
codex_exec_server::run_main(listen_url, runtime_paths)
|
||||
.await
|
||||
.map_err(anyhow::Error::from_boxed)
|
||||
}
|
||||
@@ -1387,7 +1411,7 @@ async fn run_debug_prompt_input_command(
|
||||
});
|
||||
}
|
||||
|
||||
let prompt_input = codex_core::build_prompt_input(config, input, /*state_db*/ None).await?;
|
||||
let prompt_input = codex_core::build_prompt_input(config, input).await?;
|
||||
println!("{}", serde_json::to_string_pretty(&prompt_input)?);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -9,6 +9,9 @@ doctest = false
|
||||
name = "codex_code_mode"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
sandbox = ["v8/v8_enable_sandbox"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -128,6 +128,14 @@ impl McpConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elicitations_auto_deny(&self) -> bool {
|
||||
self.elicitation_requests.auto_deny()
|
||||
}
|
||||
|
||||
pub fn set_elicitations_auto_deny(&self, auto_deny: bool) {
|
||||
self.elicitation_requests.set_auto_deny(auto_deny);
|
||||
}
|
||||
|
||||
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
mcp_servers: &HashMap<String, McpServerConfig>,
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(crate) struct ElicitationRequestManager {
|
||||
requests: Arc<Mutex<ResponderMap>>,
|
||||
pub(crate) approval_policy: Arc<StdMutex<AskForApproval>>,
|
||||
pub(crate) permission_profile: Arc<StdMutex<PermissionProfile>>,
|
||||
auto_deny: Arc<StdMutex<bool>>,
|
||||
}
|
||||
|
||||
impl ElicitationRequestManager {
|
||||
@@ -47,6 +48,20 @@ impl ElicitationRequestManager {
|
||||
requests: Arc::new(Mutex::new(HashMap::new())),
|
||||
approval_policy: Arc::new(StdMutex::new(approval_policy)),
|
||||
permission_profile: Arc::new(StdMutex::new(permission_profile)),
|
||||
auto_deny: Arc::new(StdMutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn auto_deny(&self) -> bool {
|
||||
self.auto_deny
|
||||
.lock()
|
||||
.map(|auto_deny| *auto_deny)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn set_auto_deny(&self, auto_deny: bool) {
|
||||
if let Ok(mut current) = self.auto_deny.lock() {
|
||||
*current = auto_deny;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +88,27 @@ impl ElicitationRequestManager {
|
||||
let elicitation_requests = self.requests.clone();
|
||||
let approval_policy = self.approval_policy.clone();
|
||||
let permission_profile = self.permission_profile.clone();
|
||||
let auto_deny = self.auto_deny.clone();
|
||||
Box::new(move |id, elicitation| {
|
||||
let elicitation_requests = elicitation_requests.clone();
|
||||
let tx_event = tx_event.clone();
|
||||
let server_name = server_name.clone();
|
||||
let approval_policy = approval_policy.clone();
|
||||
let permission_profile = permission_profile.clone();
|
||||
let auto_deny = auto_deny.clone();
|
||||
async move {
|
||||
let auto_deny = auto_deny
|
||||
.lock()
|
||||
.map(|auto_deny| *auto_deny)
|
||||
.unwrap_or(false);
|
||||
if auto_deny {
|
||||
return Ok(ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
let approval_policy = approval_policy
|
||||
.lock()
|
||||
.map(|policy| *policy)
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct HooksToml {
|
||||
pub struct HookStateToml {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trusted_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -90,6 +90,7 @@ fn hooks_toml_deserializes_inline_events_and_state_map() {
|
||||
r#"
|
||||
[state."/tmp/hooks.json:pre_tool_use:0:0"]
|
||||
enabled = false
|
||||
trusted_hash = "sha256:abc123"
|
||||
|
||||
[[PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
@@ -120,6 +121,7 @@ command = "python3 /tmp/pre.py"
|
||||
"/tmp/hooks.json:pre_tool_use:0:0".to_string(),
|
||||
super::HookStateToml {
|
||||
enabled: Some(false),
|
||||
trusted_hash: Some("sha256:abc123".to_string()),
|
||||
},
|
||||
)]),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::config_toml::ToolsToml;
|
||||
use crate::types::AnalyticsConfigToml;
|
||||
use crate::types::ApprovalsReviewer;
|
||||
use crate::types::Personality;
|
||||
use crate::types::SessionPickerViewMode;
|
||||
use crate::types::WindowsToml;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -63,6 +64,9 @@ pub struct ConfigProfile {
|
||||
pub tools: Option<ToolsToml>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub analytics: Option<AnalyticsConfigToml>,
|
||||
/// TUI settings scoped to this profile.
|
||||
#[serde(default)]
|
||||
pub tui: Option<ProfileTui>,
|
||||
#[serde(default)]
|
||||
pub windows: Option<WindowsToml>,
|
||||
/// Optional feature toggles scoped to this profile.
|
||||
@@ -73,6 +77,16 @@ pub struct ConfigProfile {
|
||||
pub oss_provider: Option<String>,
|
||||
}
|
||||
|
||||
/// TUI settings supported inside a named profile.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ProfileTui {
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
#[serde(default)]
|
||||
pub session_picker_view: Option<SessionPickerViewMode>,
|
||||
}
|
||||
|
||||
impl From<ConfigProfile> for codex_app_server_protocol::Profile {
|
||||
fn from(config_profile: ConfigProfile) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -106,6 +106,8 @@ pub struct TuiGlobalKeymap {
|
||||
pub toggle_vim_mode: Option<KeybindingsSpec>,
|
||||
/// Toggle Fast mode.
|
||||
pub toggle_fast_mode: Option<KeybindingsSpec>,
|
||||
/// Toggle raw scrollback mode for copy-friendly transcript selection.
|
||||
pub toggle_raw_output: Option<KeybindingsSpec>,
|
||||
}
|
||||
|
||||
/// Chat context keybindings.
|
||||
|
||||
@@ -57,6 +57,30 @@ const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Preferred layout for the resume/fork session picker.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Copy, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SessionPickerViewMode {
|
||||
Comfortable,
|
||||
#[default]
|
||||
Dense,
|
||||
}
|
||||
|
||||
impl SessionPickerViewMode {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Comfortable => "comfortable",
|
||||
Self::Dense => "dense",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionPickerViewMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine where Codex should store CLI auth credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -617,6 +641,11 @@ pub struct Tui {
|
||||
#[serde(default)]
|
||||
pub vim_mode_default: bool,
|
||||
|
||||
/// Start the TUI in raw scrollback mode for copy-friendly transcript output.
|
||||
/// Defaults to `false`.
|
||||
#[serde(default)]
|
||||
pub raw_output_mode: bool,
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
///
|
||||
/// - `auto` (default): Disable alternate screen in Zellij, enable elsewhere.
|
||||
@@ -656,6 +685,10 @@ pub struct Tui {
|
||||
#[serde(default)]
|
||||
pub theme: Option<String>,
|
||||
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
#[serde(default)]
|
||||
pub session_picker_view: Option<SessionPickerViewMode>,
|
||||
|
||||
/// Keybinding overrides for the TUI.
|
||||
///
|
||||
/// This supports rebinding selected actions globally and by context.
|
||||
|
||||
@@ -18,6 +18,7 @@ pub use codex_config::types::ModelAvailabilityNuxConfig;
|
||||
pub use codex_config::types::Notice;
|
||||
pub use codex_config::types::OAuthCredentialsStoreMode;
|
||||
pub use codex_config::types::OtelConfig;
|
||||
pub use codex_config::types::SessionPickerViewMode;
|
||||
pub use codex_config::types::ToolSuggestConfig;
|
||||
pub use codex_config::types::TuiKeymap;
|
||||
pub use codex_config::types::TuiNotificationSettings;
|
||||
@@ -30,6 +31,7 @@ pub use codex_core::StartThreadOptions;
|
||||
pub use codex_core::StateDbHandle;
|
||||
pub use codex_core::ThreadManager;
|
||||
pub use codex_core::ThreadShutdownReport;
|
||||
pub use codex_core::agent_graph_store_from_state_db;
|
||||
pub use codex_core::config::Config;
|
||||
pub use codex_core::config::Constrained;
|
||||
pub use codex_core::config::GhostSnapshotConfig;
|
||||
@@ -39,6 +41,7 @@ pub use codex_core::config::TerminalResizeReflowConfig;
|
||||
pub use codex_core::config::ThreadStoreConfig;
|
||||
pub use codex_core::config::find_codex_home;
|
||||
pub use codex_core::init_state_db;
|
||||
pub use codex_core::init_state_db_from_config;
|
||||
pub use codex_core::skills::SkillsManager;
|
||||
pub use codex_core::thread_store_from_config;
|
||||
pub use codex_exec_server::EnvironmentManager;
|
||||
|
||||
@@ -26,6 +26,7 @@ bm25 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-agent-graph-store = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-apply-patch = { workspace = true }
|
||||
|
||||
@@ -674,6 +674,15 @@
|
||||
"tools_view_image": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tui": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ProfileTui"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "TUI settings scoped to this profile."
|
||||
},
|
||||
"web_search": {
|
||||
"$ref": "#/definitions/WebSearchMode"
|
||||
},
|
||||
@@ -999,6 +1008,9 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"trusted_hash": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1873,6 +1885,22 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileTui": {
|
||||
"additionalProperties": false,
|
||||
"description": "TUI settings supported inside a named profile.",
|
||||
"properties": {
|
||||
"session_picker_view": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionPickerViewMode"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Preferred layout for resume/fork session picker results."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProjectConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2160,6 +2188,14 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionPickerViewMode": {
|
||||
"description": "Preferred layout for the resume/fork session picker.",
|
||||
"enum": [
|
||||
"comfortable",
|
||||
"dense"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ShellEnvironmentPolicyInherit": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2460,6 +2496,7 @@
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_raw_output": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
},
|
||||
@@ -2558,6 +2595,20 @@
|
||||
"default": true,
|
||||
"description": "Enable desktop notifications from the TUI. Defaults to `true`."
|
||||
},
|
||||
"raw_output_mode": {
|
||||
"default": false,
|
||||
"description": "Start the TUI in raw scrollback mode for copy-friendly transcript output. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_picker_view": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionPickerViewMode"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Preferred layout for resume/fork session picker results."
|
||||
},
|
||||
"show_tooltips": {
|
||||
"default": true,
|
||||
"description": "Show startup tooltips in the TUI welcome screen. Defaults to `true`.",
|
||||
@@ -2956,6 +3007,14 @@
|
||||
],
|
||||
"description": "Toggle Fast mode."
|
||||
},
|
||||
"toggle_raw_output": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Toggle raw scrollback mode for copy-friendly transcript selection."
|
||||
},
|
||||
"toggle_shortcuts": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -3062,6 +3121,7 @@
|
||||
"queue": null,
|
||||
"submit": null,
|
||||
"toggle_fast_mode": null,
|
||||
"toggle_raw_output": null,
|
||||
"toggle_shortcuts": null,
|
||||
"toggle_vim_mode": null
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::ReadThreadParams;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -296,7 +295,6 @@ impl AgentControl {
|
||||
state.notify_thread_created(new_thread.thread_id);
|
||||
|
||||
self.persist_thread_spawn_edge_for_source(
|
||||
new_thread.thread.as_ref(),
|
||||
new_thread.thread_id,
|
||||
notification_source.as_ref(),
|
||||
)
|
||||
@@ -449,19 +447,14 @@ impl AgentControl {
|
||||
))
|
||||
.await?;
|
||||
let state = self.upgrade()?;
|
||||
let Ok(resumed_thread) = state.get_thread(resumed_thread_id).await else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
let Some(state_db_ctx) = resumed_thread.state_db() else {
|
||||
return Ok(resumed_thread_id);
|
||||
};
|
||||
let agent_graph_store = state.agent_graph_store();
|
||||
|
||||
let mut resume_queue = VecDeque::from([(thread_id, root_depth)]);
|
||||
while let Some((parent_thread_id, parent_depth)) = resume_queue.pop_front() {
|
||||
let child_ids = match state_db_ctx
|
||||
.list_thread_spawn_children_with_status(
|
||||
let child_ids = match agent_graph_store
|
||||
.list_thread_spawn_children(
|
||||
parent_thread_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
Some(codex_agent_graph_store::ThreadSpawnEdgeStatus::Open),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -525,7 +518,6 @@ impl AgentControl {
|
||||
let _ = config.features.disable(Feature::Collab);
|
||||
}
|
||||
let state = self.upgrade()?;
|
||||
let state_db_ctx = state.state_db();
|
||||
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
|
||||
let (session_source, agent_metadata) = match session_source {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
@@ -535,14 +527,11 @@ impl AgentControl {
|
||||
agent_role: _,
|
||||
agent_nickname: _,
|
||||
}) => {
|
||||
let state_db_ctx = state.state_db();
|
||||
let (resumed_agent_nickname, resumed_agent_role) =
|
||||
if let Some(state_db_ctx) = state_db_ctx.as_ref() {
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
match state_db_ctx.get_thread(thread_id).await {
|
||||
Ok(Some(metadata)) => (metadata.agent_nickname, metadata.agent_role),
|
||||
Ok(None) | Err(_) => (None, None),
|
||||
};
|
||||
self.prepare_thread_spawn(
|
||||
&mut reservation,
|
||||
@@ -609,7 +598,6 @@ impl AgentControl {
|
||||
);
|
||||
}
|
||||
self.persist_thread_spawn_edge_for_source(
|
||||
resumed_thread.thread.as_ref(),
|
||||
resumed_thread.thread_id,
|
||||
Some(¬ification_source),
|
||||
)
|
||||
@@ -722,11 +710,13 @@ impl AgentControl {
|
||||
/// agent and any live descendants reached from the in-memory tree.
|
||||
pub(crate) async fn close_agent(&self, agent_id: ThreadId) -> CodexResult<String> {
|
||||
let state = self.upgrade()?;
|
||||
if let Ok(thread) = state.get_thread(agent_id).await
|
||||
&& let Some(state_db_ctx) = thread.state_db()
|
||||
&& let Err(err) = state_db_ctx
|
||||
.set_thread_spawn_edge_status(agent_id, DirectionalThreadSpawnEdgeStatus::Closed)
|
||||
.await
|
||||
if let Err(err) = state
|
||||
.agent_graph_store()
|
||||
.set_thread_spawn_edge_status(
|
||||
agent_id,
|
||||
codex_agent_graph_store::ThreadSpawnEdgeStatus::Closed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("failed to persist thread-spawn edge status for {agent_id}: {err}");
|
||||
}
|
||||
@@ -1142,21 +1132,21 @@ impl AgentControl {
|
||||
|
||||
async fn persist_thread_spawn_edge_for_source(
|
||||
&self,
|
||||
thread: &crate::CodexThread,
|
||||
child_thread_id: ThreadId,
|
||||
session_source: Option<&SessionSource>,
|
||||
) {
|
||||
let Some(parent_thread_id) = session_source.and_then(thread_spawn_parent_thread_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(state_db_ctx) = thread.state_db() else {
|
||||
let Ok(state) = self.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = state_db_ctx
|
||||
if let Err(err) = state
|
||||
.agent_graph_store()
|
||||
.upsert_thread_spawn_edge(
|
||||
parent_thread_id,
|
||||
child_thread_id,
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
codex_agent_graph_store::ThreadSpawnEdgeStatus::Open,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::*;
|
||||
use crate::CodexThread;
|
||||
use crate::StateDbHandle;
|
||||
use crate::ThreadManager;
|
||||
use crate::agent::agent_status_from_event;
|
||||
use crate::config::AgentRoleConfig;
|
||||
@@ -8,7 +7,6 @@ use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::SubagentNotification;
|
||||
use crate::init_state_db;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -86,7 +84,6 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
|
||||
struct AgentControlHarness {
|
||||
_home: TempDir,
|
||||
config: Config,
|
||||
state_db: Option<StateDbHandle>,
|
||||
manager: ThreadManager,
|
||||
control: AgentControl,
|
||||
}
|
||||
@@ -94,19 +91,17 @@ struct AgentControlHarness {
|
||||
impl AgentControlHarness {
|
||||
async fn new() -> Self {
|
||||
let (home, config) = test_config().await;
|
||||
let state_db = init_state_db(&config).await;
|
||||
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
state_db.clone(),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
Self {
|
||||
_home: home,
|
||||
config,
|
||||
state_db,
|
||||
manager,
|
||||
control,
|
||||
}
|
||||
@@ -955,7 +950,8 @@ async fn spawn_agent_respects_max_threads_limit() {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
|
||||
let _ = manager
|
||||
@@ -1007,7 +1003,8 @@ async fn spawn_agent_releases_slot_after_shutdown() {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
|
||||
let first_agent_id = control
|
||||
@@ -1050,7 +1047,8 @@ async fn spawn_agent_limit_shared_across_clones() {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
let cloned = control.clone();
|
||||
|
||||
@@ -1095,7 +1093,8 @@ async fn resume_agent_respects_max_threads_limit() {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
|
||||
let resumable_id = control
|
||||
@@ -1151,7 +1150,8 @@ async fn resume_agent_releases_slot_after_resume_failure() {
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
|
||||
let _ = control
|
||||
@@ -1543,19 +1543,17 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() {
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow sqlite");
|
||||
let state_db = init_state_db(&config).await;
|
||||
let manager = ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
state_db.clone(),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
let control = manager.agent_control();
|
||||
let harness = AgentControlHarness {
|
||||
_home: home,
|
||||
config,
|
||||
state_db,
|
||||
manager,
|
||||
control,
|
||||
};
|
||||
@@ -1706,7 +1704,12 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
|
||||
.expect("child shutdown should succeed");
|
||||
let store = LocalThreadStore::new(
|
||||
LocalThreadStoreConfig::from_config(&harness.config),
|
||||
harness.state_db.clone(),
|
||||
codex_state::StateRuntime::init(
|
||||
harness.config.sqlite_home.clone(),
|
||||
harness.config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize"),
|
||||
);
|
||||
store
|
||||
.archive_thread(ArchiveThreadParams {
|
||||
|
||||
@@ -95,6 +95,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
parent_trace: None,
|
||||
environment_selections: parent_ctx.environments.clone(),
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
state_db: parent_session.services.state_db.clone(),
|
||||
thread_store: Arc::clone(&parent_session.services.thread_store),
|
||||
}))
|
||||
.or_cancel(&cancel_token)
|
||||
|
||||
@@ -221,9 +221,14 @@ impl CodexThread {
|
||||
&self,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
mcp_elicitations_auto_deny: bool,
|
||||
) -> ConstraintResult<()> {
|
||||
self.codex
|
||||
.set_app_server_client_info(app_server_client_name, app_server_client_version)
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ use codex_config::types::NotificationCondition;
|
||||
use codex_config::types::NotificationMethod;
|
||||
use codex_config::types::Notifications;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
@@ -550,11 +551,13 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
vim_mode_default: false,
|
||||
raw_output_mode: false,
|
||||
alternate_screen: AltScreenMode::default(),
|
||||
status_line: None,
|
||||
status_line_use_colors: true,
|
||||
terminal_title: None,
|
||||
theme: None,
|
||||
session_picker_view: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig {
|
||||
shown_count: HashMap::from([
|
||||
@@ -660,6 +663,53 @@ fn test_tui_vim_mode_default_true() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tui_raw_output_mode_defaults_to_false() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
"#;
|
||||
let parsed: ConfigToml = toml::from_str(toml).expect("deserialize empty [tui] table");
|
||||
assert!(
|
||||
!parsed
|
||||
.tui
|
||||
.expect("config should include tui section")
|
||||
.raw_output_mode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tui_raw_output_mode_true() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
raw_output_mode = true
|
||||
"#;
|
||||
let parsed: ConfigToml = toml::from_str(toml).expect("deserialize raw_output_mode=true");
|
||||
assert!(
|
||||
parsed
|
||||
.tui
|
||||
.expect("config should include tui section")
|
||||
.raw_output_mode
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_config_uses_tui_raw_output_mode() {
|
||||
let toml = r#"
|
||||
[tui]
|
||||
raw_output_mode = true
|
||||
"#;
|
||||
let cfg_toml: ConfigToml = toml::from_str(toml).expect("deserialize raw_output_mode=true");
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
cfg_toml,
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
|
||||
assert!(cfg.tui_raw_output_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_deserializes_permission_profiles() {
|
||||
let toml = r#"
|
||||
@@ -2108,6 +2158,31 @@ fn tui_theme_defaults_to_none() {
|
||||
assert_eq!(parsed.tui.as_ref().and_then(|t| t.theme.as_deref()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_session_picker_view_deserializes_from_toml() {
|
||||
let cfg = r#"
|
||||
[tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
let parsed = toml::from_str::<ConfigToml>(cfg).expect("TOML deserialization should succeed");
|
||||
assert_eq!(
|
||||
parsed.tui.as_ref().and_then(|t| t.session_picker_view),
|
||||
Some(SessionPickerViewMode::Dense),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_session_picker_view_defaults_to_none() {
|
||||
let cfg = r#"
|
||||
[tui]
|
||||
"#;
|
||||
let parsed = toml::from_str::<ConfigToml>(cfg).expect("TOML deserialization should succeed");
|
||||
assert_eq!(
|
||||
parsed.tui.as_ref().and_then(|t| t.session_picker_view),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
let cfg = r#"
|
||||
@@ -2125,11 +2200,13 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
vim_mode_default: false,
|
||||
raw_output_mode: false,
|
||||
alternate_screen: AltScreenMode::Auto,
|
||||
status_line: None,
|
||||
status_line_use_colors: true,
|
||||
terminal_title: None,
|
||||
theme: None,
|
||||
session_picker_view: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow_max_rows: None,
|
||||
@@ -2195,6 +2272,78 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_tui_rejects_unsupported_settings() {
|
||||
let err = toml::from_str::<ConfigToml>(
|
||||
r#"profile = "work"
|
||||
|
||||
[profiles.work.tui]
|
||||
theme = "dark"
|
||||
"#,
|
||||
)
|
||||
.expect_err("profile TUI config should only accept supported fields");
|
||||
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
assert!(err.to_string().contains("theme"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_config_resolves_session_picker_view_default_and_override() {
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load default config");
|
||||
|
||||
assert_eq!(cfg.tui_session_picker_view, SessionPickerViewMode::Dense);
|
||||
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
tui: Some(Tui {
|
||||
session_picker_view: Some(SessionPickerViewMode::Comfortable),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load root override config");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tui_session_picker_view,
|
||||
SessionPickerViewMode::Comfortable
|
||||
);
|
||||
|
||||
let cfg_toml = toml::from_str::<ConfigToml>(
|
||||
r#"profile = "work"
|
||||
|
||||
[tui]
|
||||
session_picker_view = "dense"
|
||||
|
||||
[profiles.work.tui]
|
||||
session_picker_view = "comfortable"
|
||||
"#,
|
||||
)
|
||||
.expect("parse profile scoped tui config");
|
||||
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
cfg_toml,
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load profile override config");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tui_session_picker_view,
|
||||
SessionPickerViewMode::Comfortable
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sandbox_config_parsing() {
|
||||
let sandbox_full_access = r#"
|
||||
@@ -6450,6 +6599,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
tui_raw_output_mode: false,
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
@@ -6461,6 +6611,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
},
|
||||
o3_profile_config
|
||||
@@ -6652,6 +6803,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
tui_raw_output_mode: false,
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
@@ -6663,6 +6815,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -6808,6 +6961,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
tui_raw_output_mode: false,
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
@@ -6819,6 +6973,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -6949,6 +7104,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
tui_raw_output_mode: false,
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
@@ -6960,6 +7116,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::path_utils::write_atomically;
|
||||
use anyhow::Context;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -91,6 +92,14 @@ pub fn syntax_theme_edit(name: &str) -> ConfigEdit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a config edit that sets `[tui].session_picker_view = "<mode>"`.
|
||||
pub fn session_picker_view_edit(mode: SessionPickerViewMode) -> ConfigEdit {
|
||||
ConfigEdit::SetPath {
|
||||
segments: vec!["tui".to_string(), "session_picker_view".to_string()],
|
||||
value: value(mode.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a config edit that sets `[tui].status_line` to an explicit ordered list.
|
||||
///
|
||||
/// The array is written even when it is empty so "hide the status line" stays
|
||||
@@ -1316,6 +1325,25 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self {
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"tui".to_string(),
|
||||
"session_picker_view".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["tui".to_string(), "session_picker_view".to_string()]
|
||||
};
|
||||
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
value: value(mode.to_string()),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_edits<I>(mut self, edits: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = ConfigEdit>,
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(unix)]
|
||||
@@ -48,6 +49,41 @@ fn builder_with_edits_applies_custom_paths() {
|
||||
assert_eq!(contents, "enabled = true\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_picker_view_edit_writes_root_tui_setting() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([session_picker_view_edit(SessionPickerViewMode::Dense)])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_picker_view_builder_respects_active_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_profile(Some("work"))
|
||||
.set_session_picker_view(SessionPickerViewMode::Dense)
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[profiles.work.tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_binding_edit_writes_root_action_binding() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -49,6 +49,7 @@ use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::OtelConfig;
|
||||
use codex_config::types::OtelConfigToml;
|
||||
use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
@@ -517,6 +518,9 @@ pub struct Config {
|
||||
/// Start the composer in Vim mode (`Normal`) by default.
|
||||
pub tui_vim_mode_default: bool,
|
||||
|
||||
/// Start the TUI in raw scrollback mode for copy-friendly transcript output.
|
||||
pub tui_raw_output_mode: bool,
|
||||
|
||||
/// Start the TUI in the specified collaboration mode (plan/default).
|
||||
|
||||
/// Controls whether the TUI uses the terminal's alternate screen buffer.
|
||||
@@ -544,6 +548,9 @@ pub struct Config {
|
||||
/// Syntax highlighting theme override (kebab-case name).
|
||||
pub tui_theme: Option<String>,
|
||||
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
pub tui_session_picker_view: SessionPickerViewMode,
|
||||
|
||||
/// Terminal resize-reflow tuning knobs.
|
||||
pub terminal_resize_reflow: TerminalResizeReflowConfig,
|
||||
|
||||
@@ -3147,6 +3154,11 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(|t| t.vim_mode_default)
|
||||
.unwrap_or(false),
|
||||
tui_raw_output_mode: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|t| t.raw_output_mode)
|
||||
.unwrap_or(false),
|
||||
tui_alternate_screen: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
@@ -3160,6 +3172,12 @@ impl Config {
|
||||
.unwrap_or(true),
|
||||
tui_terminal_title: cfg.tui.as_ref().and_then(|t| t.terminal_title.clone()),
|
||||
tui_theme: cfg.tui.as_ref().and_then(|t| t.theme.clone()),
|
||||
tui_session_picker_view: config_profile
|
||||
.tui
|
||||
.as_ref()
|
||||
.and_then(|t| t.session_picker_view)
|
||||
.or_else(|| cfg.tui.as_ref().and_then(|t| t.session_picker_view))
|
||||
.unwrap_or_default(),
|
||||
terminal_resize_reflow,
|
||||
tui_keymap: cfg
|
||||
.tui
|
||||
|
||||
@@ -29,7 +29,6 @@ use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::validate_thread_goal_objective;
|
||||
use codex_rollout::state_db::reconcile_rollout;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_utils_template::Template;
|
||||
use futures::future::BoxFuture;
|
||||
use std::sync::Arc;
|
||||
@@ -1338,17 +1337,6 @@ impl Session {
|
||||
state_db
|
||||
} else if let Some(state_db) = self.goal_runtime.state_db.lock().await.clone() {
|
||||
state_db
|
||||
} else if let Some(local_store) = self
|
||||
.services
|
||||
.thread_store
|
||||
.as_any()
|
||||
.downcast_ref::<LocalThreadStore>()
|
||||
{
|
||||
local_store.state_db().await.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"thread goals require a local persisted thread with a state database"
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
anyhow::bail!("thread goals require a local persisted thread with a state database");
|
||||
};
|
||||
|
||||
@@ -124,7 +124,9 @@ pub use thread_manager::NewThread;
|
||||
pub use thread_manager::StartThreadOptions;
|
||||
pub use thread_manager::ThreadManager;
|
||||
pub use thread_manager::ThreadShutdownReport;
|
||||
pub use thread_manager::agent_graph_store_from_state_db;
|
||||
pub use thread_manager::build_models_manager;
|
||||
pub use thread_manager::init_state_db_from_config;
|
||||
pub use thread_manager::thread_store_from_config;
|
||||
pub use web_search::web_search_action_detail;
|
||||
pub use web_search::web_search_detail;
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::hooks::trusted_config_layer_stack;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -163,13 +164,24 @@ print({hook_output:?})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write hooks.json");
|
||||
let hook_list = codex_hooks::list_hooks(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
..HooksConfig::default()
|
||||
});
|
||||
assert_eq!(hook_list.hooks.len(), 1);
|
||||
let trusted_config_layer_stack = trusted_config_layer_stack(
|
||||
&turn_context.config.config_layer_stack,
|
||||
&turn_context.config.codex_home,
|
||||
hook_list.hooks,
|
||||
);
|
||||
|
||||
session
|
||||
.services
|
||||
.hooks
|
||||
.store(Arc::new(Hooks::new(HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(turn_context.config.config_layer_stack.clone()),
|
||||
config_layer_stack: Some(trusted_config_layer_stack),
|
||||
shell_program: (!cfg!(windows)).then_some("/bin/sh".to_string()),
|
||||
shell_args: if cfg!(windows) {
|
||||
Vec::new()
|
||||
|
||||
@@ -25,7 +25,7 @@ pub enum PersonalityMigrationStatus {
|
||||
pub async fn maybe_migrate_personality(
|
||||
codex_home: &Path,
|
||||
config_toml: &ConfigToml,
|
||||
state_db: Option<StateDbHandle>,
|
||||
state_db: StateDbHandle,
|
||||
) -> io::Result<PersonalityMigrationStatus> {
|
||||
let marker_path = codex_home.join(PERSONALITY_MIGRATION_FILENAME);
|
||||
if tokio::fs::try_exists(&marker_path).await? {
|
||||
@@ -65,16 +65,13 @@ pub async fn maybe_migrate_personality(
|
||||
async fn has_recorded_sessions(
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
state_db: Option<StateDbHandle>,
|
||||
state_db: StateDbHandle,
|
||||
) -> io::Result<bool> {
|
||||
let store = LocalThreadStore::new(
|
||||
LocalThreadStoreConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
sqlite_home: codex_home.to_path_buf(),
|
||||
default_model_provider_id: default_provider.to_string(),
|
||||
},
|
||||
state_db,
|
||||
);
|
||||
let config = LocalThreadStoreConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
default_model_provider_id: default_provider.to_string(),
|
||||
};
|
||||
let store = LocalThreadStore::new(config, state_db);
|
||||
if has_threads(&store, /*archived*/ false).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
use codex_rollout::RolloutConfig;
|
||||
use codex_rollout::SESSIONS_SUBDIR;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_state::state_db_path;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -20,6 +23,26 @@ async fn read_config_toml(codex_home: &Path) -> io::Result<ConfigToml> {
|
||||
toml::from_str(&contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
|
||||
}
|
||||
|
||||
async fn state_db_for_test(codex_home: &Path) -> io::Result<StateDbHandle> {
|
||||
state_db_for_test_with_sqlite_home(codex_home, codex_home).await
|
||||
}
|
||||
|
||||
async fn state_db_for_test_with_sqlite_home(
|
||||
codex_home: &Path,
|
||||
sqlite_home: &Path,
|
||||
) -> io::Result<StateDbHandle> {
|
||||
let config = RolloutConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
sqlite_home: sqlite_home.to_path_buf(),
|
||||
cwd: codex_home.to_path_buf(),
|
||||
model_provider_id: "openai".to_string(),
|
||||
generate_memories: false,
|
||||
};
|
||||
codex_rollout::state_db::try_init(&config)
|
||||
.await
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
|
||||
async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> {
|
||||
let thread_id = ThreadId::new();
|
||||
let dir = codex_home
|
||||
@@ -87,7 +110,8 @@ async fn applies_when_sessions_exist_and_no_personality() -> io::Result<()> {
|
||||
write_session_with_user_event(temp.path()).await?;
|
||||
|
||||
let config_toml = ConfigToml::default();
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
let state_db = state_db_for_test(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
@@ -103,7 +127,8 @@ async fn applies_when_only_archived_sessions_exist_and_no_personality() -> io::R
|
||||
write_archived_session_with_user_event(temp.path()).await?;
|
||||
|
||||
let config_toml = ConfigToml::default();
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
let state_db = state_db_for_test(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
@@ -119,7 +144,8 @@ async fn skips_when_marker_exists() -> io::Result<()> {
|
||||
create_marker(&temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?;
|
||||
|
||||
let config_toml = ConfigToml::default();
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
let state_db = state_db_for_test(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedMarker);
|
||||
assert!(!temp.path().join("config.toml").exists());
|
||||
@@ -136,7 +162,8 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
|
||||
.map_err(|err| io::Error::other(format!("failed to write config: {err}")))?;
|
||||
|
||||
let config_toml = read_config_toml(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
let state_db = state_db_for_test(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
@@ -153,10 +180,37 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
|
||||
async fn skips_when_no_sessions() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let config_toml = ConfigToml::default();
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?;
|
||||
let state_db = state_db_for_test(temp.path()).await?;
|
||||
let status = maybe_migrate_personality(temp.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions);
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
assert!(!temp.path().join("config.toml").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn uses_configured_sqlite_home_when_checking_for_sessions() -> io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let sqlite_home = TempDir::new()?;
|
||||
write_session_with_user_event(codex_home.path()).await?;
|
||||
|
||||
let config_toml = ConfigToml::default();
|
||||
let state_db =
|
||||
state_db_for_test_with_sqlite_home(codex_home.path(), sqlite_home.path()).await?;
|
||||
let status = maybe_migrate_personality(codex_home.path(), &config_toml, state_db).await?;
|
||||
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
.join(PERSONALITY_MIGRATION_FILENAME)
|
||||
.exists()
|
||||
);
|
||||
|
||||
let persisted = read_config_toml(codex_home.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert!(!state_db_path(codex_home.path()).exists());
|
||||
assert!(state_db_path(sqlite_home.path()).exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ use crate::config::Config;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn::build_prompt;
|
||||
use crate::session::turn::built_tools;
|
||||
use crate::state_db_bridge::StateDbHandle;
|
||||
use crate::thread_manager::ThreadManager;
|
||||
use crate::thread_manager::agent_graph_store_from_state_db;
|
||||
use crate::thread_manager::init_state_db_from_config;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
|
||||
/// Build the model-visible `input` list for a single debug turn.
|
||||
@@ -25,7 +26,6 @@ use crate::thread_manager::thread_store_from_config;
|
||||
pub async fn build_prompt_input(
|
||||
mut config: Config,
|
||||
input: Vec<UserInput>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> CodexResult<Vec<ResponseItem>> {
|
||||
config.ephemeral = true;
|
||||
|
||||
@@ -37,15 +37,20 @@ pub async fn build_prompt_input(
|
||||
config.codex_linux_sandbox_exe.clone(),
|
||||
)?;
|
||||
|
||||
let state_db = init_state_db_from_config(&config)
|
||||
.await
|
||||
.ok_or_else(|| std::io::Error::other("prompt debug requires state db"))?;
|
||||
let thread_store = thread_store_from_config(&config, state_db.clone());
|
||||
let agent_graph_store = agent_graph_store_from_state_db(state_db.clone());
|
||||
let thread_manager = ThreadManager::new(
|
||||
&config,
|
||||
Arc::clone(&auth_manager),
|
||||
SessionSource::Exec,
|
||||
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await),
|
||||
/*analytics_events_client*/ None,
|
||||
state_db,
|
||||
thread_store,
|
||||
state_db.clone(),
|
||||
agent_graph_store,
|
||||
);
|
||||
let thread = thread_manager.start_thread(config).await?;
|
||||
|
||||
|
||||
@@ -254,6 +254,10 @@ impl Session {
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
{
|
||||
let current_manager = self.services.mcp_connection_manager.read().await;
|
||||
refreshed_manager.set_elicitations_auto_deny(current_manager.elicitations_auto_deny());
|
||||
}
|
||||
{
|
||||
let mut guard = self.services.mcp_startup_cancellation_token.lock().await;
|
||||
if guard.is_cancelled() {
|
||||
|
||||
@@ -132,7 +132,6 @@ use codex_terminal_detection::user_agent;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::LiveThread;
|
||||
use codex_thread_store::LiveThreadInitGuard;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::ResumeThreadParams;
|
||||
use codex_thread_store::ThreadEventPersistenceMode;
|
||||
use codex_thread_store::ThreadPersistenceMetadata;
|
||||
@@ -356,7 +355,6 @@ use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
use codex_tools::ToolsConfig;
|
||||
use codex_tools::ToolsConfigParams;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -412,6 +410,7 @@ pub(crate) struct CodexSpawnArgs {
|
||||
pub(crate) parent_trace: Option<W3cTraceContext>,
|
||||
pub(crate) environment_selections: ResolvedTurnEnvironments,
|
||||
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
pub(crate) state_db: Option<state_db::StateDbHandle>,
|
||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||
}
|
||||
|
||||
@@ -469,6 +468,7 @@ impl Codex {
|
||||
parent_trace: _,
|
||||
environment_selections,
|
||||
analytics_events_client,
|
||||
state_db,
|
||||
thread_store,
|
||||
} = args;
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
@@ -557,15 +557,7 @@ impl Codex {
|
||||
};
|
||||
match thread_id {
|
||||
Some(thread_id) => {
|
||||
let state_db_ctx = if config.ephemeral {
|
||||
None
|
||||
} else if let Some(local_store) =
|
||||
thread_store.as_any().downcast_ref::<LocalThreadStore>()
|
||||
{
|
||||
local_store.state_db().await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let state_db_ctx = state_db.clone();
|
||||
state_db::get_dynamic_tools(state_db_ctx.as_deref(), thread_id, "codex_spawn")
|
||||
.await
|
||||
}
|
||||
@@ -651,6 +643,7 @@ impl Codex {
|
||||
agent_control,
|
||||
environment_manager,
|
||||
analytics_events_client,
|
||||
state_db,
|
||||
thread_store,
|
||||
parent_rollout_thread_trace,
|
||||
)
|
||||
@@ -758,6 +751,7 @@ impl Codex {
|
||||
&self,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
mcp_elicitations_auto_deny: bool,
|
||||
) -> ConstraintResult<()> {
|
||||
self.session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
@@ -765,7 +759,10 @@ impl Codex {
|
||||
app_server_client_version,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.await?;
|
||||
let mcp_connection_manager = self.session.services.mcp_connection_manager.read().await;
|
||||
mcp_connection_manager.set_elicitations_auto_deny(mcp_elicitations_auto_deny);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn agent_status(&self) -> AgentStatus {
|
||||
@@ -1307,7 +1304,7 @@ impl Session {
|
||||
self.services.user_shell.as_ref().clone(),
|
||||
self.services.shell_snapshot_tx.clone(),
|
||||
self.services.session_telemetry.clone(),
|
||||
self.services.state_db.clone(),
|
||||
self.state_db(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -343,6 +343,7 @@ impl Session {
|
||||
agent_control: AgentControl,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
state_db: Option<state_db::StateDbHandle>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
parent_rollout_thread_trace: ThreadTraceContext,
|
||||
) -> anyhow::Result<Arc<Self>> {
|
||||
@@ -441,22 +442,7 @@ impl Session {
|
||||
otel.name = "session_init.thread_persistence",
|
||||
session_init.ephemeral = config.ephemeral,
|
||||
));
|
||||
let state_db_fut = async {
|
||||
if config.ephemeral {
|
||||
None
|
||||
} else if let Some(local_store) =
|
||||
thread_store.as_any().downcast_ref::<LocalThreadStore>()
|
||||
{
|
||||
local_store.state_db().await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.instrument(info_span!(
|
||||
"session_init.state_db",
|
||||
otel.name = "session_init.state_db",
|
||||
session_init.ephemeral = config.ephemeral,
|
||||
));
|
||||
let state_db_ctx = if config.ephemeral { None } else { state_db };
|
||||
|
||||
let is_subagent = session_configuration.session_source.is_non_root_agent();
|
||||
let history_meta_fut = async {
|
||||
@@ -495,15 +481,9 @@ impl Session {
|
||||
// Join all independent futures.
|
||||
let (
|
||||
thread_persistence_result,
|
||||
state_db_ctx,
|
||||
(history_log_id, history_entry_count),
|
||||
(auth, mcp_servers, auth_statuses),
|
||||
) = tokio::join!(
|
||||
thread_persistence_fut,
|
||||
state_db_fut,
|
||||
history_meta_fut,
|
||||
auth_and_mcp_fut
|
||||
);
|
||||
) = tokio::join!(thread_persistence_fut, history_meta_fut, auth_and_mcp_fut);
|
||||
|
||||
let mut live_thread_init =
|
||||
LiveThreadInitGuard::new(thread_persistence_result.map_err(|e| {
|
||||
|
||||
+151
-103
@@ -66,9 +66,10 @@ use crate::tasks::execute_user_shell_command;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::GoalHandler;
|
||||
use crate::tools::handlers::CreateGoalHandler;
|
||||
use crate::tools::handlers::ExecCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::handlers::UnifiedExecHandler;
|
||||
use crate::tools::handlers::UpdateGoalHandler;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::router::ToolCallSource;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
@@ -866,7 +867,7 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)?;
|
||||
|
||||
let session = make_session_with_config(move |config| {
|
||||
let (session, _codex_home) = make_session_with_config(move |config| {
|
||||
let cwd = config.cwd.clone();
|
||||
config
|
||||
.permissions
|
||||
@@ -932,7 +933,7 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)?;
|
||||
|
||||
let session = make_session_with_config(move |config| {
|
||||
let (session, _codex_home) = make_session_with_config(move |config| {
|
||||
let cwd = config.cwd.clone();
|
||||
config
|
||||
.permissions
|
||||
@@ -1007,7 +1008,7 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any
|
||||
&permission_profile_for_sandbox_policy(&sandbox_policy),
|
||||
)?;
|
||||
|
||||
let session = make_session_with_config(move |config| {
|
||||
let (session, _codex_home) = make_session_with_config(move |config| {
|
||||
let cwd = config.cwd.clone();
|
||||
config
|
||||
.permissions
|
||||
@@ -1034,7 +1035,7 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R
|
||||
&permission_profile_for_sandbox_policy(&sandbox_policy),
|
||||
)?;
|
||||
|
||||
let (session, rx) = make_session_with_config_and_rx(move |config| {
|
||||
let (session, rx, _codex_home) = make_session_with_config_and_rx(move |config| {
|
||||
let cwd = config.cwd.clone();
|
||||
config
|
||||
.permissions
|
||||
@@ -1164,7 +1165,7 @@ async fn reload_user_config_layer_updates_effective_apps_config() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_refreshes_hooks() -> anyhow::Result<()> {
|
||||
let session = make_session_with_config(|config| {
|
||||
let (session, _codex_home) = make_session_with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::CodexHooks)
|
||||
@@ -1173,15 +1174,17 @@ async fn reload_user_config_layer_refreshes_hooks() -> anyhow::Result<()> {
|
||||
.await?;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[hooks]
|
||||
|
||||
[[hooks.SessionStart]]
|
||||
hooks = [{ type = "command", command = "python3 /tmp/user.py" }]
|
||||
"#,
|
||||
)?;
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let user_config: codex_config::TomlValue = serde_json::from_value(serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "python3 /tmp/user.py",
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}))?;
|
||||
|
||||
let request = codex_hooks::SessionStartRequest {
|
||||
session_id: session.conversation_id,
|
||||
@@ -1193,6 +1196,39 @@ hooks = [{ type = "command", command = "python3 /tmp/user.py" }]
|
||||
};
|
||||
assert!(session.hooks().preview_session_start(&request).is_empty());
|
||||
|
||||
let config = session.get_config().await;
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(
|
||||
config
|
||||
.config_layer_stack
|
||||
.with_user_config(&config_toml_path, user_config.clone()),
|
||||
),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert_eq!(hook_list.hooks.len(), 1);
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
|
||||
let trusted_user_config: codex_config::TomlValue = serde_json::from_value(serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "python3 /tmp/user.py",
|
||||
}],
|
||||
}],
|
||||
"state": {
|
||||
hook_list.hooks[0].key.clone(): {
|
||||
"trusted_hash": hook_list.hooks[0].current_hash.clone(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))?;
|
||||
std::fs::write(&config_toml_path, toml::to_string(&trusted_user_config)?)?;
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
assert_eq!(session.hooks().preview_session_start(&request).len(), 1);
|
||||
@@ -2946,15 +2982,13 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
fn turn_environments_for_tests(
|
||||
environment: &Arc<codex_exec_server::Environment>,
|
||||
cwd: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
) -> crate::environment_selection::ResolvedTurnEnvironments {
|
||||
crate::environment_selection::ResolvedTurnEnvironments {
|
||||
turn_environments: vec![TurnEnvironment {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
environment: Arc::clone(environment),
|
||||
cwd: cwd.clone(),
|
||||
shell: "bash".to_string(),
|
||||
}],
|
||||
}
|
||||
) -> Vec<TurnEnvironment> {
|
||||
vec![TurnEnvironment {
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
environment: Arc::clone(environment),
|
||||
cwd: cwd.clone(),
|
||||
shell: "bash".to_string(),
|
||||
}]
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3492,9 +3526,15 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
AgentControl::default(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
/*state_db*/ None,
|
||||
Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
/*state_db*/ None,
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize"),
|
||||
)),
|
||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
)
|
||||
@@ -3642,7 +3682,12 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
live_thread: None,
|
||||
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
/*state_db*/ None,
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize"),
|
||||
)),
|
||||
model_client: ModelClient::new(
|
||||
Some(auth_manager.clone()),
|
||||
@@ -3687,7 +3732,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
model_info,
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
crate::environment_selection::ResolvedTurnEnvironments { turn_environments },
|
||||
session_configuration.cwd.clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
@@ -3720,14 +3765,18 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
|
||||
async fn make_session_with_config(
|
||||
mutator: impl FnOnce(&mut Config),
|
||||
) -> anyhow::Result<Arc<Session>> {
|
||||
let (session, _rx_event) = make_session_with_config_and_rx(mutator).await?;
|
||||
Ok(session)
|
||||
) -> anyhow::Result<(Arc<Session>, tempfile::TempDir)> {
|
||||
let (session, _rx_event, codex_home) = make_session_with_config_and_rx(mutator).await?;
|
||||
Ok((session, codex_home))
|
||||
}
|
||||
|
||||
async fn make_session_with_config_and_rx(
|
||||
mutator: impl FnOnce(&mut Config),
|
||||
) -> anyhow::Result<(Arc<Session>, async_channel::Receiver<Event>)> {
|
||||
) -> anyhow::Result<(
|
||||
Arc<Session>,
|
||||
async_channel::Receiver<Event>,
|
||||
tempfile::TempDir,
|
||||
)> {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let mut config = build_test_config(codex_home.path()).await;
|
||||
mutator(&mut config);
|
||||
@@ -3812,15 +3861,21 @@ async fn make_session_with_config_and_rx(
|
||||
AgentControl::default(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
/*analytics_events_client*/ None,
|
||||
/*state_db*/ None,
|
||||
Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
/*state_db*/ None,
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize"),
|
||||
)),
|
||||
codex_rollout_trace::ThreadTraceContext::disabled(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((session, rx_event))
|
||||
Ok((session, rx_event, codex_home))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4482,7 +4537,7 @@ async fn default_turn_honors_empty_stored_thread_environments() {
|
||||
|
||||
let turn_context = session.new_default_turn().await;
|
||||
|
||||
assert!(turn_context.environments.primary().is_none());
|
||||
assert!(turn_context.environments.primary_environment().is_none());
|
||||
assert!(turn_context.environments.turn_environments.is_empty());
|
||||
assert_eq!(turn_context.cwd, session_cwd);
|
||||
assert_eq!(turn_context.config.cwd, session_cwd);
|
||||
@@ -4501,7 +4556,7 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
environment_id: "second".to_string(),
|
||||
environment: Arc::clone(&first_environment.environment),
|
||||
cwd: second_cwd.clone(),
|
||||
shell: first_environment.shell.clone(),
|
||||
shell: "bash".to_string(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -4544,7 +4599,7 @@ async fn empty_turn_environments_clear_primary_environment() {
|
||||
.await
|
||||
.expect("turn should start");
|
||||
|
||||
assert!(turn_context.environments.primary().is_none());
|
||||
assert!(turn_context.environments.primary_environment().is_none());
|
||||
assert!(turn_context.environments.turn_environments.is_empty());
|
||||
assert_eq!(turn_context.cwd, session.get_config().await.cwd);
|
||||
assert_eq!(turn_context.config.cwd, session.get_config().await.cwd);
|
||||
@@ -5000,47 +5055,13 @@ async fn make_session_and_context_with_auth_and_config_and_rx<F>(
|
||||
Arc<TurnContext>,
|
||||
async_channel::Receiver<Event>,
|
||||
)
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
make_session_and_context_with_auth_config_home_and_rx(
|
||||
auth,
|
||||
dynamic_tools,
|
||||
codex_home.path(),
|
||||
configure_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn make_session_and_context_with_auth_config_home_and_rx<F>(
|
||||
auth: CodexAuth,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
codex_home: &Path,
|
||||
configure_config: F,
|
||||
) -> (
|
||||
Arc<Session>,
|
||||
Arc<TurnContext>,
|
||||
async_channel::Receiver<Event>,
|
||||
)
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
let mut config = build_test_config(codex_home).await;
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir").keep();
|
||||
let mut config = build_test_config(codex_home.as_path()).await;
|
||||
configure_config(&mut config);
|
||||
let state_db = if config.features.enabled(Feature::Goals) {
|
||||
Some(
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("goal tests should initialize sqlite state db"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let config = Arc::new(config);
|
||||
let conversation_id = ThreadId::default();
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
@@ -5126,6 +5147,12 @@ where
|
||||
.expect("create environment"),
|
||||
);
|
||||
|
||||
let state_db = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let skills_watcher = Arc::new(SkillsWatcher::noop());
|
||||
let services = SessionServices {
|
||||
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
|
||||
@@ -5166,7 +5193,7 @@ where
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db.clone(),
|
||||
state_db: Some(state_db.clone()),
|
||||
live_thread: None,
|
||||
thread_store: Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(config.as_ref()),
|
||||
@@ -5215,7 +5242,7 @@ where
|
||||
model_info,
|
||||
&models_manager,
|
||||
/*network*/ None,
|
||||
turn_environments,
|
||||
crate::environment_selection::ResolvedTurnEnvironments { turn_environments },
|
||||
session_configuration.cwd.clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_outcome,
|
||||
@@ -5265,13 +5292,10 @@ async fn make_goal_session_and_context_with_rx() -> (
|
||||
Arc<Session>,
|
||||
Arc<TurnContext>,
|
||||
async_channel::Receiver<Event>,
|
||||
tempfile::TempDir,
|
||||
) {
|
||||
let codex_home = tempfile::tempdir().expect("create temp dir");
|
||||
let (session, turn_context, rx) = make_session_and_context_with_auth_config_home_and_rx(
|
||||
let (session, turn_context, rx) = make_session_and_context_with_auth_and_config_and_rx(
|
||||
CodexAuth::from_api_key("Test API Key"),
|
||||
Vec::new(),
|
||||
codex_home.path(),
|
||||
|config| {
|
||||
config
|
||||
.features
|
||||
@@ -5281,14 +5305,14 @@ async fn make_goal_session_and_context_with_rx() -> (
|
||||
)
|
||||
.await;
|
||||
upsert_goal_test_thread(session.as_ref()).await;
|
||||
(session, turn_context, rx, codex_home)
|
||||
(session, turn_context, rx)
|
||||
}
|
||||
|
||||
async fn upsert_goal_test_thread(session: &Session) {
|
||||
let config = session.get_config().await;
|
||||
let state_db = session
|
||||
.state_db()
|
||||
.expect("goal test session should have a state db");
|
||||
let state_db = goal_test_state_db(session)
|
||||
.await
|
||||
.expect("goal test state db should initialize");
|
||||
let mut builder = codex_state::ThreadMetadataBuilder::new(
|
||||
session.conversation_id,
|
||||
config
|
||||
@@ -7112,7 +7136,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupt_accounts_active_goal_before_pausing() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7376,7 +7400,7 @@ async fn goal_test_state_db(sess: &Session) -> anyhow::Result<crate::StateDbHand
|
||||
|
||||
#[tokio::test]
|
||||
async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyhow::Result<()> {
|
||||
let (sess, tc, rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, rx) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7476,7 +7500,7 @@ async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyh
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_goal_mutation_accounts_active_turn_before_status_change() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
sess.set_thread_goal(
|
||||
tc.as_ref(),
|
||||
SetGoalRequest {
|
||||
@@ -7543,7 +7567,7 @@ async fn external_goal_mutation_accounts_active_turn_before_status_change() -> a
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn external_active_goal_set_marks_current_turn_for_accounting() -> anyhow::Result<()> {
|
||||
let (sess, tc, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (sess, tc, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
sess.spawn_task(
|
||||
Arc::clone(&tc),
|
||||
Vec::new(),
|
||||
@@ -8210,9 +8234,9 @@ async fn sample_rollout(
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_goal_tool_rejects_existing_goal() {
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
let handler = CreateGoalHandler;
|
||||
|
||||
handler
|
||||
.handle(ToolInvocation {
|
||||
@@ -8272,11 +8296,12 @@ async fn create_goal_tool_rejects_existing_goal() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_tool_rejects_pausing_goal() {
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
let create_handler = CreateGoalHandler;
|
||||
let update_handler = UpdateGoalHandler;
|
||||
|
||||
handler
|
||||
create_handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
@@ -8296,7 +8321,7 @@ async fn update_goal_tool_rejects_pausing_goal() {
|
||||
.await
|
||||
.expect("initial create_goal should succeed");
|
||||
|
||||
let response = handler
|
||||
let response = update_handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
@@ -8332,11 +8357,12 @@ async fn update_goal_tool_rejects_pausing_goal() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_tool_marks_goal_complete() {
|
||||
let (session, turn_context, _rx, _codex_home) = make_goal_session_and_context_with_rx().await;
|
||||
let (session, turn_context, _rx) = make_goal_session_and_context_with_rx().await;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handler = GoalHandler;
|
||||
let create_handler = CreateGoalHandler;
|
||||
let update_handler = UpdateGoalHandler;
|
||||
|
||||
handler
|
||||
create_handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
@@ -8356,7 +8382,7 @@ async fn update_goal_tool_marks_goal_complete() {
|
||||
.await
|
||||
.expect("initial create_goal should succeed");
|
||||
|
||||
handler
|
||||
update_handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
@@ -8513,7 +8539,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
|
||||
let handler = UnifiedExecHandler;
|
||||
let handler = ExecCommandHandler;
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
@@ -8568,17 +8594,27 @@ async fn session_start_hooks_only_load_from_trusted_project_layers() -> std::io:
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let preview = preview_session_start_hooks(&config).await?;
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
let expected_source_path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(
|
||||
nested_dot_codex.join("hooks.json"),
|
||||
)?;
|
||||
assert_eq!(
|
||||
preview
|
||||
hook_list
|
||||
.hooks
|
||||
.iter()
|
||||
.map(|run| &run.source_path)
|
||||
.map(|hook| &hook.source_path)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![&expected_source_path],
|
||||
);
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
assert!(preview_session_start_hooks(&config).await?.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8618,11 +8654,23 @@ async fn session_start_hooks_require_project_trust_without_config_toml() -> std:
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let hook_list = codex_hooks::list_hooks(codex_hooks::HooksConfig {
|
||||
feature_enabled: true,
|
||||
config_layer_stack: Some(config.config_layer_stack.clone()),
|
||||
..codex_hooks::HooksConfig::default()
|
||||
});
|
||||
assert_eq!(
|
||||
preview_session_start_hooks(&config).await?.len(),
|
||||
hook_list.hooks.len(),
|
||||
expected_hooks,
|
||||
"unexpected hook count for {name}",
|
||||
"unexpected discovered hook count for {name}",
|
||||
);
|
||||
assert!(preview_session_start_hooks(&config).await?.is_empty());
|
||||
if expected_hooks == 1 {
|
||||
assert_eq!(
|
||||
hook_list.hooks[0].trust_status,
|
||||
codex_protocol::protocol::HookTrustStatus::Untrusted
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -498,7 +498,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
|
||||
let handler = UnifiedExecHandler;
|
||||
let handler = ExecCommandHandler;
|
||||
let resp = handler
|
||||
.handle(ToolInvocation {
|
||||
session: Arc::clone(&session),
|
||||
@@ -731,7 +731,12 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
let skills_watcher = Arc::new(SkillsWatcher::noop());
|
||||
let thread_store = Arc::new(codex_thread_store::LocalThreadStore::new(
|
||||
codex_thread_store::LocalThreadStoreConfig::from_config(&config),
|
||||
/*state_db*/ None,
|
||||
codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize"),
|
||||
));
|
||||
|
||||
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
|
||||
@@ -760,6 +765,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
turn_environments: Vec::new(),
|
||||
},
|
||||
analytics_events_client: None,
|
||||
state_db: None,
|
||||
thread_store,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
|
||||
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
use codex_sandboxing::policy_transforms::effective_network_sandbox_policy;
|
||||
use codex_tools::ToolEnvironmentMode;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
|
||||
@@ -138,11 +138,8 @@ pub(crate) async fn record_completed_response_item(
|
||||
.await;
|
||||
}
|
||||
mark_thread_memory_mode_polluted_if_external_context(sess, turn_context, item).await;
|
||||
let has_memory_citation = record_stage1_output_usage_and_detect_memory_citation(
|
||||
sess.services.state_db.as_ref(),
|
||||
item,
|
||||
)
|
||||
.await;
|
||||
let has_memory_citation =
|
||||
record_stage1_output_usage_and_detect_memory_citation(sess.state_db(), item).await;
|
||||
if has_memory_citation {
|
||||
sess.record_memory_citation_for_turn(&turn_context.sub_id)
|
||||
.await;
|
||||
@@ -177,7 +174,7 @@ pub(crate) async fn mark_thread_memory_mode_polluted_if_external_context(
|
||||
}
|
||||
|
||||
async fn record_stage1_output_usage_and_detect_memory_citation(
|
||||
state_db_ctx: Option<&state_db::StateDbHandle>,
|
||||
state_db_ctx: Option<state_db::StateDbHandle>,
|
||||
item: &ResponseItem,
|
||||
) -> bool {
|
||||
let Some(raw_text) = raw_assistant_output_text_from_item(item) else {
|
||||
|
||||
@@ -52,14 +52,14 @@ pub fn auth_manager_from_auth_with_home(auth: CodexAuth, codex_home: PathBuf) ->
|
||||
AuthManager::from_auth_for_testing_with_home(auth, codex_home)
|
||||
}
|
||||
|
||||
pub fn thread_manager_with_models_provider(
|
||||
pub async fn thread_manager_with_models_provider(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
) -> ThreadManager {
|
||||
ThreadManager::with_models_provider_for_tests(auth, provider)
|
||||
ThreadManager::with_models_provider_for_tests(auth, provider).await
|
||||
}
|
||||
|
||||
pub fn thread_manager_with_models_provider_and_home(
|
||||
pub async fn thread_manager_with_models_provider_and_home(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
@@ -71,22 +71,7 @@ pub fn thread_manager_with_models_provider_and_home(
|
||||
codex_home,
|
||||
environment_manager,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread_manager_with_models_provider_home_and_state(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<crate::StateDbHandle>,
|
||||
) -> ThreadManager {
|
||||
ThreadManager::with_models_provider_home_and_state_for_tests(
|
||||
auth,
|
||||
provider,
|
||||
codex_home,
|
||||
environment_manager,
|
||||
state_db,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_thread_with_user_shell_override(
|
||||
|
||||
@@ -18,6 +18,8 @@ use crate::skills_watcher::SkillsWatcher;
|
||||
use crate::skills_watcher::SkillsWatcherEvent;
|
||||
use crate::tasks::InterruptedTurnHistoryMarker;
|
||||
use crate::tasks::interrupted_turn_history_marker;
|
||||
use codex_agent_graph_store::AgentGraphStore;
|
||||
use codex_agent_graph_store::LocalAgentGraphStore;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -50,8 +52,8 @@ use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout::state_db;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_state::DirectionalThreadSpawnEdgeStatus;
|
||||
use codex_thread_store::InMemoryThreadStore;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
use codex_thread_store::LocalThreadStoreConfig;
|
||||
@@ -247,9 +249,10 @@ pub(crate) struct ThreadManagerState {
|
||||
mcp_manager: Arc<McpManager>,
|
||||
skills_watcher: Arc<SkillsWatcher>,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
state_db: StateDbHandle,
|
||||
agent_graph_store: Arc<dyn AgentGraphStore>,
|
||||
session_source: SessionSource,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
// Captures submitted ops for testing purpose when test mode is enabled.
|
||||
ops_log: Option<SharedCapturedOps>,
|
||||
}
|
||||
@@ -265,10 +268,11 @@ pub fn build_models_manager(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn thread_store_from_config(
|
||||
config: &Config,
|
||||
state_db: Option<StateDbHandle>,
|
||||
) -> Arc<dyn ThreadStore> {
|
||||
pub async fn init_state_db_from_config(config: &Config) -> Option<StateDbHandle> {
|
||||
state_db::init(config).await
|
||||
}
|
||||
|
||||
pub fn thread_store_from_config(config: &Config, state_db: StateDbHandle) -> Arc<dyn ThreadStore> {
|
||||
match &config.experimental_thread_store {
|
||||
ThreadStoreConfig::Local => Arc::new(LocalThreadStore::new(
|
||||
LocalThreadStoreConfig::from_config(config),
|
||||
@@ -279,15 +283,38 @@ pub fn thread_store_from_config(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_graph_store_from_state_db(state_db: StateDbHandle) -> Arc<dyn AgentGraphStore> {
|
||||
Arc::new(LocalAgentGraphStore::new(state_db))
|
||||
}
|
||||
|
||||
async fn state_db_from_roots_for_tests(
|
||||
codex_home: PathBuf,
|
||||
sqlite_home: PathBuf,
|
||||
default_model_provider_id: String,
|
||||
) -> StateDbHandle {
|
||||
let config = codex_rollout::RolloutConfig {
|
||||
codex_home: codex_home.clone(),
|
||||
sqlite_home,
|
||||
cwd: codex_home,
|
||||
model_provider_id: default_model_provider_id,
|
||||
generate_memories: false,
|
||||
};
|
||||
state_db::try_init(&config)
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("test state db should initialize: {err}"))
|
||||
}
|
||||
|
||||
impl ThreadManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
config: &Config,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
session_source: SessionSource,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
state_db: StateDbHandle,
|
||||
thread_store: Arc<dyn ThreadStore>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
agent_graph_store: Arc<dyn AgentGraphStore>,
|
||||
) -> Self {
|
||||
let codex_home = config.codex_home.clone();
|
||||
let restriction_product = session_source.restriction_product();
|
||||
@@ -314,10 +341,11 @@ impl ThreadManager {
|
||||
mcp_manager,
|
||||
skills_watcher,
|
||||
thread_store,
|
||||
state_db,
|
||||
agent_graph_store,
|
||||
auth_manager,
|
||||
session_source,
|
||||
analytics_events_client,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -327,7 +355,7 @@ impl ThreadManager {
|
||||
|
||||
/// Construct with a dummy AuthManager containing the provided CodexAuth.
|
||||
/// Used for integration tests: should not be used by ordinary business logic.
|
||||
pub(crate) fn with_models_provider_for_tests(
|
||||
pub(crate) async fn with_models_provider_for_tests(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
) -> Self {
|
||||
@@ -338,11 +366,18 @@ impl ThreadManager {
|
||||
));
|
||||
std::fs::create_dir_all(&codex_home)
|
||||
.unwrap_or_else(|err| panic!("temp codex home dir create failed: {err}"));
|
||||
let mut manager = Self::with_models_provider_and_home_for_tests(
|
||||
let state_db = state_db_from_roots_for_tests(
|
||||
codex_home.clone(),
|
||||
codex_home.clone(),
|
||||
OPENAI_PROVIDER_ID.to_string(),
|
||||
)
|
||||
.await;
|
||||
let mut manager = Self::with_models_provider_and_home_and_state_db_for_tests(
|
||||
auth,
|
||||
provider,
|
||||
codex_home.clone(),
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
state_db,
|
||||
);
|
||||
manager._test_codex_home_guard = Some(TempCodexHomeGuard { path: codex_home });
|
||||
manager
|
||||
@@ -350,27 +385,33 @@ impl ThreadManager {
|
||||
|
||||
/// Construct with a dummy AuthManager containing the provided CodexAuth and codex home.
|
||||
/// Used for integration tests: should not be used by ordinary business logic.
|
||||
pub(crate) fn with_models_provider_and_home_for_tests(
|
||||
pub(crate) async fn with_models_provider_and_home_for_tests(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
) -> Self {
|
||||
Self::with_models_provider_home_and_state_for_tests(
|
||||
let state_db = state_db_from_roots_for_tests(
|
||||
codex_home.clone(),
|
||||
codex_home.clone(),
|
||||
OPENAI_PROVIDER_ID.to_string(),
|
||||
)
|
||||
.await;
|
||||
Self::with_models_provider_and_home_and_state_db_for_tests(
|
||||
auth,
|
||||
provider,
|
||||
codex_home,
|
||||
environment_manager,
|
||||
/*state_db*/ None,
|
||||
state_db,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn with_models_provider_home_and_state_for_tests(
|
||||
fn with_models_provider_and_home_and_state_db_for_tests(
|
||||
auth: CodexAuth,
|
||||
provider: ModelProviderInfo,
|
||||
codex_home: PathBuf,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
state_db: Option<StateDbHandle>,
|
||||
state_db: StateDbHandle,
|
||||
) -> Self {
|
||||
set_thread_manager_test_mode_for_tests(/*enabled*/ true);
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
@@ -396,11 +437,11 @@ impl ThreadManager {
|
||||
let thread_store: Arc<dyn ThreadStore> = Arc::new(LocalThreadStore::new(
|
||||
LocalThreadStoreConfig {
|
||||
codex_home: codex_home.clone(),
|
||||
sqlite_home: codex_home.clone(),
|
||||
default_model_provider_id: OPENAI_PROVIDER_ID.to_string(),
|
||||
},
|
||||
state_db.clone(),
|
||||
));
|
||||
let agent_graph_store = agent_graph_store_from_state_db(state_db.clone());
|
||||
Self {
|
||||
state: Arc::new(ThreadManagerState {
|
||||
threads: Arc::new(RwLock::new(HashMap::new())),
|
||||
@@ -413,10 +454,11 @@ impl ThreadManager {
|
||||
mcp_manager,
|
||||
skills_watcher,
|
||||
thread_store,
|
||||
state_db,
|
||||
agent_graph_store,
|
||||
auth_manager,
|
||||
session_source: SessionSource::Exec,
|
||||
analytics_events_client: None,
|
||||
state_db,
|
||||
ops_log: should_use_test_thread_manager_behavior()
|
||||
.then(|| Arc::new(std::sync::Mutex::new(Vec::new()))),
|
||||
}),
|
||||
@@ -523,22 +565,17 @@ impl ThreadManager {
|
||||
subtree_thread_ids.push(thread_id);
|
||||
seen_thread_ids.insert(thread_id);
|
||||
|
||||
if let Some(state_db_ctx) = thread.state_db() {
|
||||
for status in [
|
||||
DirectionalThreadSpawnEdgeStatus::Open,
|
||||
DirectionalThreadSpawnEdgeStatus::Closed,
|
||||
] {
|
||||
for descendant_id in state_db_ctx
|
||||
.list_thread_spawn_descendants_with_status(thread_id, status)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!("failed to load thread-spawn descendants: {err}"))
|
||||
})?
|
||||
{
|
||||
if seen_thread_ids.insert(descendant_id) {
|
||||
subtree_thread_ids.push(descendant_id);
|
||||
}
|
||||
}
|
||||
for descendant_id in self
|
||||
.state
|
||||
.agent_graph_store
|
||||
.list_thread_spawn_descendants(thread_id, /*status_filter*/ None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
CodexErr::Fatal(format!("failed to load thread-spawn descendants: {err}"))
|
||||
})?
|
||||
{
|
||||
if seen_thread_ids.insert(descendant_id) {
|
||||
subtree_thread_ids.push(descendant_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,10 +901,14 @@ impl ThreadManager {
|
||||
}
|
||||
|
||||
impl ThreadManagerState {
|
||||
pub(crate) fn state_db(&self) -> Option<StateDbHandle> {
|
||||
pub(crate) fn state_db(&self) -> StateDbHandle {
|
||||
self.state_db.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn agent_graph_store(&self) -> Arc<dyn AgentGraphStore> {
|
||||
self.agent_graph_store.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn list_thread_ids(&self) -> Vec<ThreadId> {
|
||||
self.threads
|
||||
.read()
|
||||
@@ -1172,6 +1213,7 @@ impl ThreadManagerState {
|
||||
parent_trace,
|
||||
environment_selections,
|
||||
analytics_events_client: self.analytics_events_client.clone(),
|
||||
state_db: Some(self.state_db.clone()),
|
||||
thread_store: Arc::clone(&self.thread_store),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user