Skip to content

Add persistent SDK sessions, native context controls and durable attachments - #427

Draft
raullopez-sandbox wants to merge 88 commits into
mainfrom
codex/sdk-server-split
Draft

raullopez-sandbox wants to merge 88 commits into
mainfrom
codex/sdk-server-split

Conversation

@raullopez-sandbox

@raullopez-sandbox raullopez-sandbox commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Switch Console uses persistent SDK hosts for local and SSH sessions. Closing Console leaves execution running. The server owns authorization, tenant isolation, leases, recovery epochs, command receipts and answer arbitration. Tmux agent execution is removed.

Reset retains transcript history and starts a fresh native conversation. An interrupted reset or a missing Codex rollout requires an explicit fresh-reset decision; Switch never chooses a replacement conversation or repeats uncertain actions automatically. Recovery persists uploads, acknowledgements and room cursor changes. Process fencing requires confirmed provider-group exit before another host starts. Discovery restores room bindings and isolates failed sessions.

The composer provides searchable model and effort menus plus Context actions. Confirmed model choices persist across restart. Native compaction is exposed only where supported. Picker, paste and drop attachments use authenticated durable references; the execution host downloads and verifies the actual bytes. Skills, MCP, credentials and native configuration load on the execution machine. Discord sender-name rejection uses a safe, stable name with visible agent attribution.

Provider Reset Model selection Native compaction Attachment delivery
Claude Code Yes Catalog and effort When advertised Images as bytes; files staged
Codex Yes Catalog and effort App-server compaction Images and staged file mentions
OpenCode Yes Connected models and variants Session compaction Staged file URLs; model MIME support checked
Gemini CLI Yes ACP catalog Unavailable Image/resource bytes
Cursor Yes ACP catalog Unavailable Image bytes and staged file references

Validation: final provider suite 240 passed; runtime suite 113 passed; Discord/display-name suite 177 passed. Ten recovery tests passed against real PostgreSQL. Provider/runtime types, Console types, affected lint and Discord adapter mypy passed. Earlier isolated local SSH checks covered bundle startup, staging, retries, native skill paths and MCP environment with simulated provider/download inputs.

Live Discord and Console checks verified both channel-creation directions, chat-triggered Claude and Codex sessions, sidebar room binding, transcript replay, unaddressed-message cues, queued delivery during approval, Codex interrupt/reset, and explicit recovery of preserved Claude and Codex failures. A later Codex process crash resumed the same native conversation ID. After fixing the server-reset cursor regression, the original waiting Discord message arrived without being resent. Reserved-name delivery also passed in native Discord with visible attribution. Approvals were answered through Console.

Earlier live provider checks covered context controls, file contents, project skills and MCP with Claude, Codex, OpenCode and Gemini; image recognition passed with Claude, Codex and Gemini. The tested OpenCode model rejects images. Cursor generation remains blocked by its account limit. The current Discord run covers Claude and Codex only.

Remaining limits: the in-memory room stream can detect a server restart only while the old cursor is ahead of the new buffer; it has no persistent generation identity. Legacy delivery acknowledgements without substitution records require server-evidence reconciliation. Discord-only approval cards and publication changes remain on the separate messaging companion branch, which is not merged here. Automatic attachment retention is not implemented. No authenticated external remote host was supplied, so local SSH tests do not establish external deployment readiness.

Review candidate reconstructed from cc3bb22 on e45a620. Preserve the SDK and shared server implementation, move the Python wire contract and projection into sessions, and extract request-card rendering, publication, callbacks and storage into a companion layer. Shared protocol code retains contributions introduced in 5eb9105..0bf7f88. This is an isolated squash candidate, not a rewrite of the active integration branch.
@socket-security

socket-security Bot commented Sep 10, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
License policy violation: npm typescript

License: LicenseRef-W3C-Community-Final-Specification-Agreement - The applicable license policy does not permit this license (5) (package/ThirdPartyNoticeText.txt)

From: console/pnpm-lock.yamlnpm/typescript@5.9.3

ℹ Read more on: This package | This alert | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/typescript@5.9.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Unmaintained: npm fast-sha256 was last published 7 years ago

Last Publish: 1/16/2020, 3:52:26 PM

From: console/pnpm-lock.yamlnpm/@anthropic-ai/claude-agent-sdk@0.3.260npm/fast-sha256@1.3.0

ℹ Read more on: This package | This alert | What are unmaintained packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Package should publish periodic maintenance releases if they are maintained, or deprecate if they have no intention in further maintenance.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/fast-sha256@1.3.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

}
}
const sourceConfig = await optionalText(join(input.sourceHome, 'config.toml'));
const config = { ...(sourceConfig ? parse(sourceConfig) : {}), ...parse(input.config) };

@semgrep-code-sandbox-quantum semgrep-code-sandbox-quantum Bot Sep 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's parse() never returns when a value inside an array or inline table is followed by a comment that ends the document without a trailing newline (for example a=[1 #). The internal skipUntil() helper resets its cursor to the start of the input instead of exiting, so the loop's exit condition becomes unreachable. An attacker who can supply a TOML document to be parsed can block the event loop indefinitely with a few bytes, pinning a CPU core and denying service to the entire process. Serialization via stringify() is unaffected.

Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5190.

Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730

🧹 Fixed in commit 621369a 🧹

skill: 'Switch workflow',
};
const home = await prepareCodexSessionHome(input);
expect(parse(await readFile(join(home, 'config.toml'), 'utf8'))).toEqual({

@semgrep-code-sandbox-quantum semgrep-code-sandbox-quantum Bot Sep 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's parse() never returns when a value inside an array or inline table is followed by a comment that ends the document without a trailing newline (for example a=[1 #). The internal skipUntil() helper resets its cursor to the start of the input instead of exiting, so the loop's exit condition becomes unreachable. An attacker who can supply a TOML document to be parsed can block the event loop indefinitely with a few bytes, pinning a CPU core and denying service to the entire process. Serialization via stringify() is unaffected.

Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5190.

Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730

🧼 Fixed in commit 621369a 🧼

@semgrep-code-sandbox-quantum

Copy link
Copy Markdown

Semgrep found 1 ssc-077b416b-f96a-4ee4-ab72-8118ac663a01 finding:

  • console/packages/agent-providers/src/codex/home.ts

Risk: Affected versions of smol-toml are vulnerable to Loop with Unreachable Exit Condition ('Infinite Loop') / Unchecked Input for Loop Condition. smol-toml's parse() never returns when a value inside an array or inline table is followed by a comment that ends the document without a trailing newline (for example a=[1 #). The internal skipUntil() helper resets its cursor to the start of the input instead of exiting, so the loop's exit condition becomes unreachable. An attacker who can supply a TOML document to be parsed can block the event loop indefinitely with a few bytes, pinning a CPU core and denying service to the entire process. Serialization via stringify() is unaffected.

Fix: Upgrade this library to at least version 1.7.1 at switch/console/pnpm-lock.yaml:5187.

Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-71261, GHSA-7w5x-hrqm-74c2, CVE-2026-85730

@raullopez-sandbox raullopez-sandbox changed the title Use persistent SDK hosts and shared server authority for Console sessions Add persistent SDK sessions, native context controls and durable attachments Sep 10, 2026
raullopez-sandbox and others added 9 commits September 10, 2026 18:03
…rams

The Codex adapter sends an `effort` value with `turn/start`, but the
`CodexTurnStartParams` type did not declare the field. The generated
protocol for codex-cli 0.153.4 has `effort` as an optional reasoning
effort string, which overrides the effort for this turn and the turns
after it.

Add the field to the type. Also assert that a reasoning effort selected
at session start reaches the first turn, not only an effort set later
with `setModel`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
The OpenCode adapter kept the `items`, `emittedText`, `partTypes` and
`messageRoles` maps for the life of the session. When a turn failed,
`completeTurn` walked all of the items and marked every item that was
still in progress as failed against the current turn. A tool left in
progress in an early turn was therefore reported as failed again in
every later turn that failed, and the maps grew without limit.

Clear the four maps when a new turn starts, as the Gemini adapter does.
A steered message joins the running turn, so it keeps the maps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
raullopez-sandbox and others added 5 commits September 11, 2026 11:17
The stream retried every failed open with a backoff. A 401, or a 403 that
names no declared room, carries the same token on each retry, so the loop
ran forever, logged a warning only, and told nobody that the credentials
were refused.

Both statuses now end the stream and the heartbeat and report the refusal
over the eviction callback, which the hosts already turn into a terminal
failure. The message gives the status code. The explicit refused-room case
still drops the room and reopens. Network errors, 5xx and 429 still
reconnect with the existing backoff.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
A 401 or 403 on the heartbeat ends the stream the same way it does on the
stream open. When both fail together the owner is told once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJqrbLGEn65miwfNgZ3JXd
raullopez-sandbox and others added 19 commits September 16, 2026 13:39
Mattermost reports a plain text file as `text/plain; charset=utf-8` and
Discord does the same, so the attachment allowlist — which holds bare
media types — refused every `.txt` relayed from a room with "Unsupported
attachment MIME type" while a PNG, whose type carries no parameter, went
through.

Strip the parameters where the type is derived: on the collaboration
bridge's inbound and outbound attachment models, on the agent protocol's
media upload, and on the value the session service stores and delivers.
`validate_attachment` normalises its input too, so the check and the
stored type cannot disagree.

The allowlist itself already matched the host list in
`console/packages/agent-providers/src/host/attachments.ts`; a new test
pins them together so a later edit to one fails rather than silently
delivering a file the other end refuses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
Outside `full-access` the Antigravity CLI reads only the directories it
was given at launch, so a file fetched by the `download_attachment`
Switch tool was denied with "headless mode cannot ask for permission".
The per-turn grant for staged attachments cannot cover it: the runtime
writes under `~/.switch/sessions/<runtime pid>/media`, keyed by a
process the adapter never sees, and the directory appears in the middle
of the turn that asked for the file.

Grant `~/.switch/sessions` at spawn instead, in every mode that needs
grants. The CLI accepts a directory that does not exist yet, so a
session that never downloads anything is unaffected, and `full-access`
is left alone because it needs no grant at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
A refactor commit swept in a test for behaviour whose implementation is still
uncommitted work in progress, so the branch failed that test on its own. The
test returns to what the branch actually implements; the in-progress change
lands with its implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4xR1K4CdvTmRUPJA4zidf
simonflk-sb and others added 3 commits September 17, 2026 16:02
Four conflicts, resolved as follows.

`gateway/app.py` and `protocol/service.py` were import collisions with no
behavioural content: both sides' imports are kept.

`console/.../main/rpc.ts` gains main's `roomTemplates` controller and keeps
this branch's `sdkHost`. The `sidecar` registration main still carries stays
removed — `main/core/sidecar/` no longer exists on this branch, so restoring
the registration would not resolve.

`test_row_level_security.py` is the substantive one. This branch tightened the
frozen-inventory check to full equality, reaching that by adding the second
scoping migration's table list to the comparison. main instead relaxed the
assertion to one direction, on the ground that a table added to the models
after a revision cannot appear in that revision's frozen list, and covered the
other direction against a real database in
`test_frozen_ddl_matches_create_all.py`. main's reasoning generalises where
enumerating migrations does not, so its assertions win; this branch's combined
inventory is kept as their input, so both frozen lists are still checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main added the invitations revision while this branch added the room-control
follow-up column, so bringing the two together left the chain with two heads
and `upgrade head` ambiguous. An empty merge revision joins them, as the four
earlier head collisions in this directory were joined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pre-existing on this branch rather than a product of the merge: oxfmt
rejects all six at 522da97 and accepts main's copies of the four main also
carries. The terminal removal left them behind, which is why the console
job cannot go green here.

The same six are already formatted downstream on
work/refresh-messaging-platforms at 90df974, and oxfmt's output here is
byte-identical to that copy, so merging this branch upward finds the same
content on both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants