api/routes: claim CLI/TUI/Desktop sessions on first POST, not just GET#4153
api/routes: claim CLI/TUI/Desktop sessions on first POST, not just GET#4153merodahero wants to merge 1 commit into
Conversation
|
| Filename | Overview |
|---|---|
| api/routes.py | Core logic change: extracts _session_index_marks_was_webui, adds _is_claimable_cli_source denylist, adds _claim_or_synthesize_cli_session helper, wires helper into both GET /api/session and POST /api/chat/start; also fixes read_only plumbing on the GET stub and in _handle_session_import_cli. |
| tests/test_chat_start_claim_cli_session.py | New 29-test suite covering static structure checks, source denylist, no-mutation contract, state.db timestamp mapping, read_only plumbing, and the POST claim flow with a realistic in-memory state.db. |
| CHANGELOG.md | Adds five well-scoped [Unreleased] Fixed entries covering the claim path, read_only plumbing, epoch timestamp fix, cron denylist, and diagnostic reason label. |
Reviews (10): Last reviewed commit: "api/routes: claim CLI/TUI/Desktop sessio..." | Re-trigger Greptile
|
Pulled the branch into a read-only worktree and read the full Verified: the denylist matches the agent's actual source valuesThe refused families in
So the allow-by-omission set (cli/tui/desktop) is exactly the locally-owned interactive surfaces, and every foreign-owned family the agent can stamp is in the denylist. The classification holds. The
|
a23bc0b to
da87a73
Compare
|
@greptile review |
|
@nesquena-hermes addressed — docstring updated to match what the code does (created_at is preserved by save(), updated_at is touched to now on the POST claim path, which is the desired UX per your lean), and added the path-leak regression assertion to _sanitize_error's test (feeds an OSError carrying the real SESSION_DIR path, asserts the sanitized string doesn't contain "//"). Also rebased onto current master (ebf778e) to pick up #4155; CHANGELOG conflict resolved by keeping both your v0.51.403 stamp and our [Unreleased] entries. New head SHA: da87a73. Re-ran Greptile, 5/5 holds with no new findings. Thanks for the read-through. |
Squashed from the 13-commit iteration of nesquena#4153 (Greploop iterations 1-3) to a single commit for merge-conflict durability in a fast-moving repo. The per-commit history is preserved in the closed nesquena#3901 PR and in the Greptile review thread on this PR. What this PR does ================= POST /api/chat/start now claims local CLI/TUI/Desktop sessions on the first request, not just on GET, via a shared _claim_or_synthesize_cli_session helper. Previously the POST path created a fresh session on each request, orphaning any state the CLI/TUI/Desktop already had on disk. The same helper backs both the GET /api/session read-only stub and the POST claim path, killing the GET/POST asymmetry that previously caused CLI/TUI/Desktop POST requests to bare-404 on an existing session id. Claim gate ========== _is_claimable_cli_source enforces a denylist of foreign-owned sources: {claude_code, cron, external_agent, gateway, messaging, unknown}. The allow-by-omission set is exactly the locally-owned interactive surfaces (cli / tui / desktop). Cron was added to the denylist per the maintainer's review note so scheduled CLI/cron sessions stay CLI-owned across the next scheduled run. The denylist is verified end-to-end in the tests. Not_claimable arm returns 403 (not 404) so the frontend's 404 empty-state self-heal doesn't strip the URL for a session that exists but is foreign-owned. Correctness hardening ===================== - 500 errors from synth.save() are sanitized via _sanitize_error so internal exception text (absolute sidecar paths) doesn't leak to the client. A one-line regression test pins that the sanitized output doesn't contain a '/' segment from SESSION_DIR. - state.db started_at/ended_at are mapped to created_at/updated_at on the synthesized Session. created_at is preserved by Session.save() (not in the touch list at api/models.py:715,735-736). updated_at is touched to wall-clock now by save() — desired UX per the maintainer's lean ("session was just claimed"). The mapping is load-bearing for the GET read-only stub (which never calls save()) and for created_at on both paths. - read_only is read from the synthesized Session object (not from cli_meta directly) in both the GET response and the import_cli refresh path. Source-refused sessions (messaging, claude_code) now render the composer and 403 on POST instead of falsely rendering read_only=False from a stale cli_meta. - CLI meta is looked up once per POST (dedupe). - CLI-meta enrichment is copy-on-write; the caller's dict is never mutated. 5 Greptile no-mutation tests cover the empty-dict, GET-path, and non-empty cli_meta paths. Tests ===== 32 regression tests in tests/test_chat_start_claim_cli_session.py (29 original + 3 from greploop iter 2) cover every reason branch of the helper, the denylist membership for all six refused source families, the no-mutation contract, the read_only plumbing, the timestamp-mapping invariant, the 500 sanitization path-leak guard, and the original POST-claim flow. CHANGELOG ========= Unreleased entry under [Unreleased] -> ### Fixed covering the GET/POST claim asymmetry fix, the read_only GET stub correction, the epoch-timestamp fix for synthesized sessions, the cron-source denial, and the diagnostic-reason provenance fix. Supersedes the closed nesquena#3901 (force-pushed into an unrecoverable state during reauthor; this is the same content, fresh PR). Reviewed by =========== - Greptile 5/5 - nesquena-hermes: "Reads clean overall."
da87a73 to
936b00f
Compare
|
@greptile review |
|
@nesquena-hermes squashed the 13 PR commits into 1 and rebased onto current master (1643d66) to reduce future conflict surface — the diff is identical to what you reviewed. New Greptile review on the squashed SHA confirms 5/5. No production code touched since your sign-off. |
Squashed from the 13-commit iteration of nesquena#4153 (Greploop iterations 1-3) to a single commit for merge-conflict durability in a fast-moving repo. The per-commit history is preserved in the closed nesquena#3901 PR and in the Greptile review thread on this PR. What this PR does ================= POST /api/chat/start now claims local CLI/TUI/Desktop sessions on the first request, not just on GET, via a shared _claim_or_synthesize_cli_session helper. Previously the POST path created a fresh session on each request, orphaning any state the CLI/TUI/Desktop already had on disk. The same helper backs both the GET /api/session read-only stub and the POST claim path, killing the GET/POST asymmetry that previously caused CLI/TUI/Desktop POST requests to bare-404 on an existing session id. Claim gate ========== _is_claimable_cli_source enforces a denylist of foreign-owned sources: {claude_code, cron, external_agent, gateway, messaging, unknown}. The allow-by-omission set is exactly the locally-owned interactive surfaces (cli / tui / desktop). Cron was added to the denylist per the maintainer's review note so scheduled CLI/cron sessions stay CLI-owned across the next scheduled run. The denylist is verified end-to-end in the tests. Not_claimable arm returns 403 (not 404) so the frontend's 404 empty-state self-heal doesn't strip the URL for a session that exists but is foreign-owned. Correctness hardening ===================== - 500 errors from synth.save() are sanitized via _sanitize_error so internal exception text (absolute sidecar paths) doesn't leak to the client. A one-line regression test pins that the sanitized output doesn't contain a '/' segment from SESSION_DIR. - state.db started_at/ended_at are mapped to created_at/updated_at on the synthesized Session. created_at is preserved by Session.save() (not in the touch list at api/models.py:715,735-736). updated_at is touched to wall-clock now by save() — desired UX per the maintainer's lean ("session was just claimed"). The mapping is load-bearing for the GET read-only stub (which never calls save()) and for created_at on both paths. - read_only is read from the synthesized Session object (not from cli_meta directly) in both the GET response and the import_cli refresh path. Source-refused sessions (messaging, claude_code) now render the composer and 403 on POST instead of falsely rendering read_only=False from a stale cli_meta. - CLI meta is looked up once per POST (dedupe). - CLI-meta enrichment is copy-on-write; the caller's dict is never mutated. 5 Greptile no-mutation tests cover the empty-dict, GET-path, and non-empty cli_meta paths. Tests ===== 32 regression tests in tests/test_chat_start_claim_cli_session.py (29 original + 3 from greploop iter 2) cover every reason branch of the helper, the denylist membership for all six refused source families, the no-mutation contract, the read_only plumbing, the timestamp-mapping invariant, the 500 sanitization path-leak guard, and the original POST-claim flow. CHANGELOG ========= Unreleased entry under [Unreleased] -> ### Fixed covering the GET/POST claim asymmetry fix, the read_only GET stub correction, the epoch-timestamp fix for synthesized sessions, the cron-source denial, and the diagnostic-reason provenance fix. Supersedes the closed nesquena#3901 (force-pushed into an unrecoverable state during reauthor; this is the same content, fresh PR). Reviewed by =========== - Greptile 5/5 - nesquena-hermes: "Reads clean overall."
936b00f to
3f3d2ae
Compare
Restore POST-path session claim for CLI/TUI/Desktop sources; gate bare gateway/unknown; map state.db timestamps; sanitize 500s.
Why
CLI, TUI, and Desktop sessions were being recreated on every POST to
/api/chat/startinstead of being claimed from the on-disk state, so the WebUI lost context between requests. The denylist forgateway/unknownsources, the 500-error sanitization, and thestate.dbtimestamp mapping are Greptile-driven hardening around the same claim path.What changed
api/routes.py_claim_or_synthesize_cli_session(~L3355) now claims local CLI/TUI/Desktop sessions on the first POST to/api/chat/start, not just on the GET path.gatewayandunknownsources at the claim path; they have no claimable backing store and would otherwise 500.state.dbcreated_at/updated_atare mapped into the session metadata response (was previously missing on synthesized CLI/TUI/Desktop sessions).cli_meta.tests/test_chat_start_claim_cli_session.py(new, 29 tests, 973 lines)test_helper_refuses_bare_gateway_session,test_helper_refuses_bare_unknown_session.test_helper_does_not_mutate_callers_cli_meta,test_helper_does_not_mutate_callers_cli_meta_when_empty.read_onlyplumbing: GET stub + POST 403 path.state.dbtimestamp mapping.CHANGELOG.md[Unreleased]->### Fixed.Testing
tests/test_chat_start_claim_cli_session.py.Risk / rollback
The change is gated by the source denylist, so it only activates on write-claimable local sources (CLI/TUI/Desktop). WebUI-originated sessions are unaffected. Rollback = revert the merge commit.
Supersedes the closed #3901 (force-pushed into an unrecoverable state during reauthor; this is the same content, fresh PR).