mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
fix(app-server): canonicalize local thread cwds
This commit is contained in:
Generated
+1
@@ -2004,6 +2004,7 @@ dependencies = [
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-cli",
|
||||
"codex-utils-json-to-toml",
|
||||
"codex-utils-path",
|
||||
"codex-utils-pty",
|
||||
"codex-web-search-extension",
|
||||
"codex-windows-sandbox",
|
||||
|
||||
@@ -3385,7 +3385,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -4558,7 +4558,12 @@
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
|
||||
+25
-5
@@ -15893,7 +15893,7 @@
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -16249,7 +16249,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
@@ -17927,7 +17932,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
@@ -18080,7 +18090,12 @@
|
||||
"$ref": "#/definitions/v2/CollaborationMode"
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
@@ -18343,7 +18358,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
|
||||
+25
-5
@@ -13710,7 +13710,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -14066,7 +14066,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
@@ -15744,7 +15749,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
@@ -15897,7 +15907,12 @@
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
@@ -16160,7 +16175,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
|
||||
@@ -999,7 +999,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -2293,7 +2293,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
+1
-1
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
@@ -999,7 +999,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -2319,7 +2319,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
+6
-1
@@ -297,7 +297,12 @@
|
||||
"$ref": "#/definitions/CollaborationMode"
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"effort": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -999,7 +999,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
@@ -2293,7 +2293,12 @@
|
||||
"description": "Reviewer currently used for approval requests on this thread."
|
||||
},
|
||||
"cwd": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Current working directory. Existing local paths are returned in canonical form."
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
|
||||
+1
-1
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
@@ -814,7 +814,7 @@
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Working directory captured for the thread."
|
||||
"description": "Working directory captured for the thread. Existing local paths are best-effort canonicalized with symbolic links resolved; unresolved paths are unchanged."
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ status: ThreadStatus,
|
||||
*/
|
||||
path: string | null,
|
||||
/**
|
||||
* Working directory captured for the thread.
|
||||
* Working directory captured for the thread. Existing local paths are best-effort
|
||||
* canonicalized with symbolic links resolved; unresolved paths are unchanged.
|
||||
*/
|
||||
cwd: AbsolutePathBuf,
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,10 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, /**
|
||||
* Current working directory. Existing local paths are returned in canonical form.
|
||||
*/
|
||||
cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -8,7 +8,10 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, /**
|
||||
* Current working directory. Existing local paths are returned in canonical form.
|
||||
*/
|
||||
cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -11,4 +11,8 @@ import type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
|
||||
export type ThreadSettings = { cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null, };
|
||||
export type ThreadSettings = {
|
||||
/**
|
||||
* Current working directory. Existing local paths are returned in canonical form.
|
||||
*/
|
||||
cwd: AbsolutePathBuf, approvalPolicy: AskForApproval, approvalsReviewer: ApprovalsReviewer, sandboxPolicy: SandboxPolicy, activePermissionProfile: ActivePermissionProfile | null, model: string, modelProvider: string, serviceTier: string | null, effort: ReasoningEffort | null, summary: ReasoningSummary | null, collaborationMode: CollaborationMode, personality: Personality | null, };
|
||||
|
||||
@@ -8,7 +8,10 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, cwd: AbsolutePathBuf, /**
|
||||
export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: string | null, /**
|
||||
* Current working directory. Existing local paths are returned in canonical form.
|
||||
*/
|
||||
cwd: AbsolutePathBuf, /**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
|
||||
|
||||
@@ -196,6 +196,7 @@ pub struct ThreadStartResponse {
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<String>,
|
||||
/// Current working directory. Existing local paths are returned in canonical form.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Thread-scoped runtime workspace roots used to materialize
|
||||
/// `:workspace_roots`.
|
||||
@@ -285,6 +286,7 @@ pub struct ThreadSettingsUpdateResponse {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSettings {
|
||||
/// Current working directory. Existing local paths are returned in canonical form.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub approvals_reviewer: ApprovalsReviewer,
|
||||
@@ -410,6 +412,7 @@ pub struct ThreadResumeResponse {
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<String>,
|
||||
/// Current working directory. Existing local paths are returned in canonical form.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Thread-scoped runtime workspace roots used to materialize
|
||||
/// `:workspace_roots`.
|
||||
@@ -558,6 +561,7 @@ pub struct ThreadForkResponse {
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<String>,
|
||||
/// Current working directory. Existing local paths are returned in canonical form.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Thread-scoped runtime workspace roots used to materialize
|
||||
/// `:workspace_roots`.
|
||||
|
||||
@@ -156,7 +156,8 @@ pub struct Thread {
|
||||
pub status: ThreadStatus,
|
||||
/// [UNSTABLE] Path to the thread on disk.
|
||||
pub path: Option<PathBuf>,
|
||||
/// Working directory captured for the thread.
|
||||
/// Working directory captured for the thread. Existing local paths are best-effort
|
||||
/// canonicalized with symbolic links resolved; unresolved paths are unchanged.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
/// Version of the CLI that created the thread.
|
||||
pub cli_version: String,
|
||||
|
||||
@@ -51,6 +51,7 @@ codex-plugin = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-skills-extension = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-utils-path = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
|
||||
@@ -134,6 +134,7 @@ Example with notification opt-out:
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
|
||||
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
|
||||
- Local thread paths returned as `cwd`, including nested `thread.cwd` values from `thread/start`, `thread/resume`, `thread/fork`, `thread/list`, `thread/read`, and `thread/started`, are best-effort canonicalized so filesystem aliases resolve to one stable spelling. Paths that the app-server host cannot resolve retain their original spelling.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate control/spawn parent is known.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
|
||||
@@ -1235,8 +1235,11 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::ThreadSettingsApplied(thread_settings_event) => {
|
||||
let thread_settings =
|
||||
thread_settings_from_core_snapshot(thread_settings_event.thread_settings);
|
||||
let config_snapshot = conversation.config_snapshot().await;
|
||||
let thread_settings = thread_settings_from_core_snapshot(
|
||||
thread_settings_event.thread_settings,
|
||||
&config_snapshot,
|
||||
);
|
||||
let changed = {
|
||||
let mut state = thread_state.lock().await;
|
||||
state.note_thread_settings(thread_settings.clone())
|
||||
|
||||
@@ -531,6 +531,28 @@ fn resolve_request_cwd(cwd: Option<PathBuf>) -> Result<Option<AbsolutePathBuf>,
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn thread_response_cwd_for_path(
|
||||
cwd: AbsolutePathBuf,
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
) -> AbsolutePathBuf {
|
||||
let primary_environment_is_local = config_snapshot
|
||||
.environment_selections()
|
||||
.first()
|
||||
.is_none_or(|environment| environment.environment_id == LOCAL_ENVIRONMENT_ID);
|
||||
if !primary_environment_is_local {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
let Ok(canonical) = codex_utils_path::normalize_for_path_comparison(cwd.as_path()) else {
|
||||
return cwd;
|
||||
};
|
||||
AbsolutePathBuf::from_absolute_path(canonical).unwrap_or(cwd)
|
||||
}
|
||||
|
||||
fn thread_response_cwd(config_snapshot: &ThreadConfigSnapshot) -> AbsolutePathBuf {
|
||||
thread_response_cwd_for_path(config_snapshot.cwd().clone(), config_snapshot)
|
||||
}
|
||||
|
||||
fn resolve_runtime_workspace_roots(workspace_roots: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
let mut resolved_roots = Vec::new();
|
||||
for root in workspace_roots {
|
||||
|
||||
@@ -628,7 +628,7 @@ pub(super) async fn handle_pending_thread_resume_request(
|
||||
}
|
||||
|
||||
let config_snapshot = pending.config_snapshot;
|
||||
let cwd = config_snapshot.cwd().clone();
|
||||
let cwd = thread_response_cwd(&config_snapshot);
|
||||
let ThreadConfigSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
|
||||
@@ -1202,7 +1202,7 @@ impl ThreadRequestProcessor {
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let cwd = config_snapshot.cwd().clone();
|
||||
let cwd = thread_response_cwd(&config_snapshot);
|
||||
let active_permission_profile =
|
||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||
|
||||
@@ -2712,6 +2712,7 @@ impl ThreadRequestProcessor {
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let cwd = thread_response_cwd(&config_snapshot);
|
||||
let active_permission_profile = thread_response_active_permission_profile(
|
||||
config_snapshot.active_permission_profile,
|
||||
);
|
||||
@@ -2745,7 +2746,7 @@ impl ThreadRequestProcessor {
|
||||
model: session_configured.model,
|
||||
model_provider: session_configured.model_provider_id,
|
||||
service_tier: session_configured.service_tier,
|
||||
cwd: session_configured.cwd,
|
||||
cwd,
|
||||
runtime_workspace_roots: config_snapshot.workspace_roots,
|
||||
instruction_sources,
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
@@ -3452,6 +3453,7 @@ impl ThreadRequestProcessor {
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd().as_path(),
|
||||
);
|
||||
let cwd = thread_response_cwd(&config_snapshot);
|
||||
let active_permission_profile =
|
||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||
|
||||
@@ -3460,7 +3462,7 @@ impl ThreadRequestProcessor {
|
||||
model: session_configured.model,
|
||||
model_provider: session_configured.model_provider_id,
|
||||
service_tier: session_configured.service_tier,
|
||||
cwd: session_configured.cwd,
|
||||
cwd,
|
||||
runtime_workspace_roots: config_snapshot.workspace_roots,
|
||||
instruction_sources,
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
@@ -4320,7 +4322,7 @@ fn build_thread_from_snapshot(
|
||||
updated_at: now,
|
||||
status: ThreadStatus::NotLoaded,
|
||||
path,
|
||||
cwd: config_snapshot.cwd().clone(),
|
||||
cwd: thread_response_cwd(config_snapshot),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||
|
||||
@@ -189,13 +189,14 @@ pub(crate) fn thread_response_sandbox_policy(
|
||||
pub(crate) fn thread_settings_from_config_snapshot(
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
) -> ThreadSettings {
|
||||
let cwd = thread_response_cwd(config_snapshot);
|
||||
ThreadSettings {
|
||||
cwd: config_snapshot.cwd().clone(),
|
||||
cwd: cwd.clone(),
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox_policy: thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd().as_path(),
|
||||
cwd.as_path(),
|
||||
),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
config_snapshot.active_permission_profile.clone(),
|
||||
@@ -212,6 +213,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
|
||||
pub(crate) fn thread_settings_from_core_snapshot(
|
||||
snapshot: codex_protocol::protocol::ThreadSettingsSnapshot,
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
) -> ThreadSettings {
|
||||
let codex_protocol::protocol::ThreadSettingsSnapshot {
|
||||
model,
|
||||
@@ -227,6 +229,7 @@ pub(crate) fn thread_settings_from_core_snapshot(
|
||||
personality,
|
||||
collaboration_mode,
|
||||
} = snapshot;
|
||||
let cwd = thread_response_cwd_for_path(cwd, config_snapshot);
|
||||
let sandbox_policy = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
|
||||
ThreadSettings {
|
||||
sandbox_policy,
|
||||
|
||||
@@ -555,8 +555,10 @@ async fn thread_list_respects_cwd_filters() -> Result<()> {
|
||||
vec![second_filtered_id.as_str(), first_filtered_id.as_str()]
|
||||
);
|
||||
assert!(!filtered_ids.contains(&unfiltered_id.as_str()));
|
||||
assert_eq!(data[0].cwd.as_path(), second_target_cwd.as_path());
|
||||
assert_eq!(data[1].cwd.as_path(), first_target_cwd.as_path());
|
||||
let expected_second_cwd = codex_utils_path::normalize_for_path_comparison(&second_target_cwd)?;
|
||||
let expected_first_cwd = codex_utils_path::normalize_for_path_comparison(&first_target_cwd)?;
|
||||
assert_eq!(data[0].cwd.as_path(), expected_second_cwd.as_path());
|
||||
assert_eq!(data[1].cwd.as_path(), expected_first_cwd.as_path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::io::Write;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
@@ -134,6 +136,109 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn local_thread_apis_return_one_canonical_cwd() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
let workspace_home = TempDir::new()?;
|
||||
let workspace = workspace_home.path().join("CEO Core");
|
||||
let workspace_alias = workspace_home.path().join("CEO Core Alias");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
symlink(&workspace, &workspace_alias)?;
|
||||
let canonical_cwd = workspace.canonicalize()?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
cwd: Some(workspace_alias.to_string_lossy().into_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, cwd, .. } = to_response(start_resp)?;
|
||||
assert_eq!(cwd.as_path(), canonical_cwd);
|
||||
assert_eq!(thread.cwd.as_path(), canonical_cwd);
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "Persist this thread".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let thread_id = thread.id;
|
||||
drop(mcp);
|
||||
std::fs::remove_file(&workspace_alias)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let list_id = mcp
|
||||
.send_thread_list_request(ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(50),
|
||||
sort_key: None,
|
||||
sort_direction: None,
|
||||
model_providers: None,
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
use_state_db_only: true,
|
||||
search_term: None,
|
||||
})
|
||||
.await?;
|
||||
let list_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(list_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadListResponse { data, .. } = to_response(list_resp)?;
|
||||
let listed = data
|
||||
.iter()
|
||||
.find(|thread| thread.id == thread_id)
|
||||
.expect("thread/list should return the persisted thread");
|
||||
assert_eq!(listed.cwd.as_path(), canonical_cwd);
|
||||
|
||||
for include_turns in [false, true] {
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread_id.clone(),
|
||||
include_turns,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadReadResponse { thread, .. } = to_response(read_resp)?;
|
||||
assert_eq!(thread.cwd.as_path(), canonical_cwd);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_read_can_include_turns() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -2230,7 +2230,8 @@ async fn thread_resume_defers_updated_at_until_turn_start() -> Result<()> {
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse { cwd, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(cwd, AbsolutePathBuf::from_absolute_path(codex_home.path())?);
|
||||
let expected_cwd = codex_utils_path::normalize_for_path_comparison(codex_home.path())?;
|
||||
assert_eq!(cwd.as_path(), expected_cwd.as_path());
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
|
||||
@@ -43,10 +43,18 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() ->
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let (model_id, service_tier_id) = service_tier_model_and_tier_id()?;
|
||||
let workspace = codex_home.path().join("workspace");
|
||||
let workspace_child = workspace.join("child");
|
||||
std::fs::create_dir_all(&workspace_child)?;
|
||||
let workspace_alias = workspace_child.join("..");
|
||||
let canonical_cwd = workspace.canonicalize()?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let thread = start_thread(&mut mcp).await?.thread;
|
||||
let thread = start_thread_with_cwd(&mut mcp, workspace_alias)
|
||||
.await?
|
||||
.thread;
|
||||
assert_eq!(thread.cwd.as_path(), canonical_cwd);
|
||||
|
||||
send_thread_settings_update(
|
||||
&mut mcp,
|
||||
@@ -67,6 +75,7 @@ async fn thread_settings_update_emits_notification_and_updates_future_turns() ->
|
||||
|
||||
let updated = read_thread_settings_updated(&mut mcp).await?;
|
||||
assert_eq!(updated.thread_id, thread.id);
|
||||
assert_eq!(updated.thread_settings.cwd.as_path(), canonical_cwd);
|
||||
assert_eq!(updated.thread_settings.model, model_id);
|
||||
assert_eq!(
|
||||
updated.thread_settings.service_tier.as_deref(),
|
||||
@@ -324,9 +333,24 @@ async fn start_text_turn(mcp: &mut TestAppServer, thread_id: String) -> Result<(
|
||||
}
|
||||
|
||||
async fn start_thread(mcp: &mut TestAppServer) -> Result<ThreadStartResponse> {
|
||||
start_thread_with_params(mcp, /*cwd*/ None).await
|
||||
}
|
||||
|
||||
async fn start_thread_with_cwd(
|
||||
mcp: &mut TestAppServer,
|
||||
cwd: std::path::PathBuf,
|
||||
) -> Result<ThreadStartResponse> {
|
||||
start_thread_with_params(mcp, Some(cwd.to_string_lossy().into_owned())).await
|
||||
}
|
||||
|
||||
async fn start_thread_with_params(
|
||||
mcp: &mut TestAppServer,
|
||||
cwd: Option<String>,
|
||||
) -> Result<ThreadStartResponse> {
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
cwd,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -229,7 +229,8 @@ async fn thread_start_accepts_absolute_runtime_workspace_roots() -> Result<()> {
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(resp)?;
|
||||
|
||||
assert_eq!(response_cwd, cwd.abs());
|
||||
let canonical_cwd = codex_utils_path::normalize_for_path_comparison(&cwd)?;
|
||||
assert_eq!(response_cwd.as_path(), canonical_cwd.as_path());
|
||||
assert_eq!(runtime_workspace_roots, vec![extra_root.abs()]);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -96,6 +96,10 @@ pub(super) fn touch_modified_time(path: &Path) -> std::io::Result<()> {
|
||||
OpenOptions::new().append(true).open(path)?.set_times(times)
|
||||
}
|
||||
|
||||
pub(super) fn normalize_thread_cwd(cwd: PathBuf) -> PathBuf {
|
||||
codex_utils_path::normalize_for_path_comparison(cwd.as_path()).unwrap_or(cwd)
|
||||
}
|
||||
|
||||
pub(super) fn stored_thread_from_rollout_item(
|
||||
item: ThreadItem,
|
||||
archived: bool,
|
||||
@@ -119,6 +123,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
.or_else(|| item.first_user_message.clone())
|
||||
.unwrap_or_default();
|
||||
let rollout_path = codex_rollout::plain_rollout_path(item.path.as_path());
|
||||
let cwd = normalize_thread_cwd(item.cwd.unwrap_or_default());
|
||||
|
||||
Some(StoredThread {
|
||||
thread_id,
|
||||
@@ -137,7 +142,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
created_at,
|
||||
updated_at,
|
||||
archived_at,
|
||||
cwd: item.cwd.unwrap_or_default(),
|
||||
cwd,
|
||||
cli_version: item.cli_version.unwrap_or_default(),
|
||||
source,
|
||||
thread_source: None,
|
||||
@@ -241,6 +246,9 @@ fn thread_id_from_rollout_path(path: &Path) -> Option<ThreadId> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use codex_rollout::ThreadItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
@@ -270,4 +278,34 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn stored_thread_from_rollout_item_canonicalizes_cwd() {
|
||||
let home = tempfile::tempdir().expect("temp dir");
|
||||
let target = home.path().join("target");
|
||||
let alias = home.path().join("alias");
|
||||
std::fs::create_dir(&target).expect("target dir");
|
||||
symlink(&target, &alias).expect("cwd symlink");
|
||||
let uuid = Uuid::from_u128(2);
|
||||
|
||||
let thread = stored_thread_from_rollout_item(
|
||||
ThreadItem {
|
||||
path: home.path().join(format!("rollout-{uuid}.jsonl")),
|
||||
cwd: Some(alias),
|
||||
..Default::default()
|
||||
},
|
||||
/*archived*/ false,
|
||||
"test-provider",
|
||||
)
|
||||
.expect("stored thread");
|
||||
|
||||
assert_eq!(thread.cwd, target.canonicalize().expect("canonical target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_thread_cwd_preserves_missing_path() {
|
||||
let missing = PathBuf::from("/definitely/missing/codex-thread-cwd");
|
||||
assert_eq!(normalize_thread_cwd(missing.clone()), missing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use codex_state::ThreadMetadata;
|
||||
use super::LocalThreadStore;
|
||||
use super::helpers::distinct_thread_metadata_title;
|
||||
use super::helpers::git_info_from_parts;
|
||||
use super::helpers::normalize_thread_cwd;
|
||||
use super::helpers::permission_profile_from_metadata_value;
|
||||
use super::helpers::rollout_path_is_archived;
|
||||
use super::helpers::set_thread_name_from_title;
|
||||
@@ -55,6 +56,13 @@ pub(super) async fn read_thread(
|
||||
&& (params.include_archived || rollout_thread.archived_at.is_none())
|
||||
&& !rollout_thread.preview.is_empty()
|
||||
{
|
||||
// SQLite observes and normalizes cwd updates as they are persisted. Prefer its
|
||||
// stable value while it still resolves, so a missing or retargeted rollout alias
|
||||
// cannot change the cwd returned by thread/read. If SQLite is stale or missing,
|
||||
// retain the rollout cwd as before.
|
||||
if codex_utils_path::normalize_for_path_comparison(thread.cwd.as_path()).is_ok() {
|
||||
rollout_thread.cwd = thread.cwd.clone();
|
||||
}
|
||||
if thread.name.is_some() {
|
||||
rollout_thread.name = thread.name;
|
||||
}
|
||||
@@ -321,8 +329,9 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
.clone()
|
||||
.or_else(|| metadata.first_user_message.clone())
|
||||
.unwrap_or_default();
|
||||
let cwd = normalize_thread_cwd(metadata.cwd);
|
||||
let permission_profile =
|
||||
permission_profile_from_metadata_value(&metadata.sandbox_policy, metadata.cwd.as_path());
|
||||
permission_profile_from_metadata_value(&metadata.sandbox_policy, cwd.as_path());
|
||||
StoredThread {
|
||||
thread_id: metadata.id,
|
||||
extra_config: None,
|
||||
@@ -341,7 +350,7 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
created_at: metadata.created_at,
|
||||
updated_at: metadata.updated_at,
|
||||
archived_at: metadata.archived_at,
|
||||
cwd: metadata.cwd,
|
||||
cwd,
|
||||
cli_version: metadata.cli_version,
|
||||
source: parse_session_source(&metadata.source),
|
||||
thread_source: metadata.thread_source,
|
||||
@@ -389,6 +398,7 @@ fn stored_thread_from_meta_line(
|
||||
.map(DateTime::<Utc>::from)
|
||||
.unwrap_or(created_at);
|
||||
let rollout_path = codex_rollout::plain_rollout_path(path.as_path());
|
||||
let cwd = normalize_thread_cwd(meta_line.meta.cwd);
|
||||
StoredThread {
|
||||
thread_id: meta_line.meta.id,
|
||||
extra_config: None,
|
||||
@@ -407,7 +417,7 @@ fn stored_thread_from_meta_line(
|
||||
created_at,
|
||||
updated_at,
|
||||
archived_at: archived.then_some(updated_at),
|
||||
cwd: meta_line.meta.cwd,
|
||||
cwd,
|
||||
cli_version: meta_line.meta.cli_version,
|
||||
source: meta_line.meta.source,
|
||||
thread_source: meta_line.meta.thread_source,
|
||||
@@ -447,7 +457,8 @@ fn parse_rfc3339_non_optional(value: &str) -> Option<DateTime<Utc>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -806,7 +817,9 @@ mod tests {
|
||||
std::fs::create_dir_all(&day_dir).expect("sessions dir");
|
||||
let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
|
||||
let mut file = std::fs::File::create(&rollout_path).expect("session file");
|
||||
let rollout_cwd = PathBuf::from("/");
|
||||
let rollout_cwd = home.path().join("rollout-workspace");
|
||||
std::fs::create_dir(&rollout_cwd).expect("rollout workspace dir");
|
||||
let expected_rollout_cwd = normalize_thread_cwd(rollout_cwd.clone());
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": "2025-01-03T12:00:00Z",
|
||||
"type": "session_meta",
|
||||
@@ -863,7 +876,7 @@ mod tests {
|
||||
assert_eq!(thread.preview, "Hello from rollout");
|
||||
assert_eq!(thread.name, Some("Saved title".to_string()));
|
||||
assert_eq!(thread.model_provider, "rollout-provider");
|
||||
assert_eq!(thread.cwd, rollout_cwd);
|
||||
assert_eq!(thread.cwd, expected_rollout_cwd);
|
||||
let legacy_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
@@ -874,11 +887,82 @@ mod tests {
|
||||
thread.permission_profile,
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&legacy_policy,
|
||||
rollout_cwd.as_path()
|
||||
expected_rollout_cwd.as_path()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn read_thread_returns_same_canonical_cwd_from_rollout_and_sqlite() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite_home.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
||||
let uuid = Uuid::from_u128(240);
|
||||
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
||||
let target_cwd = home.path().join("workspace");
|
||||
let alias_cwd = home.path().join("workspace-alias");
|
||||
std::fs::create_dir(&target_cwd).expect("workspace dir");
|
||||
symlink(&target_cwd, &alias_cwd).expect("workspace alias");
|
||||
|
||||
let day_dir = home.path().join("sessions/2025/01/03");
|
||||
std::fs::create_dir_all(&day_dir).expect("sessions dir");
|
||||
let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
|
||||
let mut file = std::fs::File::create(&rollout_path).expect("session file");
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": "2025-01-03T12:00:00Z",
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": uuid,
|
||||
"timestamp": "2025-01-03T12:00:00Z",
|
||||
"cwd": alias_cwd,
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version",
|
||||
"source": "cli",
|
||||
"model_provider": "rollout-provider"
|
||||
},
|
||||
});
|
||||
writeln!(file, "{meta}").expect("write session meta");
|
||||
let user_event = serde_json::json!({
|
||||
"timestamp": "2025-01-03T12:00:00Z",
|
||||
"type": "event_msg",
|
||||
"payload": {
|
||||
"type": "user_message",
|
||||
"message": "Hello from rollout",
|
||||
"kind": "plain",
|
||||
},
|
||||
});
|
||||
writeln!(file, "{user_event}").expect("write user event");
|
||||
|
||||
let mut builder =
|
||||
ThreadMetadataBuilder::new(thread_id, rollout_path, Utc::now(), SessionSource::Cli);
|
||||
builder.cwd = target_cwd.clone();
|
||||
runtime
|
||||
.upsert_thread(&builder.build(config.default_model_provider_id.as_str()))
|
||||
.await
|
||||
.expect("state db upsert should succeed");
|
||||
|
||||
let canonical_cwd = target_cwd.canonicalize().expect("canonical workspace");
|
||||
std::fs::remove_file(&alias_cwd).expect("remove workspace alias");
|
||||
for include_history in [false, true] {
|
||||
let thread = store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: false,
|
||||
include_history,
|
||||
})
|
||||
.await
|
||||
.expect("read thread");
|
||||
assert_eq!(thread.cwd, canonical_cwd);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_thread_uses_legacy_thread_name_when_sqlite_title_is_missing() {
|
||||
let home = TempDir::new().expect("temp dir");
|
||||
@@ -1112,7 +1196,10 @@ mod tests {
|
||||
);
|
||||
assert!(thread.updated_at >= thread.created_at);
|
||||
assert_eq!(thread.archived_at, None);
|
||||
assert_eq!(thread.cwd, home.path());
|
||||
assert_eq!(
|
||||
thread.cwd,
|
||||
home.path().canonicalize().expect("canonical cwd")
|
||||
);
|
||||
assert_eq!(thread.cli_version, "test_version");
|
||||
assert_eq!(thread.source, SessionSource::Cli);
|
||||
let history = thread.history.expect("history should load");
|
||||
|
||||
@@ -18,6 +18,7 @@ use tracing::warn;
|
||||
|
||||
use super::LocalThreadStore;
|
||||
use super::helpers::git_info_from_parts;
|
||||
use super::helpers::normalize_thread_cwd;
|
||||
use super::helpers::permission_profile_to_metadata_value;
|
||||
use super::live_writer;
|
||||
use crate::GitInfoPatch;
|
||||
@@ -225,7 +226,11 @@ async fn apply_metadata_update(
|
||||
builder.agent_nickname = patch.agent_nickname.clone().flatten();
|
||||
builder.agent_role = patch.agent_role.clone().flatten();
|
||||
builder.agent_path = patch.agent_path.clone().flatten();
|
||||
builder.cwd = patch.cwd.clone().map(normalize_cwd).unwrap_or_default();
|
||||
builder.cwd = patch
|
||||
.cwd
|
||||
.clone()
|
||||
.map(normalize_thread_cwd)
|
||||
.unwrap_or_default();
|
||||
builder.cli_version = patch.cli_version.clone();
|
||||
let mut metadata = builder.build(store.config.default_model_provider_id.as_str());
|
||||
if rollout_path_archived {
|
||||
@@ -276,7 +281,7 @@ async fn apply_metadata_update(
|
||||
metadata.agent_path = agent_path;
|
||||
}
|
||||
if let Some(cwd) = patch.cwd {
|
||||
metadata.cwd = normalize_cwd(cwd);
|
||||
metadata.cwd = normalize_thread_cwd(cwd);
|
||||
}
|
||||
if let Some(cli_version) = patch.cli_version {
|
||||
metadata.cli_version = cli_version;
|
||||
@@ -404,10 +409,6 @@ fn enum_to_string<T: serde::Serialize>(value: &T) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_cwd(cwd: PathBuf) -> PathBuf {
|
||||
codex_utils_path::normalize_for_path_comparison(cwd.as_path()).unwrap_or(cwd)
|
||||
}
|
||||
|
||||
async fn apply_thread_git_info(
|
||||
store: &LocalThreadStore,
|
||||
thread_id: ThreadId,
|
||||
|
||||
Reference in New Issue
Block a user