Skip to content

feat: /continue <session-id>: an AI analyzes that session and injects a concise summary message into the current session (does#397

Merged
andrei-hasna merged 2 commits into
mainfrom
factory/task-50811349d94c-9bb5bae1
Jul 25, 2026
Merged

feat: /continue <session-id>: an AI analyzes that session and injects a concise summary message into the current session (does#397
andrei-hasna merged 2 commits into
mainfrom
factory/task-50811349d94c-9bb5bae1

Conversation

@andrei-hasna

Copy link
Copy Markdown
Contributor

Objective

/continue : an AI analyzes that session and injects a concise summary message into the current session (does not switch to it). — Start a goal plan for this task and drive it node by node (required). Investigate the current codewith implementation first and cite file:line. Build and test on remote Blacksmith ONLY (never compile locally). Keep cargo fmt, clippy -D warnings, and prettier clean. Land a PR into hasna/codewith main.

Verification

  • install: pass

Run run_0d95e4d41759 · backend codewith · task inline
🏭 Generated by @hasnaxyz/factory

@andrei-hasna
andrei-hasna force-pushed the factory/task-50811349d94c-9bb5bae1 branch from 9ec323e to e4a9423 Compare July 24, 2026 18:40
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Rebased onto origin/main (86b7e78) and fixed the blocking issues; proven green on a Blacksmith 16vcpu runner.

What was broken / what changed on top of the original factory commit

  1. Branch did not compileSession::reconstruct_history_from_rollout is pub(super) (module-private to core::session), so the new CodexThread::generate_session_continuation could not call it. Added a narrow pub(crate) reconstruct_history_items_from_rollout wrapper in core/src/session/rollout_reconstruction.rs that returns just the rebuilt Vec<ResponseItem>, so callers outside the module do not need the module-private reconstruction bundle.
  2. Vendored app-server schema fixtures were incomplete — the PR hand-added v2/ThreadContinueParams.{json,ts} / v2/ThreadContinueResponse.json but never regenerated the aggregate bundles, so codex-app-server-protocol json_schema_fixtures_match_generated / typescript_schema_fixtures_match_generated would fail. Regenerated with just write-app-server-schema; this updated schema/json/ClientRequest.json, schema/json/codex_app_server_protocol.schemas.json, schema/json/codex_app_server_protocol.v2.schemas.json, schema/typescript/ClientRequest.ts, and corrected the hand-written v2 fixtures (doc comments + trailing-newline byte mismatch).
  3. rustfmt violations in thread_processor.rs, test_app_server.rs, and tests/suite/v2/thread_continue.rs (plus a misordered import). just fmt-check is now clean.

No behavioral changes to the feature itself; diff stays scoped to /continue.

Build gate — Blacksmith testbox blacksmith-16vcpu-ubuntu-2404, run 30118433296 → conclusion success

just clippy -p codex-core -p codex-app-server -p codex-tui -p codex-app-server-protocol -- -D warnings -A clippy::byte-char-slices \
  && cd codex-rs && cargo nextest run -p codex-core -p codex-app-server -p codex-tui -p codex-app-server-protocol \
     -E "test(thread_continue) or test(continue_slash_command) or test(continue_command_requires_inline_args) or test(session_continuation_rejects_active_destination) or test(truncate_recap_input) or test(schema_fixtures_match_generated) or test(client_request_serialization_scope) or test(slash_popup)"

23 tests run: 23 passed, 7975 skipped, including:

  • codex-app-server::all suite::v2::thread_continue::thread_continue_summarizes_source_into_captured_destination_once
  • codex-app-server-protocol::schema_fixtures json_schema_fixtures_match_generated + typescript_schema_fixtures_match_generated
  • codex-core session::tests::session_continuation_rejects_active_destination_without_injecting, session_recap::tests::truncate_recap_input_keeps_the_newest_items
  • codex-tui chatwidget::tests::slash_commands::continue_slash_command_targets_current_thread_without_switching, slash_command::tests::continue_command_requires_inline_args_and_waits_for_idle_session, and all slash_popup* snapshot tests (no popup snapshot drift from the new command)

-A clippy::byte-char-slices is needed only for a pre-existing lint on main in core/src/unified_exec/process_manager_tests.rs:151,176 (file untouched by this PR); a first run (30117876273) failed on exactly those two lines and nothing else. just fmt-check (cargo fmt + just + ruff) is clean locally.

… a concise summary message into the current session (does
@andrei-hasna
andrei-hasna force-pushed the factory/task-50811349d94c-9bb5bae1 branch from e4a9423 to 74a73ef Compare July 25, 2026 05:19
`record_session_continuation_if_idle` reserved a bare `ActiveTurn` and then
awaited several times before writing to history, but it never re-checked that
the reservation was still its own. `spawn_task` -> `abort_all_tasks` ->
`take_active_turn` clears the active turn unconditionally, so a user submit
landing in that window stole the reservation and the continuation still recorded
its assistant recap into a thread with a live turn, returning `Ok(())`.

Mirror the sibling helpers: extract the ownership probe both
`try_start_turn_if_idle` and `try_start_user_input_turn_if_idle` already perform
into `still_holds_reserved_idle_turn`, and call it in the continuation path
before any history write. Add a regression test that parks the continuation on
the session state lock, steals the reservation the way a concurrent submit does,
and asserts the continuation is rejected with history untouched.

Also harden the two secondary findings from review:

- `session_recap` replays source history with its original roles, so source
  model output arrives as `assistant` items. Fence the replayed transcript
  between explicit untrusted-data markers (after truncation, so the markers
  cannot be dropped) and keep the destination session's recap task as the final
  instruction. Covered by an adversarial-source-history unit test.
- The TUI continuation handler dropped both outcomes when the destination thread
  was no longer displayed, and logged nothing on failure. Log the outcome in
  both paths so a failed continuation is never silent.
@andrei-hasna
andrei-hasna force-pushed the factory/task-50811349d94c-9bb5bae1 branch from 74a73ef to ac15909 Compare July 25, 2026 05:30
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Addressed the REQUEST_CHANGES review. Rebased onto origin/main (now includes #396 and the clippy cleanup); new head ac1590906.

BLOCKER — reservation theft during record_session_continuation_if_idle (fixed)

core/src/session/inject.rs reserved a bare ActiveTurn and then awaited several times before writing to history without re-checking ownership. spawn_taskabort_all_taskstake_active_turn does an unconditional active.take(), so a concurrent user submit stole the reservation and the continuation still recorded its assistant recap into a thread with a live turn, returning Ok(()).

  • Extracted the ownership probe that try_start_turn_if_idle (was inject.rs:170-182) and try_start_user_input_turn_if_idle (was inject.rs:245-256) already duplicate into Session::still_holds_reserved_idle_turn (task.is_none() + Arc::ptr_eq on the reserved turn state), and call it from the continuation path.
  • Moved the read-only reference_context_item() probe above the check so the check dominates every history mutation: nothing between the check and the writes awaits anything other than the writes themselves.
  • Regression test session_continuation_rejects_destination_that_becomes_active_while_recording parks the continuation on the session state lock (inside new_default_turn, i.e. after the reservation and before any write), steals the reservation exactly the way a submit does (unconditional take + a fresh ActiveTurn), then asserts the continuation is rejected, history is byte-identical, and the replacing turn is not cleared.

Verified the test fails before the fix — with the re-check removed it fails on a stolen reservation should reject the continuation: (), i.e. the recap was recorded and Ok(()) returned. With the fix, 16/16 of the continuation + idle-reservation + recap tests pass.

SECONDARY 1 — source history replayed at assistant trust

session_recap.rs replays the source transcript with its original roles, so source model output arrives as assistant items. The recap prompt now fences the replayed transcript between explicit untrusted-data markers, applied after truncation so the markers can never be truncated away, with the destination session's recap task still last so this session issues the final instruction. All three wrapper items are low-trust user items. New test fence_source_history_contains_adversarial_assistant_items puts an instruction-like payload in an assistant source item and asserts it stays strictly inside the fence, plus fence_source_history_wraps_empty_source_history for the degenerate case. The prose SESSION_CONTINUATION_INSTRUCTIONS rule is now asserted rather than assumed.

SECONDARY 2 — TUI dropped the continuation outcome

tui/src/app/background_requests.rs handle_session_continuation_finished returned early when the destination thread was no longer displayed, discarding both outcomes, and logged nothing on failure. The continuation is recorded into the destination thread regardless of what is on screen, so both paths now log; failures log at warn with the thread ids and the error, in addition to the existing in-chat error cell.

Gate: Blacksmith testbox run against this head, broadened past the previous 23-test filter to the full test set for the touched crates, and with the -A clippy::byte-char-slices escape hatch removed:

just clippy -p codex-core -p codex-app-server -p codex-tui -p codex-app-server-protocol -- -D warnings \
  && cd codex-rs && cargo nextest run -p codex-core -p codex-app-server -p codex-tui -p codex-app-server-protocol

@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Gate: GREEN — Blacksmith testbox run 30147021979, conclusion=success, headSha=ac1590906c7479eedbe88abb4507f543cbea4501 (this PR's head).

Summary [ 385.574s] 7962 tests run: 7962 passed (1 slow, 1 flaky), 82 skipped
  • Clippy ran with no escape hatch. The build command is just clippy -p codex-core -p codex-app-server -p codex-tui -p codex-app-server-protocol -- -D warnings; -A clippy::byte-char-slices is gone (0 occurrences in the run log) and clippy is clean on its own.
  • Gate broadened well past the previous 23-test -E filter to the whole test set for the four touched crates (7962 tests vs 23), since this PR adds a SlashCommand variant and a ClientRequest variant.
  • All seven tests belonging to this PR pass on the box: session_continuation_rejects_destination_that_becomes_active_while_recording (the new regression test), session_continuation_rejects_active_destination_without_injecting, fence_source_history_contains_adversarial_assistant_items, fence_source_history_wraps_empty_source_history, continue_slash_command_targets_current_thread_without_switching, continue_command_requires_inline_args_and_waits_for_idle_session, suite::v2::thread_continue::thread_continue_summarizes_source_into_captured_destination_once.

On the 63 excluded tests. A first full-set run on this head (30146468493) failed on 63 tests. I ran the identical command on main at b71a05ee9 (30146467779) and the failing set is byte-for-byte identicaldiff of the two sorted lists is empty. They are all process-spawning tests that the Blacksmith runner cannot support (shell_snapshot, rmcp_client/code_mode/hooks_mcp stdio MCP servers, cli_stream subprocesses, and background_agent_live, which fails with timed out waiting for worker process group N to reap). They are pre-existing environment failures, not regressions from this PR, so the green run excludes exactly those 63 by exact test name — nothing broader.

Earlier run 30118433296 was against the old head e4a94236 and does not count.

@andrei-hasna
andrei-hasna merged commit 0642c39 into main Jul 25, 2026
28 of 32 checks passed
@andrei-hasna
andrei-hasna deleted the factory/task-50811349d94c-9bb5bae1 branch July 25, 2026 06:40
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant