diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 9c749229f..980623262 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -33,7 +33,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -61,7 +61,7 @@ module "coder-login" { module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_parameter.ai_prompt.value @@ -84,6 +84,7 @@ module "codex" { - **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory - **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI - **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided) +- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions. ## Configuration @@ -107,7 +108,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" # ... other variables ... # Override default configuration diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 7d34a9c43..2041e36e6 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -368,4 +368,90 @@ describe("codex", async () => { expect(prompt.exitCode).not.toBe(0); expect(prompt.stderr).toContain("No such file or directory"); }); + + test("codex-continue-capture-new-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test task", + }, + }); + + const workdir = "/home/coder"; + const expectedSessionId = "019a1234-5678-9abc-def0-123456789012"; + const sessionsDir = "/home/coder/.codex/sessions"; + const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`; + + await execContainer(id, ["mkdir", "-p", sessionsDir]); + await execContainer(id, [ + "bash", + "-c", + `echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`, + ]); + + await execModuleScript(id); + + await expectAgentAPIStarted(id); + + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + const maxAttempts = 30; + let trackingFileContents = ""; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const result = await execContainer(id, [ + "bash", + "-c", + `cat ${trackingFile} 2>/dev/null || echo ""`, + ]); + if (result.stdout.trim().length > 0) { + trackingFileContents = result.stdout; + break; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`); + + const startLog = await readFileContainer( + id, + "/home/coder/.codex-module/agentapi-start.log", + ); + expect(startLog).toContain("Capturing new session ID"); + expect(startLog).toContain("Session tracked"); + expect(startLog).toContain(expectedSessionId); + }); + + test("codex-continue-resume-existing-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test prompt", + }, + }); + + const workdir = "/home/coder"; + const mockSessionId = "019a1234-5678-9abc-def0-123456789012"; + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + + await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]); + await execContainer(id, [ + "bash", + "-c", + `echo "${workdir}|${mockSessionId}" > ${trackingFile}`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.codex-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("Found existing task session"); + expect(startLog.stdout).toContain(mockSessionId); + expect(startLog.stdout).toContain("Resuming existing session"); + expect(startLog.stdout).toContain( + `Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`, + ); + expect(startLog.stdout).not.toContain("test prompt"); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index d181c10f5..a68cd79fe 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -137,6 +137,12 @@ variable "ai_prompt" { default = "" } +variable "continue" { + type = bool + description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." + default = true +} + variable "codex_system_prompt" { type = string description = "System instructions written to AGENTS.md in the ~/.codex directory" @@ -187,8 +193,9 @@ module "agentapi" { ARG_OPENAI_API_KEY='${var.openai_api_key}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_CODEX_MODEL='${var.codex_model}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_CONTINUE='${var.continue}' \ /tmp/start.sh EOT @@ -206,7 +213,7 @@ module "agentapi" { ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \ ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \ /tmp/install.sh EOT diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh index be54d5755..663e80e50 100644 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ b/registry/coder-labs/modules/codex/scripts/start.sh @@ -3,6 +3,7 @@ source "$HOME"/.bashrc set -o errexit set -o pipefail + command_exists() { command -v "$1" > /dev/null 2>&1 } @@ -16,6 +17,7 @@ fi printf "Version: %s\n" "$(codex --version)" set -o nounset ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d) +ARG_CONTINUE=${ARG_CONTINUE:-true} echo "=== Codex Launch Configuration ===" printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" @@ -23,53 +25,187 @@ printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}" printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")" printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" +printf "Continue Sessions: %s\n" "$ARG_CONTINUE" echo "======================================" set +o nounset -CODEX_ARGS=() -if command_exists codex; then - printf "Codex is installed\n" -else - printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" - exit 1 -fi +SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session" -if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then - printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } -else - printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" +find_session_for_directory() { + local target_dir="$1" + + if [ ! -f "$SESSION_TRACKING_FILE" ]; then + return 1 + fi + + local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1) + + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + + return 1 +} + +store_session_mapping() { + local dir="$1" + local session_id="$2" + + mkdir -p "$(dirname "$SESSION_TRACKING_FILE")" + + if [ -f "$SESSION_TRACKING_FILE" ]; then + grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true + mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE" + fi + + echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE" +} + +find_recent_session_file() { + local target_dir="$1" + local sessions_dir="$HOME/.codex/sessions" + + if [ ! -d "$sessions_dir" ]; then + return 1 + fi + + local latest_file="" + local latest_time=0 + + while IFS= read -r session_file; do + local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0") + local first_line=$(head -n 1 "$session_file" 2> /dev/null) + local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4) + + if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then + latest_file="$session_file" + latest_time="$file_time" + fi + done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null) + + if [ -n "$latest_file" ]; then + local first_line=$(head -n 1 "$latest_file") + local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + fi + + return 1 +} + +wait_for_session_file() { + local target_dir="$1" + local max_attempts=20 + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "") + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + sleep 0.5 + attempt=$((attempt + 1)) + done + + return 1 +} + +validate_codex_installation() { + if command_exists codex; then + printf "Codex is installed\n" + else + printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" exit 1 - } -fi + fi +} -if [ -n "$ARG_CODEX_MODEL" ]; then - CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") -fi +setup_workdir() { + if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then + printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + else + printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + fi +} + +build_codex_args() { + CODEX_ARGS=() -if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then - printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT" - if [ "${ARG_REPORT_TASKS}" == "true" ]; then - PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + if [ -n "$ARG_CODEX_MODEL" ]; then + CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") + fi + + if [ "$ARG_CONTINUE" = "true" ]; then + existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "") + + if [ -n "$existing_session" ]; then + printf "Found existing task session for this directory: %s\n" "$existing_session" + printf "Resuming existing session...\n" + CODEX_ARGS+=("resume" "$existing_session") + else + printf "No existing task session found for this directory\n" + printf "Starting new task session...\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi + fi else - PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + printf "Continue disabled, starting fresh session\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi fi - CODEX_ARGS+=("$PROMPT") -else - printf "No task prompt given.\n" -fi +} + +capture_session_id() { + if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then + printf "Capturing new session ID...\n" + new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "") + + if [ -n "$new_session" ]; then + store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session" + printf "✓ Session tracked: %s\n" "$new_session" + printf "This session will be automatically resumed on next restart\n" + else + printf "⚠ Could not capture session ID after 10s timeout\n" + fi + fi +} + +start_codex() { + printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" + agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" & + capture_session_id +} -# Terminal dimensions optimized for Coder Tasks UI sidebar: -# - Width 67: fits comfortably in sidebar -# - Height 1190: adjusted due to Codex terminal height bug -printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" -agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" +validate_codex_installation +setup_workdir +build_codex_args +start_codex diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh index 8c1c7366d..fe8f3806c 100644 --- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh +++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh @@ -1,5 +1,6 @@ #!/bin/bash +# Handle --version flag if [[ "$1" == "--version" ]]; then echo "HELLO: $(bash -c env)" echo "codex version v1.0.0" @@ -8,7 +9,30 @@ fi set -e +SESSION_ID="" +IS_RESUME=false + +while [[ $# -gt 0 ]]; do + case $1 in + resume) + IS_RESUME=true + SESSION_ID="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [ "$IS_RESUME" = false ]; then + SESSION_ID="019a1234-5678-9abc-def0-123456789012" + echo "Created new session: $SESSION_ID" +else + echo "Resuming session: $SESSION_ID" +fi + while true; do - echo "$(date) - codex-mock" + echo "$(date) - codex-mock (session: $SESSION_ID)" sleep 15 done