codex-ipc 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/PKG-INFO +1 -1
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/__init__.py +8 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/constants.py +8 -3
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/conversation_state.py +222 -3
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/owner_follower.py +91 -5
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/pyproject.toml +1 -1
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/__init__.py +2 -1
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/tests/test_plan_mode_user_input.py +321 -28
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/uv.lock +1 -1
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/.github/workflows/publish.yml +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/.gitignore +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/.python-version +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/.vscode/launch.json +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/README.md +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/examples/demo.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/examples/tui.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/README.md +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/pyproject.toml +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/__init__.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/_inputs.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/_message_router.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/_run.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/_version.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/api.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/async_client.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/client.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/errors.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/generated/__init__.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/generated/notification_registry.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/generated/v2_all.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/models.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/py.typed +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/retry.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/README.md +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/pyproject.toml +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/client.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/frame_protocol.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/immer.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/py.typed +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/ipc/src/ipc/router.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/__init__.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/api.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/async_client.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/client.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/generated/__init__.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/codex/generated/v2_all.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/config.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/session.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/src/codex_ipc/user_input.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/tests/__init__.py +0 -0
- {codex_ipc-0.1.2 → codex_ipc-0.1.4}/tests/test_ssh_websocket_transport.py +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
from ipc.client import CodexIpcClient
|
|
2
2
|
from ipc.conversation_state import (
|
|
3
3
|
JsonDict,
|
|
4
|
+
canonicalize_conversation_patches,
|
|
5
|
+
conversation_state_snapshot,
|
|
6
|
+
conversation_turns,
|
|
4
7
|
conversation_messages,
|
|
5
8
|
extract_text_input,
|
|
9
|
+
materialize_conversation_state,
|
|
6
10
|
text_input_payload,
|
|
7
11
|
)
|
|
8
12
|
from ipc.immer import apply_patches, produce_with_patches
|
|
@@ -15,8 +19,12 @@ __all__ = [
|
|
|
15
19
|
"JsonDict",
|
|
16
20
|
"OwnerFollowerSession",
|
|
17
21
|
"apply_patches",
|
|
22
|
+
"canonicalize_conversation_patches",
|
|
23
|
+
"conversation_state_snapshot",
|
|
24
|
+
"conversation_turns",
|
|
18
25
|
"conversation_messages",
|
|
19
26
|
"extract_text_input",
|
|
27
|
+
"materialize_conversation_state",
|
|
20
28
|
"produce_with_patches",
|
|
21
29
|
"text_input_payload",
|
|
22
30
|
]
|
|
@@ -22,7 +22,10 @@ IPC_DEBUG_TEXT_LIMIT = 500
|
|
|
22
22
|
|
|
23
23
|
IPC_METHOD_VERSIONS: dict[str, int] = {
|
|
24
24
|
# broadcast methods
|
|
25
|
-
"thread-stream-state-changed":
|
|
25
|
+
"thread-stream-state-changed": 11,
|
|
26
|
+
"thread-stream-following-changed": 1,
|
|
27
|
+
"thread-stream-following-status-requested": 1,
|
|
28
|
+
"ipc-connection-reset": 1,
|
|
26
29
|
"thread-read-state-changed": 1,
|
|
27
30
|
"thread-archived": 2,
|
|
28
31
|
"thread-unarchived": 1,
|
|
@@ -31,12 +34,14 @@ IPC_METHOD_VERSIONS: dict[str, int] = {
|
|
|
31
34
|
"client-status-changed": 0,
|
|
32
35
|
# request methods (thread-follower series)
|
|
33
36
|
"thread-follower-start-turn": 1,
|
|
37
|
+
"thread-follower-load-complete-history": 1,
|
|
34
38
|
"thread-follower-compact-thread": 1,
|
|
35
39
|
"thread-follower-steer-turn": 1,
|
|
36
|
-
"thread-follower-interrupt-turn":
|
|
40
|
+
"thread-follower-interrupt-turn": 2,
|
|
41
|
+
"thread-follower-update-thread-settings": 1,
|
|
37
42
|
"thread-follower-set-model-and-reasoning": 1,
|
|
38
43
|
"thread-follower-set-collaboration-mode": 1,
|
|
39
|
-
"thread-follower-edit-last-user-turn":
|
|
44
|
+
"thread-follower-edit-last-user-turn": 2,
|
|
40
45
|
"thread-follower-command-approval-decision": 1,
|
|
41
46
|
"thread-follower-file-approval-decision": 1,
|
|
42
47
|
"thread-follower-permissions-request-approval-response": 1,
|
|
@@ -102,6 +102,227 @@ def empty_turns_pagination() -> JsonDict:
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def conversation_turns(state: JsonDict | None) -> list[JsonDict]:
|
|
106
|
+
if not isinstance(state, dict):
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
canonical_turns: list[JsonDict] = []
|
|
110
|
+
turn_history = state.get("turnHistory")
|
|
111
|
+
if isinstance(turn_history, dict) and turn_history.get("kind") == "canonical":
|
|
112
|
+
history = turn_history.get("history")
|
|
113
|
+
if isinstance(history, dict):
|
|
114
|
+
entities = history.get("entitiesByKey")
|
|
115
|
+
islands = history.get("islands")
|
|
116
|
+
if isinstance(entities, dict) and isinstance(islands, list):
|
|
117
|
+
for island in islands:
|
|
118
|
+
if not isinstance(island, dict):
|
|
119
|
+
continue
|
|
120
|
+
entries = island.get("entries")
|
|
121
|
+
if not isinstance(entries, list):
|
|
122
|
+
continue
|
|
123
|
+
for entry in entries:
|
|
124
|
+
if not isinstance(entry, dict):
|
|
125
|
+
continue
|
|
126
|
+
entity_key = entry.get("value")
|
|
127
|
+
turn = entities.get(entity_key)
|
|
128
|
+
if isinstance(turn, dict):
|
|
129
|
+
canonical_turns.append(turn)
|
|
130
|
+
|
|
131
|
+
overlay = state.get("turns")
|
|
132
|
+
overlay_turns = [turn for turn in overlay if isinstance(turn, dict)] if isinstance(overlay, list) else []
|
|
133
|
+
if not canonical_turns:
|
|
134
|
+
return overlay_turns
|
|
135
|
+
|
|
136
|
+
merged = list(canonical_turns)
|
|
137
|
+
turn_indexes = {
|
|
138
|
+
turn_id: index
|
|
139
|
+
for index, turn in enumerate(merged)
|
|
140
|
+
if isinstance((turn_id := turn.get("turnId")), str) and turn_id
|
|
141
|
+
}
|
|
142
|
+
for turn in overlay_turns:
|
|
143
|
+
turn_id = turn.get("turnId")
|
|
144
|
+
index = turn_indexes.get(turn_id) if isinstance(turn_id, str) else None
|
|
145
|
+
if index is None:
|
|
146
|
+
merged.append(turn)
|
|
147
|
+
if isinstance(turn_id, str) and turn_id:
|
|
148
|
+
turn_indexes[turn_id] = len(merged) - 1
|
|
149
|
+
continue
|
|
150
|
+
merged[index] = _merge_turn_overlay(merged[index], turn)
|
|
151
|
+
return merged
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def materialize_conversation_state(state: JsonDict | None) -> JsonDict | None:
|
|
155
|
+
if not isinstance(state, dict):
|
|
156
|
+
return state
|
|
157
|
+
turn_history = state.get("turnHistory")
|
|
158
|
+
if not isinstance(turn_history, dict) or turn_history.get("kind") != "canonical":
|
|
159
|
+
return state
|
|
160
|
+
return {**state, "turns": conversation_turns(state)}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def conversation_state_snapshot(state: JsonDict) -> JsonDict:
|
|
164
|
+
turns = conversation_turns(state)
|
|
165
|
+
turn_history = state.get("turnHistory")
|
|
166
|
+
history = turn_history.get("history") if isinstance(turn_history, dict) else None
|
|
167
|
+
generation = history.get("generation") if isinstance(history, dict) else None
|
|
168
|
+
next_generation = generation + 1 if isinstance(generation, int) else 0
|
|
169
|
+
tail_id = f"tail:{next_generation}"
|
|
170
|
+
entities: dict[str, JsonDict] = {}
|
|
171
|
+
entries: list[JsonDict] = []
|
|
172
|
+
|
|
173
|
+
for index, turn in enumerate(turns):
|
|
174
|
+
key = _turn_entity_key(turn, index=index, tail_id=tail_id)
|
|
175
|
+
if key in entities:
|
|
176
|
+
entities[key] = _merge_turn_overlay(entities[key], turn)
|
|
177
|
+
continue
|
|
178
|
+
entities[key] = turn
|
|
179
|
+
entries.append({"key": key, "value": key})
|
|
180
|
+
|
|
181
|
+
pagination = state.get("turnsPagination")
|
|
182
|
+
older_cursor = pagination.get("olderCursor") if isinstance(pagination, dict) else None
|
|
183
|
+
oldest_turn_id = (
|
|
184
|
+
pagination.get("oldestLoadedTurnId") if isinstance(pagination, dict) else None
|
|
185
|
+
)
|
|
186
|
+
has_loaded_oldest = (
|
|
187
|
+
pagination.get("hasLoadedOldest", older_cursor is None)
|
|
188
|
+
if isinstance(pagination, dict)
|
|
189
|
+
else True
|
|
190
|
+
)
|
|
191
|
+
if isinstance(older_cursor, str) and older_cursor:
|
|
192
|
+
handle = {"cursor": older_cursor, "oldestLoadedTurnId": oldest_turn_id}
|
|
193
|
+
older_boundary: JsonDict = {
|
|
194
|
+
"status": "available",
|
|
195
|
+
"boundaryId": f"{tail_id}:older",
|
|
196
|
+
"handle": handle,
|
|
197
|
+
"progressKey": json.dumps(
|
|
198
|
+
[older_cursor, oldest_turn_id],
|
|
199
|
+
separators=(",", ":"),
|
|
200
|
+
),
|
|
201
|
+
}
|
|
202
|
+
else:
|
|
203
|
+
older_boundary = {
|
|
204
|
+
"status": "exhausted",
|
|
205
|
+
"boundaryId": f"{tail_id}:older",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
**state,
|
|
210
|
+
"turnHistory": {
|
|
211
|
+
"kind": "canonical",
|
|
212
|
+
"history": {
|
|
213
|
+
"entitiesByKey": entities,
|
|
214
|
+
"generation": next_generation,
|
|
215
|
+
"isComplete": bool(has_loaded_oldest and older_cursor is None),
|
|
216
|
+
"islands": [
|
|
217
|
+
{
|
|
218
|
+
"id": tail_id,
|
|
219
|
+
"entries": entries,
|
|
220
|
+
"olderBoundary": older_boundary,
|
|
221
|
+
"newerBoundary": {
|
|
222
|
+
"status": "exhausted",
|
|
223
|
+
"boundaryId": f"{tail_id}:newer",
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
"turns": [],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def canonicalize_conversation_patches(
|
|
234
|
+
previous_state: JsonDict,
|
|
235
|
+
next_state: JsonDict,
|
|
236
|
+
patches: list[JsonDict],
|
|
237
|
+
) -> list[JsonDict]:
|
|
238
|
+
previous_turns = conversation_turns(previous_state)
|
|
239
|
+
next_turns = conversation_turns(next_state)
|
|
240
|
+
previous_tail_id = _snapshot_tail_id(previous_state)
|
|
241
|
+
next_tail_id = _snapshot_tail_id(next_state)
|
|
242
|
+
translated: list[JsonDict] = []
|
|
243
|
+
non_turn_patches: list[JsonDict] = []
|
|
244
|
+
has_structural_turn_change = False
|
|
245
|
+
|
|
246
|
+
for patch in patches:
|
|
247
|
+
path = patch.get("path")
|
|
248
|
+
if not isinstance(path, list) or path[:1] != ["turns"]:
|
|
249
|
+
translated.append(patch)
|
|
250
|
+
non_turn_patches.append(patch)
|
|
251
|
+
continue
|
|
252
|
+
if len(path) < 3 or not isinstance(path[1], int):
|
|
253
|
+
has_structural_turn_change = True
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
turn_index = path[1]
|
|
257
|
+
if not (0 <= turn_index < len(previous_turns)) or not (
|
|
258
|
+
0 <= turn_index < len(next_turns)
|
|
259
|
+
):
|
|
260
|
+
has_structural_turn_change = True
|
|
261
|
+
continue
|
|
262
|
+
previous_key = _turn_entity_key(
|
|
263
|
+
previous_turns[turn_index],
|
|
264
|
+
index=turn_index,
|
|
265
|
+
tail_id=previous_tail_id,
|
|
266
|
+
)
|
|
267
|
+
next_key = _turn_entity_key(
|
|
268
|
+
next_turns[turn_index],
|
|
269
|
+
index=turn_index,
|
|
270
|
+
tail_id=next_tail_id,
|
|
271
|
+
)
|
|
272
|
+
if previous_key != next_key:
|
|
273
|
+
has_structural_turn_change = True
|
|
274
|
+
continue
|
|
275
|
+
translated.append(
|
|
276
|
+
{
|
|
277
|
+
**patch,
|
|
278
|
+
"path": [
|
|
279
|
+
"turnHistory",
|
|
280
|
+
"history",
|
|
281
|
+
"entitiesByKey",
|
|
282
|
+
next_key,
|
|
283
|
+
*path[2:],
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if not has_structural_turn_change:
|
|
289
|
+
return translated
|
|
290
|
+
|
|
291
|
+
snapshot = conversation_state_snapshot(next_state)
|
|
292
|
+
return [
|
|
293
|
+
*non_turn_patches,
|
|
294
|
+
{
|
|
295
|
+
"op": "replace",
|
|
296
|
+
"path": ["turnHistory"],
|
|
297
|
+
"value": snapshot["turnHistory"],
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _snapshot_tail_id(state: JsonDict) -> str:
|
|
303
|
+
turn_history = state.get("turnHistory")
|
|
304
|
+
history = turn_history.get("history") if isinstance(turn_history, dict) else None
|
|
305
|
+
generation = history.get("generation") if isinstance(history, dict) else None
|
|
306
|
+
next_generation = generation + 1 if isinstance(generation, int) else 0
|
|
307
|
+
return f"tail:{next_generation}"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _turn_entity_key(turn: JsonDict, *, index: int, tail_id: str) -> str:
|
|
311
|
+
turn_id = turn.get("turnId")
|
|
312
|
+
if isinstance(turn_id, str) and turn_id:
|
|
313
|
+
return f"turn:{turn_id}"
|
|
314
|
+
return f"{tail_id}:local:{index}"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _merge_turn_overlay(base: JsonDict, overlay: JsonDict) -> JsonDict:
|
|
318
|
+
merged = {**base, **overlay}
|
|
319
|
+
base_items = base.get("items")
|
|
320
|
+
overlay_items = overlay.get("items")
|
|
321
|
+
if isinstance(base_items, list) and not isinstance(overlay_items, list):
|
|
322
|
+
merged["items"] = base_items
|
|
323
|
+
return merged
|
|
324
|
+
|
|
325
|
+
|
|
105
326
|
def default_collaboration_mode() -> JsonDict:
|
|
106
327
|
return {
|
|
107
328
|
"mode": "default",
|
|
@@ -1017,9 +1238,7 @@ def conversation_messages(state: JsonDict | None) -> list[tuple[str, str]]:
|
|
|
1017
1238
|
if state is None:
|
|
1018
1239
|
return []
|
|
1019
1240
|
|
|
1020
|
-
turns = state
|
|
1021
|
-
if not isinstance(turns, list):
|
|
1022
|
-
return []
|
|
1241
|
+
turns = conversation_turns(state)
|
|
1023
1242
|
|
|
1024
1243
|
messages: list[tuple[str, str]] = []
|
|
1025
1244
|
for turn in turns:
|
|
@@ -22,6 +22,8 @@ from ipc.conversation_state import (
|
|
|
22
22
|
JsonDict,
|
|
23
23
|
PLAN_IMPLEMENTATION_REQUEST_METHOD,
|
|
24
24
|
build_conversation_state,
|
|
25
|
+
canonicalize_conversation_patches,
|
|
26
|
+
conversation_state_snapshot,
|
|
25
27
|
ensure_turn_slot,
|
|
26
28
|
default_collaboration_mode,
|
|
27
29
|
extract_text_input,
|
|
@@ -149,7 +151,7 @@ class OwnerFollowerSession:
|
|
|
149
151
|
self._state: JsonDict | None = None
|
|
150
152
|
self._stream_role: JsonDict | None = None
|
|
151
153
|
self._streaming_conversations: set[str] = set()
|
|
152
|
-
# Monotonic per-conversation revision (matches Codex extension
|
|
154
|
+
# Monotonic per-conversation revision (matches Codex extension v11
|
|
153
155
|
# streamRevisionByConversationId). Owner increments on every snapshot
|
|
154
156
|
# and patch it emits; followers track it to guard out-of-order patches.
|
|
155
157
|
self._stream_revision: int = 0
|
|
@@ -206,6 +208,11 @@ class OwnerFollowerSession:
|
|
|
206
208
|
self._can_handle_owner_request,
|
|
207
209
|
self._handle_follower_start_turn,
|
|
208
210
|
)
|
|
211
|
+
self.ipc_client.add_request_handler(
|
|
212
|
+
"thread-follower-load-complete-history",
|
|
213
|
+
self._can_handle_owner_request,
|
|
214
|
+
self._handle_follower_load_complete_history,
|
|
215
|
+
)
|
|
209
216
|
self.ipc_client.add_request_handler(
|
|
210
217
|
"thread-follower-steer-turn",
|
|
211
218
|
self._can_handle_owner_request,
|
|
@@ -216,6 +223,11 @@ class OwnerFollowerSession:
|
|
|
216
223
|
self._can_handle_owner_request,
|
|
217
224
|
self._handle_follower_set_queued_followups_state,
|
|
218
225
|
)
|
|
226
|
+
self.ipc_client.add_request_handler(
|
|
227
|
+
"thread-follower-update-thread-settings",
|
|
228
|
+
self._can_handle_owner_request,
|
|
229
|
+
self._handle_follower_update_thread_settings,
|
|
230
|
+
)
|
|
219
231
|
self.ipc_client.add_request_handler(
|
|
220
232
|
"thread-follower-set-collaboration-mode",
|
|
221
233
|
self._can_handle_owner_request,
|
|
@@ -333,17 +345,17 @@ class OwnerFollowerSession:
|
|
|
333
345
|
owner_client_id = self._owner_client_id()
|
|
334
346
|
if owner_client_id:
|
|
335
347
|
response = await self.ipc_client.send_request(
|
|
336
|
-
"thread-follower-
|
|
348
|
+
"thread-follower-update-thread-settings",
|
|
337
349
|
{
|
|
338
350
|
"conversationId": self.thread_id,
|
|
339
|
-
"collaborationMode": collaboration_mode,
|
|
351
|
+
"threadSettings": {"collaborationMode": collaboration_mode},
|
|
340
352
|
},
|
|
341
353
|
target_client_id=owner_client_id,
|
|
342
354
|
)
|
|
343
355
|
if response.get("resultType") == "success":
|
|
344
356
|
return
|
|
345
357
|
raise RuntimeError(
|
|
346
|
-
"thread-follower-
|
|
358
|
+
"thread-follower-update-thread-settings failed: "
|
|
347
359
|
f"{response.get('error', '')}"
|
|
348
360
|
)
|
|
349
361
|
|
|
@@ -1945,6 +1957,11 @@ class OwnerFollowerSession:
|
|
|
1945
1957
|
if broadcast and patches and self._stream_role is not None:
|
|
1946
1958
|
if self._stream_role.get("role") == "owner":
|
|
1947
1959
|
patches = self._normalize_broadcast_patches(patches)
|
|
1960
|
+
patches = canonicalize_conversation_patches(
|
|
1961
|
+
previous_state,
|
|
1962
|
+
next_state,
|
|
1963
|
+
patches,
|
|
1964
|
+
)
|
|
1948
1965
|
await self._broadcast_patches(patches)
|
|
1949
1966
|
self._state_changed()
|
|
1950
1967
|
|
|
@@ -2606,7 +2623,7 @@ class OwnerFollowerSession:
|
|
|
2606
2623
|
"change": {
|
|
2607
2624
|
"type": "snapshot",
|
|
2608
2625
|
"revision": revision,
|
|
2609
|
-
"conversationState": self._state,
|
|
2626
|
+
"conversationState": conversation_state_snapshot(self._state),
|
|
2610
2627
|
},
|
|
2611
2628
|
}
|
|
2612
2629
|
self._session_event(
|
|
@@ -2992,6 +3009,19 @@ class OwnerFollowerSession:
|
|
|
2992
3009
|
}
|
|
2993
3010
|
}
|
|
2994
3011
|
|
|
3012
|
+
async def _handle_follower_load_complete_history(
|
|
3013
|
+
self,
|
|
3014
|
+
message: JsonDict,
|
|
3015
|
+
) -> JsonDict:
|
|
3016
|
+
params = message.get("params")
|
|
3017
|
+
if not isinstance(params, dict):
|
|
3018
|
+
raise RuntimeError("invalid params")
|
|
3019
|
+
if params.get("conversationId") != self.thread_id:
|
|
3020
|
+
raise RuntimeError("invalid conversationId")
|
|
3021
|
+
|
|
3022
|
+
await self.read_thread(include_turns=True, broadcast_snapshot=True)
|
|
3023
|
+
return {"revision": self._stream_revision}
|
|
3024
|
+
|
|
2995
3025
|
async def _handle_follower_steer_turn(self, message: JsonDict) -> JsonDict:
|
|
2996
3026
|
params = message.get("params")
|
|
2997
3027
|
if not isinstance(params, dict):
|
|
@@ -3028,6 +3058,62 @@ class OwnerFollowerSession:
|
|
|
3028
3058
|
)
|
|
3029
3059
|
return {"result": {"ok": True}}
|
|
3030
3060
|
|
|
3061
|
+
async def _handle_follower_update_thread_settings(
|
|
3062
|
+
self,
|
|
3063
|
+
message: JsonDict,
|
|
3064
|
+
) -> JsonDict:
|
|
3065
|
+
params = message.get("params")
|
|
3066
|
+
if not isinstance(params, dict):
|
|
3067
|
+
raise RuntimeError("invalid params")
|
|
3068
|
+
if params.get("conversationId") != self.thread_id:
|
|
3069
|
+
raise RuntimeError("invalid conversationId")
|
|
3070
|
+
|
|
3071
|
+
thread_settings = params.get("threadSettings")
|
|
3072
|
+
if not isinstance(thread_settings, dict):
|
|
3073
|
+
raise RuntimeError("invalid threadSettings")
|
|
3074
|
+
|
|
3075
|
+
await self._update_state(
|
|
3076
|
+
lambda draft: self._apply_thread_settings(draft, thread_settings)
|
|
3077
|
+
)
|
|
3078
|
+
return {"result": {"ok": True}}
|
|
3079
|
+
|
|
3080
|
+
def _apply_thread_settings(
|
|
3081
|
+
self,
|
|
3082
|
+
draft: JsonDict,
|
|
3083
|
+
thread_settings: JsonDict,
|
|
3084
|
+
) -> None:
|
|
3085
|
+
latest_settings = draft.get("latestThreadSettings")
|
|
3086
|
+
merged_settings = (
|
|
3087
|
+
dict(latest_settings) if isinstance(latest_settings, dict) else {}
|
|
3088
|
+
)
|
|
3089
|
+
merged_settings.update(thread_settings)
|
|
3090
|
+
|
|
3091
|
+
collaboration_mode = thread_settings.get("collaborationMode")
|
|
3092
|
+
if isinstance(collaboration_mode, dict):
|
|
3093
|
+
merged_settings["collaborationMode"] = dict(collaboration_mode)
|
|
3094
|
+
draft["latestCollaborationMode"] = dict(collaboration_mode)
|
|
3095
|
+
elif "collaborationMode" in thread_settings:
|
|
3096
|
+
collaboration_mode = default_collaboration_mode()
|
|
3097
|
+
merged_settings["collaborationMode"] = collaboration_mode
|
|
3098
|
+
draft["latestCollaborationMode"] = collaboration_mode
|
|
3099
|
+
|
|
3100
|
+
model = thread_settings.get("model")
|
|
3101
|
+
if not isinstance(model, str) and isinstance(collaboration_mode, dict):
|
|
3102
|
+
settings = collaboration_mode.get("settings")
|
|
3103
|
+
if isinstance(settings, dict) and isinstance(settings.get("model"), str):
|
|
3104
|
+
model = settings["model"]
|
|
3105
|
+
if isinstance(model, str):
|
|
3106
|
+
merged_settings["model"] = model
|
|
3107
|
+
draft["latestModel"] = model
|
|
3108
|
+
|
|
3109
|
+
if "effort" in thread_settings:
|
|
3110
|
+
draft["latestReasoningEffort"] = thread_settings.get("effort")
|
|
3111
|
+
cwd = thread_settings.get("cwd")
|
|
3112
|
+
if isinstance(cwd, str):
|
|
3113
|
+
draft["cwd"] = cwd
|
|
3114
|
+
|
|
3115
|
+
draft["latestThreadSettings"] = merged_settings
|
|
3116
|
+
|
|
3031
3117
|
async def _handle_follower_submit_user_input(
|
|
3032
3118
|
self,
|
|
3033
3119
|
message: JsonDict,
|
|
@@ -4,7 +4,7 @@ from codex_app_server import (
|
|
|
4
4
|
SshWebsocketAppServerConfig,
|
|
5
5
|
TextInput,
|
|
6
6
|
)
|
|
7
|
-
from ipc.conversation_state import JsonDict
|
|
7
|
+
from ipc.conversation_state import JsonDict, materialize_conversation_state
|
|
8
8
|
|
|
9
9
|
from codex_ipc.codex.api import AsyncCodex, AsyncThread
|
|
10
10
|
from codex_ipc.config import CodexIpcConfig
|
|
@@ -26,6 +26,7 @@ __all__ = [
|
|
|
26
26
|
"CodexIpcConfig",
|
|
27
27
|
"CodexIpcSession",
|
|
28
28
|
"JsonDict",
|
|
29
|
+
"materialize_conversation_state",
|
|
29
30
|
"TextInput",
|
|
30
31
|
"USER_INPUT_REQUEST_METHOD",
|
|
31
32
|
"UserInputOptionRow",
|
|
@@ -42,8 +42,13 @@ from codex_ipc.user_input import build_user_input_response
|
|
|
42
42
|
from ipc.conversation_state import (
|
|
43
43
|
PLAN_IMPLEMENTATION_PROMPT_PREFIX,
|
|
44
44
|
build_conversation_state,
|
|
45
|
+
canonicalize_conversation_patches,
|
|
46
|
+
conversation_state_snapshot,
|
|
47
|
+
conversation_turns,
|
|
45
48
|
default_collaboration_mode,
|
|
49
|
+
materialize_conversation_state,
|
|
46
50
|
)
|
|
51
|
+
from ipc.constants import IPC_METHOD_VERSIONS
|
|
47
52
|
from ipc.owner_follower import OwnerFollowerSession
|
|
48
53
|
|
|
49
54
|
|
|
@@ -570,6 +575,21 @@ class FacadeSessionTests(unittest.IsolatedAsyncioTestCase):
|
|
|
570
575
|
|
|
571
576
|
|
|
572
577
|
class CoreImportTests(unittest.TestCase):
|
|
578
|
+
def test_ipc_versions_match_conversation_state_v11(self) -> None:
|
|
579
|
+
expected_versions = {
|
|
580
|
+
"thread-stream-state-changed": 11,
|
|
581
|
+
"thread-stream-following-changed": 1,
|
|
582
|
+
"thread-stream-following-status-requested": 1,
|
|
583
|
+
"ipc-connection-reset": 1,
|
|
584
|
+
"thread-follower-load-complete-history": 1,
|
|
585
|
+
"thread-follower-interrupt-turn": 2,
|
|
586
|
+
"thread-follower-update-thread-settings": 1,
|
|
587
|
+
"thread-follower-edit-last-user-turn": 2,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for method, version in expected_versions.items():
|
|
591
|
+
self.assertEqual(IPC_METHOD_VERSIONS[method], version)
|
|
592
|
+
|
|
573
593
|
def test_core_import_does_not_require_tui_dependencies(self) -> None:
|
|
574
594
|
script = """
|
|
575
595
|
import builtins
|
|
@@ -596,7 +616,284 @@ print(codex_ipc.CodexIpcConfig.__name__)
|
|
|
596
616
|
self.assertIn("CodexIpcConfig", result.stdout)
|
|
597
617
|
|
|
598
618
|
|
|
619
|
+
class ConversationStateV11Tests(unittest.TestCase):
|
|
620
|
+
def test_snapshot_canonicalizes_legacy_turns_without_mutating_state(self) -> None:
|
|
621
|
+
turns = [
|
|
622
|
+
{"turnId": "turn-1", "status": "completed", "items": []},
|
|
623
|
+
{"turnId": None, "status": "inProgress", "items": []},
|
|
624
|
+
]
|
|
625
|
+
state = {
|
|
626
|
+
"id": "thread-1",
|
|
627
|
+
"turns": turns,
|
|
628
|
+
"turnsPagination": {
|
|
629
|
+
"olderCursor": None,
|
|
630
|
+
"oldestLoadedTurnId": "turn-1",
|
|
631
|
+
"isLoadingOlder": False,
|
|
632
|
+
"hasLoadedOldest": True,
|
|
633
|
+
},
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
snapshot = conversation_state_snapshot(state)
|
|
637
|
+
|
|
638
|
+
self.assertIs(state["turns"], turns)
|
|
639
|
+
self.assertEqual(snapshot["turns"], [])
|
|
640
|
+
history = snapshot["turnHistory"]["history"]
|
|
641
|
+
self.assertTrue(history["isComplete"])
|
|
642
|
+
self.assertEqual(
|
|
643
|
+
history["islands"][0]["entries"],
|
|
644
|
+
[
|
|
645
|
+
{"key": "turn:turn-1", "value": "turn:turn-1"},
|
|
646
|
+
{"key": "tail:0:local:1", "value": "tail:0:local:1"},
|
|
647
|
+
],
|
|
648
|
+
)
|
|
649
|
+
self.assertEqual(conversation_turns(snapshot), turns)
|
|
650
|
+
|
|
651
|
+
def test_materialize_merges_canonical_history_with_live_overlay(self) -> None:
|
|
652
|
+
historical_turn = {
|
|
653
|
+
"turnId": "turn-1",
|
|
654
|
+
"status": "inProgress",
|
|
655
|
+
"items": [{"type": "agentMessage", "id": "agent-1", "text": ""}],
|
|
656
|
+
}
|
|
657
|
+
completed_overlay = {
|
|
658
|
+
"turnId": "turn-1",
|
|
659
|
+
"status": "completed",
|
|
660
|
+
"items": [
|
|
661
|
+
{"type": "agentMessage", "id": "agent-1", "text": "done"}
|
|
662
|
+
],
|
|
663
|
+
}
|
|
664
|
+
state = {
|
|
665
|
+
"id": "thread-1",
|
|
666
|
+
"turnHistory": {
|
|
667
|
+
"kind": "canonical",
|
|
668
|
+
"history": {
|
|
669
|
+
"entitiesByKey": {"turn:turn-1": historical_turn},
|
|
670
|
+
"generation": 0,
|
|
671
|
+
"isComplete": True,
|
|
672
|
+
"islands": [
|
|
673
|
+
{
|
|
674
|
+
"id": "tail:0",
|
|
675
|
+
"entries": [
|
|
676
|
+
{"key": "turn:turn-1", "value": "turn:turn-1"}
|
|
677
|
+
],
|
|
678
|
+
"olderBoundary": {"status": "exhausted"},
|
|
679
|
+
"newerBoundary": {"status": "exhausted"},
|
|
680
|
+
}
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
"turns": [completed_overlay],
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
materialized = materialize_conversation_state(state)
|
|
688
|
+
|
|
689
|
+
self.assertIsNot(materialized, state)
|
|
690
|
+
self.assertEqual(materialized["turns"], [completed_overlay])
|
|
691
|
+
self.assertEqual(
|
|
692
|
+
state["turnHistory"]["history"]["entitiesByKey"]["turn:turn-1"],
|
|
693
|
+
historical_turn,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def test_translates_turn_delta_to_canonical_entity_patch(self) -> None:
|
|
697
|
+
previous = {
|
|
698
|
+
"id": "thread-1",
|
|
699
|
+
"turns": [
|
|
700
|
+
{
|
|
701
|
+
"turnId": "turn-1",
|
|
702
|
+
"status": "inProgress",
|
|
703
|
+
"items": [{"type": "agentMessage", "id": "agent-1", "text": "a"}],
|
|
704
|
+
}
|
|
705
|
+
],
|
|
706
|
+
}
|
|
707
|
+
next_state = {
|
|
708
|
+
**previous,
|
|
709
|
+
"turns": [
|
|
710
|
+
{
|
|
711
|
+
"turnId": "turn-1",
|
|
712
|
+
"status": "inProgress",
|
|
713
|
+
"items": [
|
|
714
|
+
{"type": "agentMessage", "id": "agent-1", "text": "ab"}
|
|
715
|
+
],
|
|
716
|
+
}
|
|
717
|
+
],
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
patches = canonicalize_conversation_patches(
|
|
721
|
+
previous,
|
|
722
|
+
next_state,
|
|
723
|
+
[
|
|
724
|
+
{
|
|
725
|
+
"op": "replace",
|
|
726
|
+
"path": ["turns", 0, "items", 0, "text"],
|
|
727
|
+
"value": "ab",
|
|
728
|
+
}
|
|
729
|
+
],
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
self.assertEqual(
|
|
733
|
+
patches,
|
|
734
|
+
[
|
|
735
|
+
{
|
|
736
|
+
"op": "replace",
|
|
737
|
+
"path": [
|
|
738
|
+
"turnHistory",
|
|
739
|
+
"history",
|
|
740
|
+
"entitiesByKey",
|
|
741
|
+
"turn:turn-1",
|
|
742
|
+
"items",
|
|
743
|
+
0,
|
|
744
|
+
"text",
|
|
745
|
+
],
|
|
746
|
+
"value": "ab",
|
|
747
|
+
}
|
|
748
|
+
],
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def test_replaces_canonical_history_for_structural_turn_change(self) -> None:
|
|
752
|
+
previous = {"id": "thread-1", "turns": []}
|
|
753
|
+
next_state = {
|
|
754
|
+
"id": "thread-1",
|
|
755
|
+
"turns": [
|
|
756
|
+
{"turnId": None, "status": "inProgress", "items": []}
|
|
757
|
+
],
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
patches = canonicalize_conversation_patches(
|
|
761
|
+
previous,
|
|
762
|
+
next_state,
|
|
763
|
+
[
|
|
764
|
+
{
|
|
765
|
+
"op": "add",
|
|
766
|
+
"path": ["turns", 0],
|
|
767
|
+
"value": next_state["turns"][0],
|
|
768
|
+
}
|
|
769
|
+
],
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
self.assertEqual(len(patches), 1)
|
|
773
|
+
self.assertEqual(patches[0]["op"], "replace")
|
|
774
|
+
self.assertEqual(patches[0]["path"], ["turnHistory"])
|
|
775
|
+
self.assertEqual(patches[0]["value"]["kind"], "canonical")
|
|
776
|
+
|
|
777
|
+
|
|
599
778
|
class PlanModeTests(unittest.IsolatedAsyncioTestCase):
|
|
779
|
+
async def test_owner_broadcasts_initial_state_as_canonical_snapshot(self) -> None:
|
|
780
|
+
session = make_session()
|
|
781
|
+
ipc_client = FakeIpcClient()
|
|
782
|
+
session.ipc_client = ipc_client
|
|
783
|
+
session._stream_role = {"role": "owner"}
|
|
784
|
+
session._state = {
|
|
785
|
+
"id": "thread-1",
|
|
786
|
+
"turns": [
|
|
787
|
+
{"turnId": "turn-1", "status": "completed", "items": []}
|
|
788
|
+
],
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
await session._broadcast_snapshot()
|
|
792
|
+
|
|
793
|
+
_method, params = ipc_client.broadcasts[0]
|
|
794
|
+
change = params["change"]
|
|
795
|
+
self.assertEqual(change["type"], "snapshot")
|
|
796
|
+
self.assertEqual(change["conversationState"]["turns"], [])
|
|
797
|
+
self.assertEqual(
|
|
798
|
+
change["conversationState"]["turnHistory"]["history"]["islands"][0][
|
|
799
|
+
"entries"
|
|
800
|
+
],
|
|
801
|
+
[{"key": "turn:turn-1", "value": "turn:turn-1"}],
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
async def test_follower_forwards_collaboration_mode_as_thread_settings(self) -> None:
|
|
805
|
+
session = make_session()
|
|
806
|
+
ipc_client = FakeIpcClient()
|
|
807
|
+
session.ipc_client = ipc_client
|
|
808
|
+
session._stream_role = {"role": "follower", "ownerClientId": "owner-1"}
|
|
809
|
+
collaboration_mode = {
|
|
810
|
+
"mode": "plan",
|
|
811
|
+
"settings": {
|
|
812
|
+
"model": "gpt-5.5",
|
|
813
|
+
"reasoning_effort": "high",
|
|
814
|
+
"developer_instructions": None,
|
|
815
|
+
},
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
await session.set_collaboration_mode(collaboration_mode)
|
|
819
|
+
|
|
820
|
+
self.assertEqual(
|
|
821
|
+
ipc_client.requests,
|
|
822
|
+
[
|
|
823
|
+
(
|
|
824
|
+
"thread-follower-update-thread-settings",
|
|
825
|
+
{
|
|
826
|
+
"conversationId": "thread-1",
|
|
827
|
+
"threadSettings": {"collaborationMode": collaboration_mode},
|
|
828
|
+
},
|
|
829
|
+
"owner-1",
|
|
830
|
+
)
|
|
831
|
+
],
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
async def test_owner_applies_follower_thread_settings(self) -> None:
|
|
835
|
+
session = make_session()
|
|
836
|
+
session.ipc_client = FakeIpcClient()
|
|
837
|
+
session._stream_role = {"role": "owner"}
|
|
838
|
+
session._state = {
|
|
839
|
+
"id": "thread-1",
|
|
840
|
+
"latestModel": "gpt-5.4",
|
|
841
|
+
"latestReasoningEffort": "medium",
|
|
842
|
+
"latestCollaborationMode": default_collaboration_mode(),
|
|
843
|
+
"latestThreadSettings": {"model": "gpt-5.4"},
|
|
844
|
+
"updatedAt": 0,
|
|
845
|
+
}
|
|
846
|
+
collaboration_mode = {
|
|
847
|
+
"mode": "plan",
|
|
848
|
+
"settings": {
|
|
849
|
+
"model": "gpt-5.5",
|
|
850
|
+
"reasoning_effort": "high",
|
|
851
|
+
"developer_instructions": None,
|
|
852
|
+
},
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
result = await session._handle_follower_update_thread_settings(
|
|
856
|
+
{
|
|
857
|
+
"params": {
|
|
858
|
+
"conversationId": "thread-1",
|
|
859
|
+
"threadSettings": {
|
|
860
|
+
"collaborationMode": collaboration_mode,
|
|
861
|
+
"effort": "high",
|
|
862
|
+
"cwd": "/repo",
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
self.assertEqual(result, {"result": {"ok": True}})
|
|
869
|
+
self.assertEqual(session.state["latestCollaborationMode"], collaboration_mode)
|
|
870
|
+
self.assertEqual(session.state["latestModel"], "gpt-5.5")
|
|
871
|
+
self.assertEqual(session.state["latestReasoningEffort"], "high")
|
|
872
|
+
self.assertEqual(session.state["cwd"], "/repo")
|
|
873
|
+
|
|
874
|
+
async def test_owner_loads_complete_history_and_returns_snapshot_revision(self) -> None:
|
|
875
|
+
session = make_session()
|
|
876
|
+
calls: list[tuple[bool, bool]] = []
|
|
877
|
+
|
|
878
|
+
async def read_thread(
|
|
879
|
+
_thread_id: str | None = None,
|
|
880
|
+
*,
|
|
881
|
+
include_turns: bool = False,
|
|
882
|
+
broadcast_snapshot: bool = False,
|
|
883
|
+
) -> object:
|
|
884
|
+
calls.append((include_turns, broadcast_snapshot))
|
|
885
|
+
session._stream_revision = 8
|
|
886
|
+
return object()
|
|
887
|
+
|
|
888
|
+
session.read_thread = read_thread # type: ignore[method-assign]
|
|
889
|
+
|
|
890
|
+
result = await session._handle_follower_load_complete_history(
|
|
891
|
+
{"params": {"conversationId": "thread-1"}}
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
self.assertEqual(calls, [(True, True)])
|
|
895
|
+
self.assertEqual(result, {"revision": 8})
|
|
896
|
+
|
|
600
897
|
async def test_turn_start_passes_collaboration_mode(self) -> None:
|
|
601
898
|
codex = FakeCodex()
|
|
602
899
|
thread = AsyncThread(codex, "thread-1")
|
|
@@ -691,7 +988,7 @@ class PlanModeTests(unittest.IsolatedAsyncioTestCase):
|
|
|
691
988
|
)
|
|
692
989
|
self.assertGreater(session.state["updatedAt"], 0)
|
|
693
990
|
|
|
694
|
-
async def
|
|
991
|
+
async def test_owner_broadcasts_collaboration_mode_as_patch(self) -> None:
|
|
695
992
|
session_events: list[dict] = []
|
|
696
993
|
session = make_session(session_event=session_events.append)
|
|
697
994
|
ipc_client = FakeIpcClient()
|
|
@@ -728,20 +1025,19 @@ class PlanModeTests(unittest.IsolatedAsyncioTestCase):
|
|
|
728
1025
|
self.assertEqual(len(session_events), 1)
|
|
729
1026
|
self.assertEqual(session_events[0]["method"], "thread-stream-state-changed")
|
|
730
1027
|
self.assertEqual(session_events[0]["params"], params)
|
|
1028
|
+
change = params["change"]
|
|
1029
|
+
self.assertEqual(change["type"], "patches")
|
|
1030
|
+
self.assertEqual(change["revision"], 1)
|
|
1031
|
+
self.assertEqual(change["baseRevision"], 0)
|
|
731
1032
|
self.assertEqual(
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
"path": ["latestCollaborationMode"],
|
|
741
|
-
"value": collaboration_mode,
|
|
742
|
-
}
|
|
743
|
-
],
|
|
744
|
-
},
|
|
1033
|
+
change["patches"],
|
|
1034
|
+
[
|
|
1035
|
+
{
|
|
1036
|
+
"op": "replace",
|
|
1037
|
+
"path": ["latestCollaborationMode"],
|
|
1038
|
+
"value": collaboration_mode,
|
|
1039
|
+
}
|
|
1040
|
+
],
|
|
745
1041
|
)
|
|
746
1042
|
|
|
747
1043
|
async def test_facade_session_exposes_session_event_watchers(self) -> None:
|
|
@@ -762,7 +1058,7 @@ class PlanModeTests(unittest.IsolatedAsyncioTestCase):
|
|
|
762
1058
|
self.assertEqual(listener_events, [event])
|
|
763
1059
|
await watcher.aclose()
|
|
764
1060
|
|
|
765
|
-
async def
|
|
1061
|
+
async def test_owner_broadcasts_default_mode_as_patch(self) -> None:
|
|
766
1062
|
session = make_session()
|
|
767
1063
|
ipc_client = FakeIpcClient()
|
|
768
1064
|
session.ipc_client = ipc_client
|
|
@@ -794,20 +1090,17 @@ class PlanModeTests(unittest.IsolatedAsyncioTestCase):
|
|
|
794
1090
|
|
|
795
1091
|
self.assertEqual(len(ipc_client.broadcasts), 1)
|
|
796
1092
|
_method, params = ipc_client.broadcasts[0]
|
|
1093
|
+
change = params["change"]
|
|
1094
|
+
self.assertEqual(change["type"], "patches")
|
|
797
1095
|
self.assertEqual(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
"path": ["latestCollaborationMode"],
|
|
807
|
-
"value": collaboration_mode,
|
|
808
|
-
}
|
|
809
|
-
],
|
|
810
|
-
},
|
|
1096
|
+
change["patches"],
|
|
1097
|
+
[
|
|
1098
|
+
{
|
|
1099
|
+
"op": "replace",
|
|
1100
|
+
"path": ["latestCollaborationMode"],
|
|
1101
|
+
"value": collaboration_mode,
|
|
1102
|
+
}
|
|
1103
|
+
],
|
|
811
1104
|
)
|
|
812
1105
|
|
|
813
1106
|
async def test_owner_only_accepts_snapshot_not_patches(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/_message_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/generated/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{codex_ipc-0.1.2 → codex_ipc-0.1.4}/packages/codex-sdk/src/codex_app_server/generated/v2_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|