@agentrouter-top/relay-dsh-plugin-codex 0.2.2-agentrouter.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ # Codex Reliability Specification
2
+
3
+ Status: Accepted for implementation
4
+
5
+ This specification defines the user-visible and safety-critical behavior of
6
+ `relay-dsh-plugin-codex`. The official DSH checkout remains unmodified.
7
+
8
+ ## App Server ownership and connection state
9
+
10
+ The plugin Host process owns one Codex App Server child process. It starts the
11
+ child while the DSH Host activates the plugin, before Codex models are used,
12
+ and stops it when the plugin is disposed or DSH exits. The default launcher is
13
+ the pinned `@openai/codex` package and its platform optional dependency. A
14
+ global `codex` command is not required. `codexCommand` overrides
15
+ `RELAY_CODEX_COMMAND`, which overrides the bundled launcher.
16
+
17
+ The default launcher disables Codex `features.shell_snapshot`. Shell commands still
18
+ receive the effective Codex child environment; Relay does not turn secret delivery
19
+ into transcript text or strip variables merely because their names contain `KEY`,
20
+ `SECRET`, or `TOKEN`. Disabling snapshots prevents Codex from serializing that complete
21
+ effective environment into durable files under `CODEX_HOME/shell_snapshots`. Explicit
22
+ operator-supplied App Server arguments remain an exact override and carry responsibility
23
+ for any snapshot policy they enable.
24
+
25
+ ## Plugin Hook trust propagation
26
+
27
+ Codex evaluates installed Plugin Hooks when each Thread is started, forked, or
28
+ resumed. App Server launch arguments and Thread request configuration are separate
29
+ configuration layers. Relay therefore mirrors an explicit standalone
30
+ `--dangerously-bypass-hook-trust` launch argument into
31
+ `config.bypass_hook_trust: true` on `thread/start`, `thread/fork`, and
32
+ `thread/resume`, while preserving the operator's launch argument array exactly.
33
+
34
+ This is an opt-in security exception. Relay never enables Hook trust bypass by
35
+ default, never recognizes the flag as a substring of another argument, and never
36
+ propagates unrelated launch configuration into Thread requests. Restarting without
37
+ the exact flag removes the request-level override. Existing realtime, dynamic-tool,
38
+ permission, resume, and fork settings remain unchanged.
39
+
40
+ The observable state machine is:
41
+
42
+ | State | Meaning | Required user behavior |
43
+ | --- | --- | --- |
44
+ | `not-started` | Plugin is loaded but start has not begun. | Wait for DSH startup. |
45
+ | `starting` | Child spawn and App Server initialization are in progress. | Wait; do not create a Thread. |
46
+ | `connected` | Initialize and model discovery succeeded. | Codex conversations may run. |
47
+ | `connection-failed` | A child existed or was attempted, but protocol initialization, connection, or process lifetime failed. | Preserve bindings; show restart/authentication diagnostics. |
48
+ | `unavailable` | The executable, bundled platform runtime, or supported platform is unavailable. | Show reinstall or absolute-path configuration guidance. |
49
+ | `rebind-required` | A DSH fork could not establish its App Server child binding safely. | Preserve provenance and retry Fork from the original Session after fixing the condition. |
50
+
51
+ User-facing status must never expose raw `spawn codex ENOENT`. Stable error
52
+ codes include `CODEX_EXECUTABLE_NOT_FOUND`, `CODEX_RUNTIME_MISSING`,
53
+ `CODEX_PLATFORM_UNSUPPORTED`, `CODEX_APP_SERVER_NOT_RUNNING`,
54
+ `CODEX_APP_SERVER_CONNECTION_FAILED`, and `CODEX_REBIND_REQUIRED`.
55
+
56
+ ## Shell environment persistence
57
+
58
+ A Host-only environment secret may be inherited by an intended shell consumer without
59
+ appearing in DSH messages, Codex rollout events, plugin diagnostics, or regular Codex
60
+ state files. The Relay default must create no shell snapshot at all, because name-based
61
+ redaction cannot identify every secret and filtering the command environment would break
62
+ legitimate consumers. Restart and resume preserve the Session/Thread binding without
63
+ backfilling a snapshot for an earlier or later command.
64
+
65
+ ## Backend model selection
66
+
67
+ For a blank DSH Session, the selected Agent preset determines the model
68
+ provider group:
69
+
70
+ - `relay-codex` selects `relay-codex` and its default model/default reasoning
71
+ effort;
72
+ - `relay-claude` is never rewritten by the Codex coordinator;
73
+ - leaving Codex for a native preset selects a provider group that is neither
74
+ Codex nor Claude.
75
+
76
+ Only the newest preset generation may select a model. Model discovery may be
77
+ retried with bounded delays while the App Server becomes ready. A non-blank
78
+ Session is never rewritten by this synchronization.
79
+
80
+ ## Workspace Thread discovery and selective import
81
+
82
+ The provider-neutral `relay-dsh-plugin-session-import` hub owns the sidebar
83
+ footer's single **Import sessions...** entry. Codex contributes one explicit
84
+ **Import from Codex** menu row through `relay.session-import.provider`; it owns
85
+ no standalone footer trigger. Selecting that row presents a visible Workspace
86
+ selector. The current Session owner,
87
+ then the recent Workspace, is only an initial choice. The user may change it,
88
+ and the plugin must not scan until the user invokes **Scan sessions**.
89
+
90
+ The import scan lists only Codex Threads whose canonical `cwd` belongs to the
91
+ explicitly selected DSH Workspace and whose binding state is `ready` or `recoverable`.
92
+ Each candidate exposes its complete Codex Thread id, deterministic title, canonical
93
+ path, App Server `updatedAt`, and binding status. Candidates are unique and ordered
94
+ by source activity time, with Thread id as the deterministic tie-breaker. Already
95
+ bound Threads remain part of aggregate counts but are never selectable; Threads
96
+ from another Workspace are neither counted nor disclosed.
97
+
98
+ The UI defaults to all eligible candidates and supports selecting one, several,
99
+ all, or none. An empty selection cannot be submitted. The import request carries
100
+ the exact selected Thread ids. Before creating or changing any DSH Session, the
101
+ Host rescans the Workspace and validates the entire selection for non-empty unique
102
+ ids, current Workspace membership, and unbound or recoverable state. Any unknown,
103
+ duplicate, cross-Workspace, or newly-bound id rejects the whole request without a
104
+ partial mutation. A request that omits `threadIds` retains the previous import-all
105
+ Host API behavior for compatible clients; an explicit empty array is invalid.
106
+
107
+ Recoverable imports remain idempotent. After a successful selective import, DSH
108
+ refreshes Sessions before Workspace membership so the imported Session appears
109
+ with the original Codex title and source activity ordering.
110
+
111
+ ## Thread binding and forks
112
+
113
+ One DSH Session binds at most one Codex Thread, and one Codex Thread binds at
114
+ most one DSH Session. A persisted binding is never deleted merely
115
+ because `thread/resume` fails. Active-writer and transient failures retain the
116
+ binding for retry. A missing Thread enters `rebind-required`.
117
+
118
+ DSH forks inherit assistant messages and their Codex `replayState`. When an
119
+ unbound child contains an original `threadId` and completed `turnId`, and that
120
+ Thread is still owned by another DSH Session, the plugin calls App Server
121
+ `thread/fork` with `threadId` and `lastTurnId`. It persists the returned new
122
+ Thread as the child's one-to-one binding before starting the child Turn. It
123
+ never writes the child continuation to the parent Thread.
124
+
125
+ Missing Turn provenance, an unowned or rebind-required source, an in-progress
126
+ Turn, an App Server rejection, or an invalid fork response enters
127
+ `CODEX_REBIND_REQUIRED`. Diagnostics retain the original Thread and, when
128
+ available, Turn and Item ids. These paths perform neither `thread/start` nor
129
+ `turn/start`, and the plugin must not silently create a replacement Thread.
130
+ Retrying the same provenance may retry `thread/fork`; it still cannot fall back
131
+ to fresh Thread creation.
132
+
133
+ ## Approval provenance and reconnect
134
+
135
+ Every App Server approval is owned by this tuple:
136
+
137
+ `(DSH Session id, Codex Thread id, Turn id, Item id, App Server request id, binding epoch)`.
138
+
139
+ The tuple is captured before asking DSH for approval and validated again after
140
+ the user decision but before responding to Codex. Detach, rebind state, binding
141
+ replacement, request identity change, or provenance mismatch makes the
142
+ approval stale. A stale approval is rejected with `CODEX_STALE_APPROVAL`; it is
143
+ never accepted or routed to another Thread. The diagnostic names the original
144
+ Thread, Turn, and Item.
145
+
146
+ DSH may replay the same still-pending approval rpc id after a browser
147
+ disconnect. That replay is safe only while the ownership tuple remains valid.
148
+
149
+ ## Subagent interaction ownership
150
+
151
+ A Codex subagent Thread does not receive an independent DSH Session binding. While
152
+ the root Turn is active, App Server emits `subAgentActivity` items whose enclosing
153
+ `threadId` is the parent and whose `agentThreadId` is the spawned child. The adapter
154
+ records those observed edges with the root binding epoch. Thread inventory metadata
155
+ is not treated as authorization because it may be absent before the interaction or
156
+ outlive the Turn that created the child. Dynamic tools, approvals,
157
+ and structured questions from a descendant may use the root DSH Agent only after the
158
+ adapter proves an acyclic observed parent chain from the requesting
159
+ Thread to the currently bound root Thread.
160
+
161
+ Resolution never uses cwd, title, recency, or model as ownership evidence. An unknown
162
+ Thread, missing parent, cycle, inconsistent shared Session, unbound root, disposed DSH
163
+ Agent, changed binding epoch, or rebind-required root fails closed. A descendant uses
164
+ only the DSH tool names captured for the owning root Turn; it cannot gain a capability
165
+ that was absent from that Turn. Observed edges are released when the root Turn ends or
166
+ the DSH Agent detaches. Conflicting observations permanently reject that child identity.
167
+
168
+ ## Reasoning summary presentation
169
+
170
+ Business Turns request App Server reasoning summaries with `summary: auto`.
171
+ When Codex supplies a summary, the adapter projects its public summary deltas as
172
+ one DSH reasoning block that remains distinct from the final answer. It never
173
+ projects encrypted or raw hidden reasoning content.
174
+
175
+ An App Server reasoning item with no public summary produces no DSH reasoning
176
+ block rather than an empty `Think` disclosure. Ephemeral title and compaction
177
+ Turns explicitly use `summary: none`; their internal work is not added to the
178
+ business conversation and does not incur a presentation-only summary.
179
+
180
+ ## Image projection and failure isolation
181
+
182
+ Codex `imageView` and `imageGeneration` items are admitted according to their
183
+ encoded byte signature, not a local filename extension or unverified data-URI
184
+ declaration. PNG, JPEG, GIF, and WebP signatures map to the corresponding DSH
185
+ media type. The DSH attachment store remains the authority for full decode,
186
+ normalization, size, and pixel-limit validation.
187
+
188
+ A filename such as `completed-clean.png` may therefore produce an
189
+ `image/jpeg` attachment when its bytes are JPEG. Workspace and generated-image
190
+ root checks still run before any local file is read; byte detection does not
191
+ expand the allowed filesystem boundary.
192
+
193
+ Image preview admission and storage are projection concerns. Failure of one
194
+ image emits one terminal text placeholder and a Host warning containing only a
195
+ stable reason code and the owning Thread, Turn, and Item identifiers. Raw
196
+ storage errors and absolute paths are not projected or logged. Projection then
197
+ continues through later Codex items and the source Turn's terminal status. It
198
+ must not throw out of the adapter stream, mark an otherwise successful DSH Turn
199
+ as failed, or interrupt the backing Codex Thread.
200
+
201
+ A completed App Server `mcpToolCall` may carry standard MCP image entries in
202
+ `result.content`. Each `type: image` entry is decoded independently in content order,
203
+ limited to 25 MiB, and admitted only when its declared supported MIME exactly matches
204
+ the encoded PNG, JPEG, GIF, or WebP signature. Text, resources, and
205
+ `structuredContent` are not reinterpreted as images. Valid bytes are saved directly
206
+ through the owning DSH attachment service with deterministic sanitized names; no
207
+ temporary Workspace file is created.
208
+
209
+ Malformed base64, unsupported or mismatched media, oversized data, and attachment
210
+ storage rejection follow the same failure-isolation contract: one sanitized placeholder
211
+ and stable warning reason per failed image, followed by the remaining MCP images and
212
+ the source Turn's final answer. Raw base64, storage errors, and private paths are never
213
+ logged or projected.
214
+
215
+ ## DSH image input transport
216
+
217
+ DSH user image blocks normally contain a content-addressed attachment reference,
218
+ not a local path. Before creating or resuming a Codex Thread, the adapter reads
219
+ each image through DSH's attachment service, preserves message order, and verifies
220
+ the encoded PNG, JPEG, GIF, or WebP signature. Encoded bytes are authoritative
221
+ when stored metadata or the display name disagrees.
222
+
223
+ Verified bytes are materialized outside the Workspace under
224
+ `$CODEX_HOME/dsh-input-images` (or the default `~/.codex` equivalent). Files use
225
+ their SHA-256 digest plus a signature-derived extension, directories are private,
226
+ and writes are atomic without replacing an existing digest. Repeated immutable
227
+ attachments reuse the same verified path. The Workspace is not modified.
228
+
229
+ The resulting path is sent through App Server `turn/start` as native
230
+ `localImage` input and attachment metadata. Multiple images retain DSH order and
231
+ pure-image messages are valid. Existing trusted path-backed image blocks remain
232
+ supported.
233
+
234
+ Missing/corrupt attachments, unavailable attachment service, invalid bytes,
235
+ oversized data, and cancellation fail before a Codex Thread or Turn starts. They
236
+ use stable `CODEX_IMAGE_*` codes and never silently degrade an image-bearing user
237
+ message to text-only input.
238
+
239
+ ## Turn interruption and process cleanup
240
+
241
+ Stopping a DSH Codex Turn must stop both model generation and every active App
242
+ Server background terminal owned by that Turn. Before sending `turn/interrupt`,
243
+ the runtime identifies the Turn's in-progress `commandExecution` item ids and
244
+ terminates only matching `thread/backgroundTerminals` process ids. It repeats
245
+ discovery after interruption to close races and confirms that no matching
246
+ terminal remains.
247
+
248
+ Background terminals owned by another Turn are not terminated. The plugin must
249
+ not use the thread-wide background-terminal cleanup operation for an ordinary
250
+ Turn stop.
251
+
252
+ The Turn is reported as aborted only after targeted cleanup and
253
+ `turn/interrupt` succeed. If cleanup cannot be confirmed, the DSH Turn ends with
254
+ `CODEX_TURN_INTERRUPT_CLEANUP_FAILED`, tells the user to check for late Workspace
255
+ side effects, and logs only the stable code plus Thread and Turn identifiers.
256
+
257
+ ## Command output streaming
258
+
259
+ App Server shell output belongs to the user-visible Codex response even though the
260
+ command is executed inside Codex rather than by the DSH tool dispatcher. Code mode
261
+ returns the first yielded bytes in a raw `custom_tool_call_output`, while later PTY
262
+ bytes also arrive as native `item/commandExecution/outputDelta` notifications. New
263
+ durable plugin-owned Threads opt into raw response items; ephemeral auxiliary Threads
264
+ do not. The runtime never forwards a raw item: it correlates only `exec` call/output pairs, parses structured text results, and
265
+ projects only a non-empty result containing `session_id`, `wall_time_seconds`, and
266
+ `output`. Raw messages, prompts, reasoning, encrypted content, unrelated tools,
267
+ malformed results, and completed results without a live `session_id` remain private.
268
+
269
+ The adapter correlates the sanitized first yield and native command notifications by
270
+ `session_id`/`processId`, then retains one reconciled activity output. Mirrored raw
271
+ and native output is deduplicated while repeated native output remains repeated.
272
+ It must not emit an executable DSH `tool-call` stream chunk, because doing so would
273
+ ask the DSH Agent to execute the already-running command a second time. Instead,
274
+ presentation-only calls/results use native persistence envelopes with the
275
+ `relay_codex_activity` name and validated activity metadata.
276
+
277
+ All deltas for one process share one buffer and retain App Server order. For
278
+ native-only commands, the completed item's `aggregatedOutput` supplies the settled
279
+ snapshot. Code-mode output also reconciles sanitized first yields with the native
280
+ PTY side. A completed item with no preceding delta retains its non-empty aggregate.
281
+ Empty output stays empty. Late deltas after completion are ignored. Cancellation
282
+ also drains already-owned command notifications received during the interrupt RPC
283
+ before settling any remaining activity as failed.
284
+
285
+ Bounded command output is persisted in the native tool result's activity metadata,
286
+ separate from assistant commentary and the final answer. The grouped presentation
287
+ shows it as literal text in an expandable panel, never assistant Markdown. Native
288
+ persistence vocabulary survives Session reload without introducing plugin-private
289
+ mandatory event types. See [Execution presentation](spec/execution-presentation.md)
290
+ for ordering, ownership, legacy fallback, and file-delivery acceptance.
291
+
292
+ `experimentalRawEvents` is immutable App Server Thread creation state in the pinned
293
+ runtime: resume, settings update, and fork cannot enable it for a Thread created by an
294
+ older plugin version. Such Sessions continue to receive native command deltas, but
295
+ complete first-yield streaming requires a new DSH Session created after this feature
296
+ is installed. The plugin must not silently replace or summarize an existing Codex
297
+ Thread because that would weaken its model-context continuity.
298
+
299
+ ## Platform contract
300
+
301
+ The bundled launcher supports darwin, linux, and win32 on arm64 and x64 using
302
+ the matching `@openai/codex-<platform>-<arch>` package. Commands are spawned
303
+ directly with an argument array and never through a shell, so spaces and
304
+ Windows backslashes remain literal. CI runs launcher, App Server client, and
305
+ status/error tests on macOS, Windows, and Linux.
@@ -0,0 +1,168 @@
1
+ # DSH Interaction Bridge Specification
2
+
3
+ ## Scope
4
+
5
+ Codex App Server can pause a Turn to request approval for a command, file
6
+ change, or permission profile, or to ask the user a structured question. The
7
+ plugin routes those requests through the owning DSH Session so DSH remains the
8
+ authority for human interaction and conversation continuity.
9
+
10
+ ## Composition contract
11
+
12
+ `approval` and `userQuestions` are required Host injections. They are provided
13
+ by sibling plugins in the official DSH composition, so listing them in the Host
14
+ plugin's exported `inject` array binds them into the Codex consumer fiber and
15
+ makes activation wait for both services.
16
+
17
+ The bridge must not read either service as an undeclared context property. It
18
+ must not make either dependency optional, and it must not bypass DSH approval or
19
+ question handling when a service is missing, cancelled, or fails. Failure is
20
+ closed: no App Server request is accepted and no protected operation executes.
21
+
22
+ ## Request ownership
23
+
24
+ Every modern request carries a Codex `threadId`; legacy command and patch
25
+ requests carry the same identity as `conversationId`. The adapter normalizes
26
+ both forms and must resolve that id to one live DSH Session and Agent before
27
+ invoking a DSH interaction service. Modern `itemId` and legacy `callId` are
28
+ normalized into the same ownership slot.
29
+
30
+ Subagent requests carry the descendant Thread id rather than the root Thread id.
31
+ During the root Turn, the adapter observes `subAgentActivity.agentThreadId` as the
32
+ child of the notification's enclosing `threadId` and records that edge with the
33
+ current root binding epoch. App Server inventory can also identify the root through
34
+ shared `sessionId` and each edge through `parentThreadId`, but that durable inventory
35
+ is not authorization because it may be absent before the request or outlive the Turn.
36
+ The adapter must prove the complete acyclic observed parent chain to a currently
37
+ bound root before routing a descendant dynamic tool, approval, or question through
38
+ that root's DSH Agent.
39
+ Matching cwd, title, model, or recency is never sufficient ownership evidence.
40
+
41
+ An observed edge is valid only for its root Turn and binding epoch. Turn completion,
42
+ Agent detach, conflicting parent observations, a missing edge, or a root rebind makes
43
+ the descendant fail closed without DSH tool execution.
44
+
45
+ Approval ownership includes the DSH Session, Codex Thread, Turn, Item, App
46
+ Server request id, and binding epoch. Unknown, stale, re-bound, or unowned
47
+ requests are rejected without asking the user or executing the operation.
48
+
49
+ ## Approval mapping
50
+
51
+ For command, file-change, and permission approval requests:
52
+
53
+ | DSH outcome | App Server response |
54
+ | --- | --- |
55
+ | `allowed-once` | `accept` for the current request |
56
+ | `rejected` | `decline` |
57
+ | `cancelled` | `decline` |
58
+ | `unavailable` | `decline` |
59
+
60
+ The requested command or App Server reason is shown through DSH's approval
61
+ service. The protected operation must not begin before `allowed-once` returns.
62
+ Thrown service errors reject the pending App Server request and never become an
63
+ implicit allow.
64
+
65
+ ## Question mapping
66
+
67
+ For `item/tool/requestUserInput`, the bridge maps at most three Codex questions
68
+ to DSH questions, waits for `userQuestions.ask()`, and maps selected and custom
69
+ answers back to App Server. Cancellation or provider failure rejects the
70
+ pending request. Unsupported interaction methods are rejected.
71
+
72
+ ## Activity presentation contract
73
+
74
+ Official DSH conversation rendering remains unmodified. New Codex runtime
75
+ activities use the official `assistant/message` (tool-call block), `tool/call`,
76
+ and `tool/result` envelopes with the plugin-owned tool name
77
+ `relay_codex_activity`. Versioned activity data travels in call arguments and
78
+ result `meta.codexActivity`. The plugin renders that name through the official
79
+ `tool.call.toolview` slot. This records work executed by Codex; it does not
80
+ register or dispatch an additional DSH tool. Calls have matching result messages,
81
+ carry the current DSH Turn/Step, and use Thread/Turn/Item-scoped call IDs.
82
+
83
+ Compatibility was checked against official DSH commit
84
+ `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`. Its persistence reader refuses unknown
85
+ event types unless `ignorable: true` is on the envelope, but its public
86
+ `Session.append()` does not accept that marker. Type declaration merging and
87
+ client conversation registration do not extend the persistence vocabulary.
88
+ New writes MUST NOT use `relay-codex/activity` or patch the official registry.
89
+
90
+ The legacy `relay-codex-activity` chat node remains read-only compatibility for
91
+ old logs. `scripts/repair-activity-history.mjs` defaults to dry-run; with DSH
92
+ stopped, `--write` backs up the original bytes and atomically adds only the
93
+ `ignorable: true` envelope marker to validated legacy activity events. It preserves
94
+ all sequence numbers, timestamps, payloads, and other records, handles every zstd
95
+ frame (with a separate header frame), is idempotent, and refuses malformed or
96
+ torn input. No other unknown event is made ignorable. Backups stay beside the
97
+ private session log, outside the repository.
98
+
99
+ Recovery must audit the complete configured session root without a modification
100
+ date or workspace filter. `--root <sessions-directory>` discovers canonical
101
+ plaintext/zstd logs across workspaces, excluding backups and symlinks. Batch
102
+ repair refuses to write if any log could not be inspected and rescans the root
103
+ after applying fixes; acceptance requires no unmarked legacy activity events
104
+ and no scan errors. Fixing only the first reported Session is not sufficient.
105
+
106
+ Assistant-facing text and reasoning keep using DSH's native text and reasoning
107
+ blocks. Command output, file changes, image views/generations, MCP tool calls,
108
+ web searches, plans, and future non-message App Server items must not be
109
+ serialized into assistant markdown. The adapter must append one started activity
110
+ when an item starts and one completed activity when it settles. Unsettled calls
111
+ are closed as failures when the stream terminates. Completed command
112
+ activities carry bounded output in their event payload so the UI can show it in
113
+ an expandable shell/detail panel without changing the assistant message body.
114
+ Command activity labels distinguish running from completed work, prefer Codex's
115
+ structured command actions (read/search/list), and retain the full command only
116
+ in expandable details. Consecutive activities are grouped in presentation; naming
117
+ each individual call `Ran commands` is not grouping. See
118
+ [Execution presentation acceptance](execution-presentation.md).
119
+
120
+ When App Server exposes duplicate command output through both legacy
121
+ `codeModeShell/outputDelta` and modern `commandExecution/outputDelta`
122
+ notifications, the adapter keeps a single reconciled activity output. Late
123
+ output deltas after item completion are ignored.
124
+
125
+ Unknown App Server item types that are not user, assistant, or reasoning items
126
+ must also become activity rows. This fail-open-for-presentation rule prevents new
127
+ Codex tool surfaces from falling back into MarkdownText while preserving DSH as
128
+ the owner of conversation layout.
129
+
130
+ ## Verification contract
131
+
132
+ 1. A unit contract test fails when either required Host injection is absent.
133
+ 2. A Cordis composition test mounts interaction services as sibling providers,
134
+ mounts the Codex consumer with its exported `inject`, and completes one
135
+ approval plus one question request.
136
+ 3. Handler and runtime tests cover allow, deny, permission, answer, stale
137
+ ownership, unknown Session, and unsupported request mappings.
138
+ 4. An official DSH Web acceptance test uses two independent Codex Sessions.
139
+ Both request the same class of outside-Workspace write. Before either answer,
140
+ both target files are absent and the sentinel is unchanged. One-time allow
141
+ creates only the allow target with exact bytes. Reject leaves the deny target
142
+ absent. Both Sessions finish normally and remain usable.
143
+ 5. The historical pre-fix plugin commit must reproduce the missing approval UI
144
+ and automatic fail-closed response for the same request shape.
145
+ 6. Subagent tests emit App Server-shaped `subAgentActivity` notifications while the
146
+ root Turn remains live, cover direct and nested descendants plus duplicate,
147
+ conflicting, unbound, orphaned, cyclic, stale, expired, and cross-Session trees,
148
+ and assert rejected trees execute zero DSH tools. An official DSH Web regression
149
+ proves one child reads the exact oracle through the DSH tool bridge and returns it
150
+ to the owning parent without a parent-side read.
151
+ 7. Codex adapter tests prove command output, mixed file/image/MCP/search items,
152
+ duplicate legacy/native command streams, empty outputs, and late deltas are
153
+ emitted as bounded native tool events and never as assistant text deltas.
154
+ 8. Codex client tests prove the plugin registers the activity conversation
155
+ definition for legacy reads, injects the native keyed tool view, and renders collapsed
156
+ activity rows plus expandable shell/details without markdown interpretation.
157
+ 9. Real official JSONL persistence tests cover both plaintext and zstd: stream a
158
+ tool through the adapter, persist it, dispose the writer, load using a fresh
159
+ reader, and reopen twice. Assert exact event preservation, known event types,
160
+ call/result pairing, coordinates, output, and final assistant text.
161
+ 10. Legacy repair tests reproduce `SessionFormatUnsupportedError` before repair,
162
+ verify exact backup bytes, then cold-load through official persistence.
163
+ Verify dry-run, idempotence, malformed/torn refusal, and unchanged unrelated
164
+ unknown events. Component tests cover native running-to-settled updates,
165
+ result-only history pages, interruption, and malformed payload fallback.
166
+ 11. Root scan tests include an old-dated log, multiple workspaces, both physical
167
+ encodings, an already repaired log, backups, symlinks, and corrupt input.
168
+ No date or workspace selection may omit an affected canonical log.
@@ -0,0 +1,103 @@
1
+ # Codex Execution Presentation
2
+
3
+ ## Scope and baseline
4
+
5
+ Plugin-only integration against immutable official DSH
6
+ `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`. DSH continues to own navigation,
7
+ composer, history, attachments, and execution controls. No official source edits,
8
+ new mandatory persistence event types, PR, or private session logs in this repo.
9
+
10
+ The supplied 2026-08-30 recording is the behavioral reference: readable progress
11
+ paragraphs interleave with grouped tool activity; only the current activity is
12
+ foregrounded while tools run; the process can collapse when the answer is ready.
13
+ The current one-row-per-command implementation is not accepted as grouping.
14
+
15
+ ## Delivery slices and review gates
16
+
17
+ 1. Projection: reconstruct an ordered, turn-scoped presentation from native
18
+ chunks and native tool envelopes. Tool messages must not finalize or erase
19
+ streamed commentary. Keep text, reasoning, images, and tool data distinct.
20
+ Review ordering, incremental versus full replay, duplicates, and ownership.
21
+ 2. UI: group adjacent tool activities between text/image boundaries; expose one
22
+ summary/current action by default and all individual details on expansion.
23
+ Use stable keys, semantic labels, truthful status, bounded shell panels,
24
+ accessible disclosure controls, and existing DSH design primitives.
25
+ Review long labels, interrupted/error work, images, and foreign-provider views.
26
+ 3. Integration: build/typecheck/full tests plus official persistence round trips,
27
+ restart and history reload. Review loader compatibility and source cleanliness.
28
+ 4. Live acceptance: submit exactly `当前Relay项目还有哪些逻辑没有实现为DSH插件?`
29
+ in the Relay workspace using `gpt-5.6-sol` with `high` reasoning. Capture live,
30
+ expanded-detail, final-answer, and reopened-history screenshots. Record model
31
+ and actual outcome; do not substitute a synthetic preview for live evidence.
32
+
33
+ Each slice needs its own test results and review before delivery. Synthetic tests
34
+ cover nondeterministic branches; the real question is the final acceptance, not
35
+ a speed comparison with an independent Codex App run.
36
+
37
+ ## Acceptance scenarios
38
+
39
+ | ID | Scenario | Required observation |
40
+ | --- | --- | --- |
41
+ | EXEC-01 | Commentary, ten tools, commentary, more tools, answer | Commentary appears while running, in source order; two compact tool groups, not ten top-level rows |
42
+ | EXEC-02 | Streaming commentary followed by native tool request | Tool-only assistant message cannot erase or freeze commentary |
43
+ | EXEC-03 | Read/search/list, shell, edit, image and unknown tools | Semantic current label and category icon; unknown tool remains inspectable |
44
+ | EXEC-04 | Open a group and child while new items arrive | Stable disclosure state; command/output retains newlines and is never Markdown |
45
+ | EXEC-05 | Concurrent tools, nonzero exit, cancellation | Active count is truthful; error is not green success; unresolved calls settle on termination |
46
+ | EXEC-06 | Final text-only answer and answer after tools | Answer remains visible; process collapses without losing history; reopenable process |
47
+ | EXEC-07 | Images and consecutive file edits | Images use DSH attachment rendering; edits remain expandable and ordered |
48
+ | EXEC-08 | Refresh/reopen/cold-load plaintext and multi-frame zstd | Same logical order and grouping, no duplicate text or unsupported event error |
49
+ | EXEC-09 | Non-Codex session and mixed-provider history | Native rendering and controls unaffected; ownership is turn-scoped |
50
+ | EXEC-10 | Desktop/narrow viewport, long path, keyboard disclosure | No horizontal overflow/overlap; Enter/Space disclosure works; status is not color-only |
51
+ | EXEC-11 | Exact user question, Sol High | Real execution visibly meets live grouping and commentary criteria; retained screenshots |
52
+ | EXEC-12 | Cancel after a command emitted partial output | Failed terminal result retains all buffered output within the existing output limit |
53
+ | EXEC-13 | Imported native tools or foreign-provider steps between Codex paragraphs | Preserve native chronological rendering where a complete grouped projection is unavailable |
54
+ | EXEC-14 | Generated image before final prose, or image-only completion | Deliverable remains visible after process collapse; viewing an image remains tool activity |
55
+ | EXEC-15 | Closing answer references a known produced file | Preserve the native closing-turn resolver; completed structured Codex edits also resolve through the native DSH opener |
56
+ | EXEC-16 | Replacement mount fails or plugin unloads | Native assistant and tool content remains available or is restored; no invisible conversation |
57
+ | EXEC-17 | Pack and clean-install in isolated official DSH profile | Install the actual tarball, not a workspace link; execute its browser loader without missing modules; verify installed bundle identity |
58
+ | EXEC-18 | Upgrade published 0.1.4 to candidate, restart | Existing native history stays readable, new grouped history reloads; installed client is the candidate, not stale same-version package content |
59
+ | EXEC-19 | Native DSH, Codex-only, Claude-only and combined profiles | All selected loaders execute; native and Claude histories do not receive Codex process ownership |
60
+ | EXEC-20 | Keyboard and real 390 x 844 viewport | Enter and Space toggle process/group/child disclosures; focus remains visible; no horizontal document overflow |
61
+ | EXEC-21 | Real edit, image-view and produced-file workflow | Actual file content changes; edit and image categories render; final file reference opens via native DSH; viewed image bytes decode |
62
+ | EXEC-22 | Browser failure/cancel, cold replay and image delivery | Explicit terminal states and retained partial output; no duplicate activities after reload; generated image output remains visible after collapse |
63
+
64
+ Delivery evidence distinguishes real model execution, deterministic protocol
65
+ fixtures, component tests, and manual-only checks. Fixture image delivery is not
66
+ reported as a successful live image-generation service call. Tarballs, raw logs,
67
+ isolated profiles and screenshots stay outside the repository. No publishing or
68
+ PR creation is part of these acceptance commands.
69
+
70
+ ## Output and file contracts
71
+
72
+ Cancellation preserves stdout already received by the adapter, including
73
+ notifications queued while the interrupt RPC is pending. Only commands already
74
+ owned by that turn may be settled; foreign turns, unknown commands and new prose
75
+ are not admitted during cleanup. An immediate stop before the execution backend
76
+ delivers stdout may legitimately have no output. The acceptance oracle examines
77
+ persisted activity output, not text echoed in the command input.
78
+
79
+ The pinned Codex runtime cannot enable first-yield raw notifications for a
80
+ Thread created without them by an older plugin. Upgrade acceptance must preserve
81
+ that Thread and its history, not silently replace it. First-yield cancellation
82
+ acceptance uses a new post-upgrade Session; an old Thread can retain only output
83
+ the backend actually delivers. See the compatibility contract in
84
+ [Reliability](../reliability-spec.md#command-output-streaming).
85
+
86
+ Synthetic Codex file activities do not have native DSH mutation presenters.
87
+ Supplement the native file-mention resolver only with completed structured
88
+ file-change records. Exact paths and unique basenames can open through DSH;
89
+ ambiguous, failed, running, deleted, malformed and prose-only paths cannot.
90
+ Renames replace the old path with the destination. Native resolution retains
91
+ priority and is returned unchanged when there are no produced Codex files.
92
+
93
+ Reasoning is secondary and collapsed, not mixed into assistant prose. Missing
94
+ model commentary must not be invented. Partial history must fall back without
95
+ discarding available content. Debug context remains available without dominating
96
+ normal Codex process presentation. Model phase information may be used only when
97
+ actually supplied; do not guess a final answer from its wording.
98
+
99
+ Older text-only projections without presentation metadata remain native. A
100
+ visible legacy activity between projected segments also disables takeover; its
101
+ native row must not be crossed by moving later prose to the process anchor.
102
+ Missing tool-call history, unrepresented tool types, and mixed providers likewise
103
+ retain native presentation until the projection has sufficient evidence.
@@ -0,0 +1,4 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '@deepseek-ai/dsh-client-ui-model-selection/client'
3
+ export function sessionPreset(session: { agentPreset?: string | null; projectionValues?: { agentPreset?: string | null } } | undefined): string | null | undefined
4
+ export function modelDirectory(ctx: unknown, sessionId: string): Pick<ReturnType<Context['modelDirectories']['directoryFor']>, 'load' | 'select'>
@@ -0,0 +1,26 @@
1
+ // Public interface adapters shared by both supported DSH generations.
2
+ export function sessionPreset(session) {
3
+ if (session?.projectionValues && Object.hasOwn(session.projectionValues, 'agentPreset')) {
4
+ return session.projectionValues.agentPreset;
5
+ }
6
+ return session?.agentPreset;
7
+ }
8
+
9
+ const service = (ctx, name) => typeof ctx.get === 'function' ? ctx.get(name) : ctx[name];
10
+ export function modelDirectory(ctx, sessionId) {
11
+ const directories = service(ctx, 'modelDirectories');
12
+ if (directories) return directories.directoryFor(sessionId);
13
+ const sessions = service(ctx, 'connection')?.api?.sessions;
14
+ if (!sessions) throw new Error('DSH model selection interface is unavailable');
15
+ return {
16
+ async load() {
17
+ const response = await sessions.models({ sessionId });
18
+ if (!response.result.ok) throw new Error('DSH model discovery failed');
19
+ return response.result.value;
20
+ },
21
+ async select(selection) {
22
+ const response = await sessions.selectModel({ sessionId, ...selection });
23
+ if (!response.result.ok) throw new Error('DSH model selection failed');
24
+ },
25
+ };
26
+ }