diff --git a/docs/PROJECT_STATE.md b/docs/PROJECT_STATE.md index 84c41a9..cb0188c 100644 --- a/docs/PROJECT_STATE.md +++ b/docs/PROJECT_STATE.md @@ -4,13 +4,13 @@ - 本仓用于在不修改上游 `codex` 的前提下验证和实现 `cbth` background task handler;当前主线已经完成 CLI dogfood v1 的主要能力、`v0.2.2` generation-aware app-server diagnostics / Desktop ready arm workflow / standalone review gate release,以及 Desktop bridge foundation。 - 顶层历史 tracker 已迁移到 project journal;完整旧记录保存在 [legacy snapshot](project_journal/2026/05/2026-05-05-legacy-tracker-snapshot-bbe4003.md),完成历史摘要见 [completed work archive](project_journal/2026/05/2026-05-05-completed-work-archive-bbe4003.md)。Legacy snapshot 是逐字归档,内部复制的旧相对链接保留历史原貌;实时导航以本文件、TODO 和 archive summary 为准。 -- 当前活跃工作集中在 Desktop bridge 自动投递闭环、CLI/daemon recovery contract 的剩余收口、host-level plugin runtime / generic delivery design,以及外部 review / output / PR polling integrations;`cbth resume` 的 cwd、canonical permission profile、Codex CLI version warning follow-up 已收口,Codex 0.129 request-side permission profile selection 已补齐 stable built-in exact-match 优先与 legacy fallback,Codex 0.130 accepted-turn reconcile 已优先使用 `thread/turns/list` 并保留 `thread/read` fallback,CLI operator UX 已补齐 help、app-server listing 和 interactive self-update,daemon upgrade safety 已完成 PR1-PR5 并进入 `v0.2.0` release,详见 [resume follow-ups](project_journal/2026/05/2026-05-07-cbth-resume-followups-698664c.md)、[Codex 0.129 permission selection](project_journal/2026/05/2026-05-08-codex-129-permission-selection-6431a08.md)、[Codex 0.130 pagination](project_journal/2026/05/2026-05-09-codex-130-pagination-41fb384.md)、[cbth CLI operator UX](project_journal/2026/05/2026-05-10-cbth-cli-operator-ux-41fb384.md)、[daemon upgrade safety](project_journal/2026/05/2026-05-11-daemon-upgrade-safety-5721.md) 和 [current follow-ups](project_journal/2026/05/2026-05-05-current-follow-ups-bbe4003.md)。 +- 当前活跃工作集中在 Desktop bridge 自动投递闭环、CLI/daemon recovery contract 的剩余收口、host-level plugin runtime / generic delivery design,以及外部 review / output / PR polling integrations;`cbth resume` 的 cwd、canonical permission profile、Codex CLI version warning follow-up 已收口,Codex 0.129 request-side permission profile selection 已补齐 stable built-in exact-match 优先与 legacy fallback,Codex 0.130 accepted-turn reconcile 已优先使用 `thread/turns/list` 并保留 `thread/read` fallback,Codex 0.131-0.132 兼容性已通过真实 app-server/TUI delivery live checks 扩展 soft validated range,CLI operator UX 已补齐 help、app-server listing 和 interactive self-update,daemon upgrade safety 已完成 PR1-PR5 并进入 `v0.2.0` release,详见 [resume follow-ups](project_journal/2026/05/2026-05-07-cbth-resume-followups-698664c.md)、[Codex 0.129 permission selection](project_journal/2026/05/2026-05-08-codex-129-permission-selection-6431a08.md)、[Codex 0.130 pagination](project_journal/2026/05/2026-05-09-codex-130-pagination-41fb384.md)、[Codex 0.131-0.132 compatibility](project_journal/2026/05/2026-05-20-codex-131-132-compatibility-37fd.md)、[cbth CLI operator UX](project_journal/2026/05/2026-05-10-cbth-cli-operator-ux-41fb384.md)、[daemon upgrade safety](project_journal/2026/05/2026-05-11-daemon-upgrade-safety-5721.md) 和 [current follow-ups](project_journal/2026/05/2026-05-05-current-follow-ups-bbe4003.md)。 - Desktop heartbeat 已通过 no-DB helper path 验证 direct-file-read inbox 消费能力;Desktop writeback helper foundation 已有本地 fake coverage,但直接 heartbeat writeback 被 `startup.lock` / local filesystem sandbox 阻断;transcript / tool-output relay 已在 interactive Desktop tool call 和 heartbeat automation run 中验证为 `function_call_output` carrier,并已通过真实 heartbeat arm envelopes + non-Desktop consumer live validation 驱动既有 CAS;production scanner foundation 已补齐显式 rollout binding、marker issuance、daemon-owned bounded cursor scanning 和 replay/marker retention cleanup,并已完成真实 Desktop heartbeat live validation;ready materialization 和 two-phase bridge arm workflow 已有 fake coverage;`read_transport_capability=validated`,`writeback_capability=validated`,`artifact_read_capability` 仍为 `unknown`。 ## Active Handoff - Phase: `v0.2.2` release is prepared for generation-aware app-server visibility, Desktop ready materialization / bridge arm workflow, and the standalone Codex review gate action; broader CLI/Desktop follow-ups remain active. -- Summary: The managed resume path now preserves native cwd behavior unless an explicit/interactive cwd is selected, parses Codex 0.129 tagged canonical permission profiles on read, pins Codex 0.129 `turn/start.permissions` when a stable built-in current active profile exactly matches the effective cap, falls back to legacy `sandboxPolicy` otherwise, treats `codex-cli 0.130.x` as the validated CLI range, uses Codex 0.130 paginated turn reads for accepted-turn reconcile when available, and `cbth` now has clearer help, generation-aware `cli app-servers --format json|human` with `-H` and `--latest-generation`, best-effort loaded non-bound session diagnostics, `self update --interactive` with `-i`, daemon upgrade fail-closed/coexistence/handoff/drain support, Desktop transcript relay writeback plus production scanner validation, Desktop ready materialization and two-phase bridge arm markers, host-level plugin runtime design, and standalone Codex review gate integration. +- Summary: The managed resume path now preserves native cwd behavior unless an explicit/interactive cwd is selected, parses Codex 0.129 tagged canonical permission profiles on read, pins Codex 0.129 `turn/start.permissions` when a stable built-in current active profile exactly matches the effective cap, falls back to legacy `sandboxPolicy` otherwise, treats `codex-cli 0.130.x-0.132.x` as the validated CLI range, uses Codex 0.130 paginated turn reads for accepted-turn reconcile when available, and `cbth` now has clearer help, generation-aware `cli app-servers --format json|human` with `-H` and `--latest-generation`, best-effort loaded non-bound session diagnostics, `self update --interactive` with `-i`, daemon upgrade fail-closed/coexistence/handoff/drain support, Desktop transcript relay writeback plus production scanner validation, Desktop ready materialization and two-phase bridge arm markers, host-level plugin runtime design, and standalone Codex review gate integration. - Next Steps: - Continue post-`0.2.0` daemon recovery follow-ups from [daemon upgrade safety](project_journal/2026/05/2026-05-11-daemon-upgrade-safety-5721.md) and [current follow-ups](project_journal/2026/05/2026-05-05-current-follow-ups-bbe4003.md). - Continue from the active backlog in [current follow-ups](project_journal/2026/05/2026-05-05-current-follow-ups-bbe4003.md). @@ -22,7 +22,7 @@ - Desktop direct writeback is blocked by heartbeat-owned startup-lock access and local filesystem write denial; transcript relay writeback and production scanner consumption are live-validated, while boundary crossing, binding lifecycle cleanup, artifact reads, and several daemon deadline/recovery contracts remain incomplete. - Evidence: - Release: `v0.2.2` - - Latest completed Codex 0.130 note: [2026-05-09-codex-130-pagination-41fb384.md](project_journal/2026/05/2026-05-09-codex-130-pagination-41fb384.md) + - Latest completed Codex compatibility note: [2026-05-20-codex-131-132-compatibility-37fd.md](project_journal/2026/05/2026-05-20-codex-131-132-compatibility-37fd.md) - Latest completed CLI operator UX note: [2026-05-10-cbth-cli-operator-ux-41fb384.md](project_journal/2026/05/2026-05-10-cbth-cli-operator-ux-41fb384.md) - Design docs: [SHARED_CORE_ARCHITECTURE.md](design/SHARED_CORE_ARCHITECTURE.md), [DESKTOP_BRIDGE_FOUNDATION.md](design/DESKTOP_BRIDGE_FOUNDATION.md), [DESKTOP_LIVE_PREFLIGHT_VALIDATION.md](validation/DESKTOP_LIVE_PREFLIGHT_VALIDATION.md), [DESKTOP_WRITEBACK_HELPER_LIVE_VALIDATION.md](validation/DESKTOP_WRITEBACK_HELPER_LIVE_VALIDATION.md), [DAEMON_UPGRADE_SAFETY.md](plans/DAEMON_UPGRADE_SAFETY.md), [HOST_PLUGIN_RUNTIME_AND_DELIVERY.md](design/HOST_PLUGIN_RUNTIME_AND_DELIVERY.md), [LIVE_E2E.en-GB.md](LIVE_E2E.en-GB.md) diff --git a/docs/PROJECT_TODO.md b/docs/PROJECT_TODO.md index 367e081..793ace3 100644 --- a/docs/PROJECT_TODO.md +++ b/docs/PROJECT_TODO.md @@ -7,7 +7,7 @@ Active backlog lives in [current follow-ups](project_journal/2026/05/2026-05-05- - [pending] Finish remaining CLI/daemon recovery and fixed-thread contract follow-ups after the `0.2.0` daemon upgrade safety release. - [done] Follow up `cbth resume` hardening in order: cwd UX parity, canonical permission profile parsing, Codex 0.129 stable built-in request-side permission profile selection with legacy fallback, and Codex CLI version compatibility warnings. - [done] Improve `cbth` operator UX with clearer help, `cli app-servers --format json|human`, and `self update --interactive`. -- [done] Adapt managed CLI accepted-turn reconcile for Codex 0.130 `thread/turns/list`, keep `thread/read(includeTurns=true)` fallback for older app-server surfaces, and update the soft validated Codex CLI range to `0.130.x`. +- [done] Adapt managed CLI accepted-turn reconcile for Codex 0.130 `thread/turns/list`, keep `thread/read(includeTurns=true)` fallback for older app-server surfaces, and update the soft validated Codex CLI range through `0.132.x`. - [pending] Implement host-level plugin runtime and generic delivery in the C1-C7 / W1-W7 sequence recorded in [Host plugin runtime design](project_journal/2026/05/2026-05-13-host-plugin-runtime-design-143a38f.md). - [pending] Implement external code-review delegation, app-server output bridge, and PR / GitHub Actions polling integrations. - [done] Migrate the long-form top-level trackers into project journal entries without dropping the original records. diff --git a/docs/design/CLI_SHARED_APP_SERVER_SIDECAR_DESIGN.md b/docs/design/CLI_SHARED_APP_SERVER_SIDECAR_DESIGN.md index 88657dd..872383d 100644 --- a/docs/design/CLI_SHARED_APP_SERVER_SIDECAR_DESIGN.md +++ b/docs/design/CLI_SHARED_APP_SERVER_SIDECAR_DESIGN.md @@ -785,7 +785,7 @@ v1 范围外: - bootstrap app-server 在 promote 前只存在于 pending registry,不触碰 managed session proof;bootstrap 失败、promote 失败、lease 过期或 daemon shutdown 时必须停止子进程并 join drain worker。 - discovery 的 remote error / timeout / closed / malformed response 都会 fail closed,不创建 managed session。 - daemon status 现在会列出 active CLI app-server,并且 daemon capability 列表包含 `cli-app-server-lifecycle` / `cli-app-server-probe`;新 CLI 不会把 lifecycle 或 doctor probe request 投递给不支持该命令的旧 daemon。 -- managed CLI startup 和 `cbth doctor cli` 会执行 `codex --version`,把当前 `codex-cli 0.130.x` 作为 soft validated range。版本不可解析或不在该范围内只输出 warning / doctor `warn` 状态,真实执行仍由后续 protocol field 解析与 capability gate fail-closed。 +- managed CLI startup 和 `cbth doctor cli` 会执行 `codex --version`,把当前 `codex-cli 0.130.x-0.132.x` 作为 soft validated range。版本不可解析或不在该范围内只输出 warning / doctor `warn` 状态,真实执行仍由后续 protocol field 解析与 capability gate fail-closed。 - public `cbth resume [-- ]` 已作为 native resume wrapper 落地;它复用 fixed-thread managed session / daemon-owned app-server / passive sidecar 流程,前台命令为 `codex resume --remote ...`,只在 operator 显式转发 `--cd` / `-C` 或交互式 cwd 选择后附加单个 `--cd `。 - 为避免 cold existing thread 被 sidecar 抢先 materialize,`cbth resume` 的 sidecar 首次 `thread/resume` 会携带从前台 argv 可推导出的 native resume overrides:已确定的 `cwd`、`model`、`profile`、`approvalsReviewer` 和 `persistExtendedHistory`;前台转发参数中的相对 `--cd` 会先按 caller cwd 规范成绝对路径,未显式转发 cwd 的非交互式路径不会强制 caller cwd,交互式路径会先读历史 thread cwd 并在 current cwd / prior thread cwd 之间选择后再启动 sidecar materialization。resume prompt 后出现的 forwarded Codex option 会 fail-closed,因为 sidecar 无法安全复刻 Codex 的 post-positional option 解析并保证首次 `thread/resume` 同步;`--remote` / `--remote-auth-token-env` transport overrides 会被拒绝,避免前台 Codex 连接到非托管 app-server;`--add-dir` 也会被拒绝,因为当前 Codex `thread/resume` 没有能无损表达 additional writable roots 的字段,不能让 sidecar 用不完整的 root set 抢先记录启动权限快照;`--last` / `--all` / `--include-non-interactive` native resume selector 会被拒绝,因为它们可能让 foreground Codex resume 到不同于 managed bound thread id 的线程;`--oss` / `--local-provider` provider overrides 会被拒绝,因为当前启动 materialization 不能无损复刻 provider selection;`--sandbox` / `--ask-for-approval` / `--full-auto` / danger-bypass permission overrides 会被拒绝,让 managed resume 权限只来自启动 snapshot;`--search` 与 `--enable` / `--disable` feature overrides 会被拒绝,因为当前首次 `thread/resume` 不能无损表达 foreground feature state;`--config` / `-c` 只允许当前能镜像进首次 `thread/resume` 的键(`model`、`model_provider`、`profile`、`config_profile`、`approvals_reviewer`)。sandbox/permission-scope 与 feature-like config roots(例如 `approval_policy`、`sandbox_mode`、`sandbox_workspace_write.*`、`sandbox_read_only.*`、`sandbox_permissions.*`、`permissions.*`、`permission_profile.*`、`active_permission_profile.*`、`default_permissions`、`profiles.*`、`projects.*`、`trust_level`、`features.*`、`use_legacy_landlock`、`request_permissions`、`web_search*`、`tools` / `tools.web_search*`、`writable_roots`、`readable_roots`、`network_access`)同样会被拒绝;其它 unmirrored config key 也 fail-closed,直到 sidecar 能把它们无损带入首次 `thread/resume`。后续自动投递前的 permission refresh 仍使用只含 `threadId` 的 current-state `thread/resume`。 - durable `cli_managed_sessions` schema 已落地,记录 `managed_session_id`、`bound_thread_id`、`session_epoch`、`session_state`、`activity_state`、`activity_revision`、current effective session risk profile、startup permission snapshot、last permission snapshot 和 timestamps。 diff --git a/docs/project_journal/2026/05/2026-05-20-codex-131-132-compatibility-37fd.md b/docs/project_journal/2026/05/2026-05-20-codex-131-132-compatibility-37fd.md new file mode 100644 index 0000000..d020f52 --- /dev/null +++ b/docs/project_journal/2026/05/2026-05-20-codex-131-132-compatibility-37fd.md @@ -0,0 +1,45 @@ +--- +id: 20260520-37fd-codex-131-132-compatibility +title: Codex 0.131-0.132 Compatibility +status: completed +created: 2026-05-20 +updated: 2026-05-20 +branch: codex/codex-131-132-compatibility +pr: https://github.com/JoeyTeng/codex-background-task-handler/pull/94 +supersedes: [] +superseded_by: +--- + +# Codex 0.131-0.132 Compatibility + +## Summary + +- Extend the soft validated Codex CLI range from `codex-cli 0.130.x` to `codex-cli 0.130.x-0.132.x`. +- Keep the existing app-server delivery protocol path: daemon-owned loopback `codex app-server`, foreground `codex --remote`, sidecar `thread/resume` / `turn/start`, and fail-closed protocol parsing. +- Confirm that v0.131/v0.132 app-server direction does not require moving TUI delivery to `codex mcp-server`; MCP remains a separate stdio integration surface. +- Update live fresh-foreground test wrappers to run the real Codex TUI under `script(1)` so Codex 0.132's non-TTY / `TERM=dumb` guard does not invalidate opt-in live E2E. Darwin uses the BSD `script` shape; Linux uses the `script -c` shape with shell-quoted argv, feature-detects util-linux `script -e`, fails on unexpected foreground exit, records the real foreground Codex PID for teardown, and suppresses TUI redraw output so test stdout pipes cannot fill. + +## Current State + +- `cbth doctor cli` and managed startup warn outside `codex-cli 0.130.x-0.132.x`. +- Deterministic doctor coverage accepts `0.130.0`, `0.131.0`, and `0.132.0`, and still warns for `0.133.0`. +- Real `codex-cli 0.132.0` live smoke, existing-thread trusted-all delivery, new-thread trusted-all delivery, and task-supervisor delivery all passed locally. + +## Validation + +- `codex --version` reported `codex-cli 0.132.0`. +- `node --version` reported `v24.15.0`. +- `cargo test --locked doctor_cli -- --nocapture` +- `CBTH_RUN_LIVE_CODEX_E2E=1 cargo test --test live_smoke -- --ignored --nocapture` +- `CBTH_RUN_LIVE_TRUSTED_ALL_E2E=1 cargo test --test live_trusted_all -- --ignored --nocapture` +- `CBTH_RUN_LIVE_NEW_THREAD_E2E=1 cargo test --test live_new_thread -- --ignored --nocapture` +- `CBTH_RUN_LIVE_TASK_SUPERVISOR_E2E=1 cargo test --test live_task_supervisor -- --ignored --nocapture` +- `cargo fmt --all -- --check` +- `cargo clippy --locked --all-targets -- -D warnings` +- `cargo test --locked` +- `git diff --check` +- `uv run python /Users/hoteng/.codex/personal-sync/releases/dcb64bf00f2f3d3b2c78edf69dc00c87c416ad9c/personal_codex/skills/project-journal/scripts/project_journal.py validate --repo /Users/hoteng/.codex/worktrees/37fd/codex-background-task-handler` + +## Next Steps + +- Re-run the same live suite before widening the soft range beyond `0.132.x`. diff --git a/src/cli.rs b/src/cli.rs index e9b5380..06999b9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -90,9 +90,10 @@ const CLI_APP_SERVER_TURNS_LIST_RECONCILE_PAGE_SIZE: u32 = 64; const CLI_APP_SERVER_TURNS_LIST_RECONCILE_MAX_PAGES: usize = 2; const DOCTOR_CODEX_VERSION_TIMEOUT_SECONDS: u64 = 5; const DOCTOR_APP_SERVER_PROBE_TIMEOUT_SECONDS: u64 = 15; -const VALIDATED_CODEX_CLI_VERSION_REQUIREMENT: &str = "0.130.x"; +const VALIDATED_CODEX_CLI_VERSION_REQUIREMENT: &str = "0.130.x-0.132.x"; const VALIDATED_CODEX_CLI_MAJOR: u64 = 0; -const VALIDATED_CODEX_CLI_MINOR: u64 = 130; +const VALIDATED_CODEX_CLI_MIN_MINOR: u64 = 130; +const VALIDATED_CODEX_CLI_MAX_MINOR: u64 = 132; const DESKTOP_INBOX_SCHEMA_VERSION: i64 = 1; const DESKTOP_INBOX_MAX_JSON_BYTES: u64 = 1024 * 1024; const DESKTOP_SNAPSHOT_REVISION_RETENTION: usize = 128; @@ -10584,7 +10585,7 @@ fn codex_cli_version_warning(raw_version: Option<&str>) -> Option { "cbth could not parse Codex CLI version {raw_version:?}; validated range is codex-cli {VALIDATED_CODEX_CLI_VERSION_REQUIREMENT}" )); }; - if version.major == VALIDATED_CODEX_CLI_MAJOR && version.minor == VALIDATED_CODEX_CLI_MINOR { + if codex_cli_version_is_validated(&version) { return None; } Some(format!( @@ -10592,6 +10593,11 @@ fn codex_cli_version_warning(raw_version: Option<&str>) -> Option { )) } +fn codex_cli_version_is_validated(version: &Version) -> bool { + version.major == VALIDATED_CODEX_CLI_MAJOR + && (VALIDATED_CODEX_CLI_MIN_MINOR..=VALIDATED_CODEX_CLI_MAX_MINOR).contains(&version.minor) +} + fn parse_codex_cli_version(raw_version: &str) -> Option { raw_version .split_whitespace() diff --git a/tests/doctor_cli.rs b/tests/doctor_cli.rs index 60d9fba..335d06f 100644 --- a/tests/doctor_cli.rs +++ b/tests/doctor_cli.rs @@ -234,6 +234,33 @@ fn doctor_cli_readiness_accepts_stderr_listener() { assert_eq!(check_status(&value, "codex-app-server-listener"), "ok"); } +#[test] +fn doctor_cli_accepts_validated_codex_versions() { + for version in ["0.130.0", "0.131.0", "0.132.0"] { + let home = temp_home(); + let fake_codex = write_fake_codex_script(&home.path().join("fake-codex")); + let fake_version = format!("codex-cli {version}"); + let output = run_doctor(&home, &fake_codex, &[("FAKE_CODEX_VERSION", &fake_version)]); + stop_daemon(&home); + + assert!( + output.status.success(), + "doctor failed for {version}\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let value = parse_stdout(&output); + assert_eq!(value["doctor"]["ok"], true); + let codex_binary = check(&value, "codex-binary"); + assert_eq!(codex_binary["status"], "ok"); + assert_eq!( + codex_binary["details"]["compatibility"]["validated_range"], + "0.130.x-0.132.x" + ); + assert!(codex_binary["details"]["compatibility"]["warning"].is_null()); + } +} + #[test] fn doctor_cli_warns_when_codex_version_is_outside_validated_range() { let home = temp_home(); @@ -241,7 +268,7 @@ fn doctor_cli_warns_when_codex_version_is_outside_validated_range() { let output = run_doctor( &home, &fake_codex, - &[("FAKE_CODEX_VERSION", "codex-cli 0.131.0")], + &[("FAKE_CODEX_VERSION", "codex-cli 0.133.0")], ); stop_daemon(&home); @@ -257,12 +284,12 @@ fn doctor_cli_warns_when_codex_version_is_outside_validated_range() { assert_eq!(codex_binary["status"], "warn"); assert_eq!( codex_binary["details"]["compatibility"]["validated_range"], - "0.130.x" + "0.130.x-0.132.x" ); assert!( codex_binary["details"]["compatibility"]["warning"] .as_str() - .is_some_and(|warning| warning.contains("0.131.0")), + .is_some_and(|warning| warning.contains("0.133.0")), "missing version warning: {codex_binary}" ); } diff --git a/tests/live_new_thread.rs b/tests/live_new_thread.rs index ec5d9f1..6ace8f2 100644 --- a/tests/live_new_thread.rs +++ b/tests/live_new_thread.rs @@ -80,16 +80,107 @@ if [ "${1:-}" = "app-server" ]; then fi exit_file="${CBTH_LIVE_FOREGROUND_EXIT_FILE:?}" +status_file="${exit_file}.foreground-status.$$" +foreground_pid_file="${exit_file}.foreground-pid.$$" timeout_seconds="${CBTH_LIVE_FOREGROUND_TIMEOUT_SECONDS:-360}" -"${CBTH_LIVE_REAL_CODEX_BIN:?}" "$@" & +rm -f "$status_file" "$foreground_pid_file" + +quote_shell_arg() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +script_supports_exit_status() { + TERM=xterm-256color script -q -e -c 'exit 0' /dev/null >/dev/null 2>&1 +} + +run_foreground_with_pty() { + if [ "$(uname -s)" = "Darwin" ]; then + script -q /dev/null /bin/sh -c 'printf "%s\n" "$$" > "$1"; shift; TERM=xterm-256color exec "$@"' sh "$foreground_pid_file" "${CBTH_LIVE_REAL_CODEX_BIN:?}" "$@" >/dev/null 2>&1 + elif command -v script >/dev/null 2>&1; then + command_text="printf '%s\n' \"\$\$\" > $(quote_shell_arg "$foreground_pid_file"); TERM=xterm-256color exec $(quote_shell_arg "${CBTH_LIVE_REAL_CODEX_BIN:?}")" + for arg in "$@"; do + command_text="$command_text $(quote_shell_arg "$arg")" + done + if script_supports_exit_status; then + TERM=xterm-256color script -q -e -c "$command_text" /dev/null >/dev/null 2>&1 + else + TERM=xterm-256color script -q -c "$command_text" /dev/null >/dev/null 2>&1 + fi + else + echo "cbth live foreground wrapper requires script(1) for a pseudo-terminal" >&2 + return 127 + fi +} + +read_foreground_pid() { + if [ ! -f "$foreground_pid_file" ]; then + return 1 + fi + foreground_pid="$(cat "$foreground_pid_file" 2>/dev/null || true)" + case "$foreground_pid" in + ''|*[!0-9]*) return 1 ;; + esac + printf '%s\n' "$foreground_pid" +} + +foreground_alive() { + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ] && kill -0 "$foreground_pid" 2>/dev/null; then + return 0 + fi + kill -0 "$child" 2>/dev/null +} + +terminate_foreground() { + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ]; then + kill -TERM "$foreground_pid" 2>/dev/null || true + fi + kill -TERM "$child" 2>/dev/null || true + waited=0 + while foreground_alive && [ "$waited" -lt 5 ]; do + sleep 1 + waited=$((waited + 1)) + done + if foreground_alive; then + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ]; then + kill -KILL "$foreground_pid" 2>/dev/null || true + fi + kill -KILL "$child" 2>/dev/null || true + fi + wait "$child" 2>/dev/null || true + rm -f "$status_file" "$foreground_pid_file" +} + +( + set +e + run_foreground_with_pty "$@" + status="$?" + printf '%s\n' "$status" > "$status_file" + exit "$status" +) & child="$!" -trap 'kill "$child" 2>/dev/null || true; wait "$child" 2>/dev/null || true' INT TERM EXIT +trap 'terminate_foreground' INT TERM EXIT elapsed=0 while [ "$elapsed" -lt "$timeout_seconds" ]; do - if [ -f "$exit_file" ]; then - kill "$child" 2>/dev/null || true + if [ -f "$status_file" ]; then + foreground_status="$(cat "$status_file" 2>/dev/null || printf '1')" + case "$foreground_status" in + ''|*[!0-9]*) foreground_status=1 ;; + esac wait "$child" 2>/dev/null || true + rm -f "$status_file" "$foreground_pid_file" + trap - INT TERM EXIT + echo "cbth live foreground wrapper exited before shutdown with status $foreground_status" >&2 + if [ "$foreground_status" -eq 0 ]; then + exit 1 + fi + exit "$foreground_status" + fi + if [ -f "$exit_file" ]; then + terminate_foreground trap - INT TERM EXIT exit 0 fi @@ -97,8 +188,7 @@ while [ "$elapsed" -lt "$timeout_seconds" ]; do elapsed=$((elapsed + 1)) done -kill "$child" 2>/dev/null || true -wait "$child" 2>/dev/null || true +terminate_foreground trap - INT TERM EXIT echo "cbth live foreground wrapper timed out" >&2 exit 124 diff --git a/tests/live_task_supervisor.rs b/tests/live_task_supervisor.rs index 7ce728e..289e71f 100644 --- a/tests/live_task_supervisor.rs +++ b/tests/live_task_supervisor.rs @@ -82,16 +82,107 @@ if [ "${1:-}" = "app-server" ]; then fi exit_file="${CBTH_LIVE_FOREGROUND_EXIT_FILE:?}" +status_file="${exit_file}.foreground-status.$$" +foreground_pid_file="${exit_file}.foreground-pid.$$" timeout_seconds="${CBTH_LIVE_FOREGROUND_TIMEOUT_SECONDS:-360}" -"${CBTH_LIVE_REAL_CODEX_BIN:?}" "$@" & +rm -f "$status_file" "$foreground_pid_file" + +quote_shell_arg() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +script_supports_exit_status() { + TERM=xterm-256color script -q -e -c 'exit 0' /dev/null >/dev/null 2>&1 +} + +run_foreground_with_pty() { + if [ "$(uname -s)" = "Darwin" ]; then + script -q /dev/null /bin/sh -c 'printf "%s\n" "$$" > "$1"; shift; TERM=xterm-256color exec "$@"' sh "$foreground_pid_file" "${CBTH_LIVE_REAL_CODEX_BIN:?}" "$@" >/dev/null 2>&1 + elif command -v script >/dev/null 2>&1; then + command_text="printf '%s\n' \"\$\$\" > $(quote_shell_arg "$foreground_pid_file"); TERM=xterm-256color exec $(quote_shell_arg "${CBTH_LIVE_REAL_CODEX_BIN:?}")" + for arg in "$@"; do + command_text="$command_text $(quote_shell_arg "$arg")" + done + if script_supports_exit_status; then + TERM=xterm-256color script -q -e -c "$command_text" /dev/null >/dev/null 2>&1 + else + TERM=xterm-256color script -q -c "$command_text" /dev/null >/dev/null 2>&1 + fi + else + echo "cbth live foreground wrapper requires script(1) for a pseudo-terminal" >&2 + return 127 + fi +} + +read_foreground_pid() { + if [ ! -f "$foreground_pid_file" ]; then + return 1 + fi + foreground_pid="$(cat "$foreground_pid_file" 2>/dev/null || true)" + case "$foreground_pid" in + ''|*[!0-9]*) return 1 ;; + esac + printf '%s\n' "$foreground_pid" +} + +foreground_alive() { + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ] && kill -0 "$foreground_pid" 2>/dev/null; then + return 0 + fi + kill -0 "$child" 2>/dev/null +} + +terminate_foreground() { + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ]; then + kill -TERM "$foreground_pid" 2>/dev/null || true + fi + kill -TERM "$child" 2>/dev/null || true + waited=0 + while foreground_alive && [ "$waited" -lt 5 ]; do + sleep 1 + waited=$((waited + 1)) + done + if foreground_alive; then + foreground_pid="$(read_foreground_pid || true)" + if [ -n "$foreground_pid" ]; then + kill -KILL "$foreground_pid" 2>/dev/null || true + fi + kill -KILL "$child" 2>/dev/null || true + fi + wait "$child" 2>/dev/null || true + rm -f "$status_file" "$foreground_pid_file" +} + +( + set +e + run_foreground_with_pty "$@" + status="$?" + printf '%s\n' "$status" > "$status_file" + exit "$status" +) & child="$!" -trap 'kill "$child" 2>/dev/null || true; wait "$child" 2>/dev/null || true' INT TERM EXIT +trap 'terminate_foreground' INT TERM EXIT elapsed=0 while [ "$elapsed" -lt "$timeout_seconds" ]; do - if [ -f "$exit_file" ]; then - kill "$child" 2>/dev/null || true + if [ -f "$status_file" ]; then + foreground_status="$(cat "$status_file" 2>/dev/null || printf '1')" + case "$foreground_status" in + ''|*[!0-9]*) foreground_status=1 ;; + esac wait "$child" 2>/dev/null || true + rm -f "$status_file" "$foreground_pid_file" + trap - INT TERM EXIT + echo "cbth live foreground wrapper exited before shutdown with status $foreground_status" >&2 + if [ "$foreground_status" -eq 0 ]; then + exit 1 + fi + exit "$foreground_status" + fi + if [ -f "$exit_file" ]; then + terminate_foreground trap - INT TERM EXIT exit 0 fi @@ -99,8 +190,7 @@ while [ "$elapsed" -lt "$timeout_seconds" ]; do elapsed=$((elapsed + 1)) done -kill "$child" 2>/dev/null || true -wait "$child" 2>/dev/null || true +terminate_foreground trap - INT TERM EXIT echo "cbth live foreground wrapper timed out" >&2 exit 124