@oneciel-ai/ciel-runtime 0.2.22 → 0.2.23

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +150 -57
  3. package/ciel-runtime-stop.cmd +1 -0
  4. package/ciel-runtime.cmd +1 -0
  5. package/ciel-runtimectl.cmd +1 -0
  6. package/ciel_runtime.py +56 -53
  7. package/ciel_runtime_support/anthropic_tool_turns.py +81 -0
  8. package/ciel_runtime_support/architecture.py +17 -0
  9. package/ciel_runtime_support/channel_injection.py +113 -3
  10. package/ciel_runtime_support/channel_message_prompt.py +15 -0
  11. package/ciel_runtime_support/channel_pending_injection.py +28 -4
  12. package/ciel_runtime_support/channel_pending_poll.py +2 -0
  13. package/ciel_runtime_support/channel_terminal_context.py +6 -0
  14. package/ciel_runtime_support/channel_terminal_dispatch.py +2 -0
  15. package/ciel_runtime_support/channel_terminal_proxy.py +8 -2
  16. package/ciel_runtime_support/channel_wake_claim_repository.py +7 -0
  17. package/ciel_runtime_support/channel_wake_context.py +7 -3
  18. package/ciel_runtime_support/cli_parser.py +6 -0
  19. package/ciel_runtime_support/codex_backend_context.py +4 -0
  20. package/ciel_runtime_support/codex_turn_recovery.py +75 -12
  21. package/ciel_runtime_support/config_migrations.py +102 -10
  22. package/ciel_runtime_support/config_repository.py +9 -0
  23. package/ciel_runtime_support/event_settings_cli.py +284 -2
  24. package/ciel_runtime_support/openai_chat_passthrough.py +2 -0
  25. package/ciel_runtime_support/protocols/openai_responses.py +22 -18
  26. package/ciel_runtime_support/provider_adapters.py +7 -0
  27. package/ciel_runtime_support/provider_policy.py +10 -1
  28. package/ciel_runtime_support/provider_request_builder.py +9 -2
  29. package/ciel_runtime_support/provider_responses_passthrough.py +4 -0
  30. package/ciel_runtime_support/providers/__init__.py +2 -0
  31. package/ciel_runtime_support/providers/alibaba.py +33 -10
  32. package/ciel_runtime_support/providers/constants.py +1 -0
  33. package/ciel_runtime_support/providers/opencode.py +25 -5
  34. package/ciel_runtime_support/providers/opencode_go.py +3 -3
  35. package/ciel_runtime_support/providers/openrouter.py +76 -3
  36. package/ciel_runtime_support/providers/tabitoken.py +130 -0
  37. package/ciel_runtime_support/remote_instructions.py +20 -6
  38. package/ciel_runtime_support/remote_memory.py +515 -55
  39. package/ciel_runtime_support/responses_input_compatibility.py +14 -0
  40. package/ciel_runtime_support/router_http.py +5 -0
  41. package/ciel_runtime_support/runtime_constants.py +1 -1
  42. package/ciel_runtime_support/runtime_launch.py +9 -2
  43. package/ciel_runtime_support/runtime_paths.py +2 -0
  44. package/ciel_runtime_support/tool_side_effect_dedupe.py +7 -4
  45. package/ciel_runtime_support/transcript_delta_delivery.py +333 -0
  46. package/ciel_runtime_support/usage_events.py +45 -3
  47. package/ciel_runtime_support/usage_service.py +871 -0
  48. package/ciel_runtime_support/windows_conpty.py +108 -2
  49. package/ciel_runtime_support/workspace_mcp.py +4 -0
  50. package/docs/CLI-Reference.md +11 -0
  51. package/docs/Configuration.md +7 -2
  52. package/docs/MCP-Channels.md +7 -3
  53. package/docs/Module-Map.md +4 -0
  54. package/docs/Providers.md +32 -0
  55. package/docs/Remote-Memory.md +29 -10
  56. package/docs/Test-Suite.md +1 -0
  57. package/docs/journal/2026/08/20/kevin-wake-orphan-tool-repair.md +161 -0
  58. package/docs/journal/2026/08/20/nightly-push-local-deployment.md +59 -2
  59. package/docs/journal/2026/08/20/wing-remote-memory-replication-verification.md +107 -0
  60. package/docs/journal/2026/08/21/alibaba-singapore-qwen38-official-alignment.md +64 -0
  61. package/docs/journal/2026/08/21/codex-visible-sse-wake-submit-regression.md +85 -0
  62. package/docs/journal/2026/08/21/external-sse-wake-only-change-analysis.md +131 -0
  63. package/docs/journal/2026/08/21/hyundai-invalid-replayed-tool-name-repair.okf +88 -0
  64. package/docs/journal/2026/08/21/local-stale-temporary-runtime-pin-repair.okf +118 -0
  65. package/docs/journal/2026/08/21/transcript-compaction-external-events.okf +131 -0
  66. package/docs/journal/2026/08/21/wing-ai-net-mcp-boundary-diagnosis.okf +129 -0
  67. package/docs/journal/2026/08/21/wing-cold-start-wake-submit-repair.okf +161 -0
  68. package/docs/journal/2026/08/21/wing-powershell-memory-runaway.okf +101 -0
  69. package/docs/journal/2026/08/21/wing-repeated-reasoning-tool-turn-repair.okf +112 -0
  70. package/docs/journal/2026/08/21/wing-repeated-tool-guard-finalization.okf +81 -0
  71. package/docs/journal/2026/08/21/wing-working-directory-memory-deployment.md +110 -0
  72. package/docs/journal/2026/08/21/wing-workspace-state-memory-prompt-injection.md +88 -0
  73. package/docs/journal/2026/08/22/alibaba-singapore-dual-endpoint-wire-alignment.okf +142 -0
  74. package/docs/journal/2026/08/22/delluhiold-claude-compaction-empty-tool-name.okf +168 -0
  75. package/docs/journal/2026/08/22/remote-memory-relative-prompt-paths.okf +79 -0
  76. package/docs/journal/2026/08/22/remote-memory-root-system-prompt.okf +121 -0
  77. package/docs/journal/2026/08/23/claude-web-chat-stateless-mcp-repair.okf +97 -0
  78. package/docs/journal/2026/08/23/cross-runtime-token-usage-interfaces-research.okf +378 -0
  79. package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +81 -0
  80. package/docs/journal/2026/08/23/mia-openrouter-ox-alpha-latency-diagnosis.okf +146 -0
  81. package/docs/journal/2026/08/23/openrouter-opencode-ox-alpha-support.okf +114 -0
  82. package/docs/journal/2026/08/23/tabitoken-provider-model-catalog.okf +97 -0
  83. package/docs/journal/2026/08/23/usage-stream-snapshot-implementation.okf +134 -0
  84. package/docs/usage-observability.md +141 -0
  85. package/install.ps1 +24 -3
  86. package/package.json +2 -1
@@ -497,3 +497,18 @@ def format_llm_delivery_wake_prompt(messages: list[dict[str, Any]]) -> str:
497
497
  ids = [str(message.get("id") or "").strip() for message in messages]
498
498
  ids = [message_id for message_id in ids if message_id]
499
499
  return f"[ciel-wake] pending_ids={','.join(ids) or '-'}"
500
+
501
+
502
+ def format_visible_llm_delivery_wake_prompt(
503
+ messages: list[dict[str, Any]],
504
+ ) -> str:
505
+ """Show a Codex wake body while retaining router replacement semantics.
506
+
507
+ The marker must remain the first physical line: conversation-turn policy
508
+ recognizes that prefix and removes the entire terminal-originated message
509
+ before the queued body is appended to the routed model request.
510
+ """
511
+
512
+ marker = format_llm_delivery_wake_prompt(messages)
513
+ body = format_llm_batch_prompt(messages)
514
+ return f"{marker}\n\n{body}" if body else marker
@@ -31,6 +31,7 @@ class ChannelInjectionState:
31
31
  @dataclass(frozen=True, slots=True)
32
32
  class ChannelInjectionPrompts:
33
33
  llm_delivery: Callable[..., str]
34
+ visible_llm_delivery: Callable[..., str]
34
35
  web_chat: Callable[..., str]
35
36
  standard: Callable[..., str]
36
37
  enter_bytes: Callable[..., bytes]
@@ -87,6 +88,7 @@ def inject_pending_channel_messages(
87
88
  *,
88
89
  web_chat_only: bool = False,
89
90
  wake_for_llm_delivery: bool = False,
91
+ display_llm_delivery_body: bool = False,
90
92
  commit_cursor: bool = True,
91
93
  injected_message_ids: list[int] | None = None,
92
94
  submit_retry_count: int = 1,
@@ -170,7 +172,12 @@ def inject_pending_channel_messages(
170
172
  io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason=duplicate_channel_event")
171
173
  continue
172
174
  if candidate_uses_router:
173
- message_prompt = prompts.llm_delivery([message])
175
+ formatter = (
176
+ prompts.visible_llm_delivery
177
+ if display_llm_delivery_body
178
+ else prompts.llm_delivery
179
+ )
180
+ message_prompt = formatter([message])
174
181
  elif web_chat_only and state.message_is_web_chat(message):
175
182
  message_prompt = prompts.web_chat([message])
176
183
  else:
@@ -228,7 +235,12 @@ def inject_pending_channel_messages(
228
235
  if not pending:
229
236
  return last_id
230
237
  if pending_uses_router:
231
- prompt = prompts.llm_delivery(pending)
238
+ formatter = (
239
+ prompts.visible_llm_delivery
240
+ if display_llm_delivery_body
241
+ else prompts.llm_delivery
242
+ )
243
+ prompt = formatter(pending)
232
244
  elif web_chat_only and all(state.message_is_web_chat(message) for message in pending):
233
245
  prompt = prompts.web_chat(pending)
234
246
  else:
@@ -245,7 +257,7 @@ def inject_pending_channel_messages(
245
257
  claimed_ids.append(claim_id)
246
258
  submit_bytes = prompts.enter_bytes(enter_bytes)
247
259
  try:
248
- io.write_prompt(
260
+ submitted = io.write_prompt(
249
261
  master_fd,
250
262
  prompt,
251
263
  submit_bytes,
@@ -254,6 +266,15 @@ def inject_pending_channel_messages(
254
266
  bracketed_paste=bracketed_paste,
255
267
  submit_delay_seconds=submit_delay_seconds,
256
268
  )
269
+ if submitted is False:
270
+ wake_store.rollback(pending, claimed_ids)
271
+ ids = ",".join(str(message.get("id") or "") for message in pending)
272
+ io.log(
273
+ "WARN",
274
+ "channel_stdin_proxy_deferred "
275
+ f"message_ids={ids} reason=prompt_not_submitted",
276
+ )
277
+ return return_last_id if pending_uses_router else previous_last_id
257
278
  wake_store.record_prompts(pending, prompt)
258
279
  except Exception:
259
280
  wake_store.rollback(pending, claimed_ids)
@@ -267,6 +288,9 @@ def inject_pending_channel_messages(
267
288
  channels = ",".join(sorted({str(message.get("channel") or "default") for message in pending}))
268
289
  io.log(
269
290
  "INFO",
270
- f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids} channels={channels} enter={prompts.enter_label(submit_bytes)} commit_cursor={commit_cursor}",
291
+ f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids} "
292
+ f"channels={channels} enter={prompts.enter_label(submit_bytes)} "
293
+ f"commit_cursor={commit_cursor} "
294
+ f"display_body={bool(pending_uses_router and display_llm_delivery_body)}",
271
295
  )
272
296
  return return_last_id if pending_uses_router else last_id
@@ -66,6 +66,7 @@ class ChannelPendingInjectionOptions:
66
66
  enabled: bool
67
67
  web_chat_only: bool
68
68
  wake_for_llm_delivery: bool
69
+ display_llm_delivery_body: bool
69
70
  submit_retry_count: int
70
71
  confirm_submit: bool
71
72
  bracketed_paste: bool
@@ -138,6 +139,7 @@ def poll_pending_channel_messages(
138
139
  enter_bytes,
139
140
  web_chat_only=options.web_chat_only,
140
141
  wake_for_llm_delivery=options.wake_for_llm_delivery,
142
+ display_llm_delivery_body=options.display_llm_delivery_body,
141
143
  commit_cursor=False,
142
144
  injected_message_ids=injected_ids,
143
145
  submit_retry_count=options.submit_retry_count,
@@ -212,6 +212,7 @@ class ChannelTerminalContext:
212
212
  inject_channel_messages: bool = True,
213
213
  inject_web_chat_only: bool = False,
214
214
  wake_for_llm_delivery: bool = False,
215
+ channel_wake_display_body: bool = False,
215
216
  synthetic_enter_bytes: str | bytes | None = None,
216
217
  normalize_bare_cr_for_synthetic_enter: bool = True,
217
218
  channel_wake_submit_retries: int = 1,
@@ -228,6 +229,7 @@ class ChannelTerminalContext:
228
229
  inject_channel_messages=inject_channel_messages,
229
230
  inject_web_chat_only=inject_web_chat_only,
230
231
  wake_for_llm_delivery=wake_for_llm_delivery,
232
+ channel_wake_display_body=channel_wake_display_body,
231
233
  synthetic_enter_bytes=synthetic_enter_bytes,
232
234
  normalize_bare_cr_for_synthetic_enter=(
233
235
  normalize_bare_cr_for_synthetic_enter
@@ -278,6 +280,7 @@ class ChannelTerminalCompatibilityApi:
278
280
  inject_channel_messages: bool = True,
279
281
  inject_web_chat_only: bool = False,
280
282
  wake_for_llm_delivery: bool = False,
283
+ channel_wake_display_body: bool = False,
281
284
  synthetic_enter_bytes: str | bytes | None = None,
282
285
  normalize_bare_cr_for_synthetic_enter: bool = True,
283
286
  channel_wake_submit_retries: int = 1,
@@ -292,6 +295,7 @@ class ChannelTerminalCompatibilityApi:
292
295
  inject_channel_messages=inject_channel_messages,
293
296
  inject_web_chat_only=inject_web_chat_only,
294
297
  wake_for_llm_delivery=wake_for_llm_delivery,
298
+ channel_wake_display_body=channel_wake_display_body,
295
299
  synthetic_enter_bytes=synthetic_enter_bytes,
296
300
  normalize_bare_cr_for_synthetic_enter=(
297
301
  normalize_bare_cr_for_synthetic_enter
@@ -311,6 +315,7 @@ class ChannelTerminalCompatibilityApi:
311
315
  inject_channel_messages: bool = True,
312
316
  inject_web_chat_only: bool = False,
313
317
  wake_for_llm_delivery: bool = False,
318
+ channel_wake_display_body: bool = False,
314
319
  synthetic_enter_bytes: str | bytes | None = None,
315
320
  normalize_bare_cr_for_synthetic_enter: bool = True,
316
321
  channel_wake_submit_retries: int = 1,
@@ -325,6 +330,7 @@ class ChannelTerminalCompatibilityApi:
325
330
  inject_channel_messages=inject_channel_messages,
326
331
  inject_web_chat_only=inject_web_chat_only,
327
332
  wake_for_llm_delivery=wake_for_llm_delivery,
333
+ channel_wake_display_body=channel_wake_display_body,
328
334
  synthetic_enter_bytes=synthetic_enter_bytes,
329
335
  normalize_bare_cr_for_synthetic_enter=(
330
336
  normalize_bare_cr_for_synthetic_enter
@@ -47,6 +47,7 @@ class ChannelTerminalDispatchService:
47
47
  inject_channel_messages: bool = True,
48
48
  inject_web_chat_only: bool = False,
49
49
  wake_for_llm_delivery: bool = False,
50
+ channel_wake_display_body: bool = False,
50
51
  synthetic_enter_bytes: str | bytes | None = None,
51
52
  normalize_bare_cr_for_synthetic_enter: bool = True,
52
53
  channel_wake_submit_retries: int = 1,
@@ -59,6 +60,7 @@ class ChannelTerminalDispatchService:
59
60
  "inject_channel_messages": inject_channel_messages,
60
61
  "inject_web_chat_only": inject_web_chat_only,
61
62
  "wake_for_llm_delivery": wake_for_llm_delivery,
63
+ "channel_wake_display_body": channel_wake_display_body,
62
64
  "synthetic_enter_bytes": synthetic_enter_bytes,
63
65
  "normalize_bare_cr_for_synthetic_enter": (
64
66
  normalize_bare_cr_for_synthetic_enter
@@ -122,6 +122,7 @@ def run_windows_channel_terminal_proxy(
122
122
  inject_channel_messages: bool = True,
123
123
  inject_web_chat_only: bool = False,
124
124
  wake_for_llm_delivery: bool = False,
125
+ channel_wake_display_body: bool = False,
125
126
  synthetic_enter_bytes: str | bytes | None = None,
126
127
  channel_wake_submit_retries: int = 1,
127
128
  channel_wake_confirm_submit: bool = False,
@@ -172,6 +173,7 @@ def run_windows_channel_terminal_proxy(
172
173
  enabled=inject_channel_messages,
173
174
  web_chat_only=inject_web_chat_only,
174
175
  wake_for_llm_delivery=wake_for_llm_delivery,
176
+ display_llm_delivery_body=channel_wake_display_body,
175
177
  submit_retry_count=submit_retry_count,
176
178
  confirm_submit=channel_wake_confirm_submit,
177
179
  bracketed_paste=windows_bracketed_paste,
@@ -194,7 +196,8 @@ def run_windows_channel_terminal_proxy(
194
196
  f"pid={proc.pid} transport={'conpty' if conpty is not None else 'console-input'} "
195
197
  f"enter={policy.enter_label(channel_enter_bytes)} "
196
198
  f"submit_retries={submit_retry_count} confirm_submit={bool(channel_wake_confirm_submit)} "
197
- f"bracketed_paste={windows_bracketed_paste}",
199
+ f"bracketed_paste={windows_bracketed_paste} "
200
+ f"display_body={bool(channel_wake_display_body)}",
198
201
  )
199
202
  try:
200
203
  while proc.poll() is None:
@@ -308,6 +311,7 @@ def run_posix_channel_terminal_proxy(
308
311
  inject_channel_messages: bool = True,
309
312
  inject_web_chat_only: bool = False,
310
313
  wake_for_llm_delivery: bool = False,
314
+ channel_wake_display_body: bool = False,
311
315
  synthetic_enter_bytes: str | bytes | None = None,
312
316
  normalize_bare_cr_for_synthetic_enter: bool = True,
313
317
  channel_wake_submit_retries: int = 1,
@@ -353,6 +357,7 @@ def run_posix_channel_terminal_proxy(
353
357
  enabled=inject_channel_messages,
354
358
  web_chat_only=inject_web_chat_only,
355
359
  wake_for_llm_delivery=wake_for_llm_delivery,
360
+ display_llm_delivery_body=channel_wake_display_body,
356
361
  submit_retry_count=submit_retry_count,
357
362
  confirm_submit=channel_wake_confirm_submit,
358
363
  bracketed_paste=channel_wake_bracketed_paste,
@@ -372,7 +377,8 @@ def run_posix_channel_terminal_proxy(
372
377
  "channel_stdin_proxy_enter_default "
373
378
  f"enter={policy.enter_label(channel_enter_bytes)} os={os.name} platform={sys.platform} "
374
379
  f"submit_retries={submit_retry_count} confirm_submit={bool(channel_wake_confirm_submit)} "
375
- f"bracketed_paste={bool(channel_wake_bracketed_paste)}",
380
+ f"bracketed_paste={bool(channel_wake_bracketed_paste)} "
381
+ f"display_body={bool(channel_wake_display_body)}",
376
382
  )
377
383
  terminal.reset_input_mode()
378
384
 
@@ -24,6 +24,13 @@ def prompt_contains(candidate: str, prompt: str) -> bool:
24
24
 
25
25
 
26
26
  def prompt_message_ids(text: str) -> set[int]:
27
+ normalized = re.sub(r"^[\x00-\x1f\x7f\s]+", "", str(text or ""))
28
+ if normalized.startswith("[ciel-wake]"):
29
+ # A visible Codex wake may contain an untrusted original body after
30
+ # the first line. Only the trusted marker line owns correlation IDs;
31
+ # strings such as ``id=999`` in the external body must not claim an
32
+ # unrelated queued message.
33
+ text = normalized.splitlines()[0]
27
34
  ids: set[int] = set()
28
35
  for match in _PROMPT_IDS_RE.finditer(str(text or "")):
29
36
  for raw in re.split(r"\D+", match.group(1)):
@@ -129,6 +129,7 @@ class ChannelPendingStatePorts:
129
129
  @dataclass(frozen=True, slots=True)
130
130
  class ChannelPendingDeliveryPorts:
131
131
  format_llm_delivery: Callable[[list[dict[str, Any]]], str]
132
+ format_visible_llm_delivery: Callable[[list[dict[str, Any]]], str]
132
133
  format_web_chat: Callable[[list[dict[str, Any]]], str]
133
134
  format_standard: Callable[[list[dict[str, Any]]], str]
134
135
  enter_label: Callable[[bytes], str]
@@ -143,7 +144,7 @@ class ChannelPendingDeliveryPorts:
143
144
  class ChannelPendingIoPorts:
144
145
  inject_lock: Any
145
146
  read_messages: Callable[..., list[dict[str, Any]]]
146
- write_prompt: Callable[..., None]
147
+ write_prompt: Callable[..., bool]
147
148
  compact_read: Callable[[], dict[str, Any] | None]
148
149
  compact_clear: Callable[[], None]
149
150
  messages_path: Path
@@ -331,7 +332,7 @@ class ChannelWakeContext:
331
332
  submit_delay_seconds: float | None = None,
332
333
  write_all: Callable[[Any, bytes], None] | None = None,
333
334
  snapshot: Callable[[], str | None] | None = None,
334
- ) -> None:
335
+ ) -> bool:
335
336
  delay = (
336
337
  self.input.submit_delay_seconds()
337
338
  if submit_delay_seconds is None
@@ -343,7 +344,7 @@ class ChannelWakeContext:
343
344
  snapshot=snapshot or self.current_tmux_pane_text,
344
345
  log=self.input.log,
345
346
  )
346
- injector.inject(
347
+ return injector.inject(
347
348
  channel_injection.CallableInputTransport(
348
349
  master_fd, write_all or self.write_all
349
350
  ),
@@ -530,6 +531,9 @@ class ChannelWakeContext:
530
531
  ),
531
532
  prompts=ChannelInjectionPrompts(
532
533
  llm_delivery=self.pending_delivery.format_llm_delivery,
534
+ visible_llm_delivery=(
535
+ self.pending_delivery.format_visible_llm_delivery
536
+ ),
533
537
  web_chat=self.pending_delivery.format_web_chat,
534
538
  standard=self.pending_delivery.format_standard,
535
539
  enter_bytes=self.enter_bytes,
@@ -34,6 +34,9 @@ class CliParserSettings:
34
34
  web_fetch: CliHandler
35
35
  log_level: CliHandler
36
36
  external_events: CliHandler
37
+ transcript_events: CliHandler
38
+ usage_events: CliHandler
39
+ usage_api_key: CliHandler
37
40
  remote_instructions: CliHandler
38
41
  remote_memory: CliHandler
39
42
 
@@ -91,6 +94,9 @@ def build_cli_parser(services: CliParserServices) -> argparse.ArgumentParser:
91
94
  _add_optional_value_command(commands, "web-fetch", services.settings.web_fetch)
92
95
  _add_optional_value_command(commands, "log-level", services.settings.log_level)
93
96
  _add_values_command(commands, "external-events", services.settings.external_events)
97
+ _add_values_command(commands, "transcript-events", services.settings.transcript_events)
98
+ _add_values_command(commands, "usage-events", services.settings.usage_events)
99
+ _add_values_command(commands, "usage-api-key", services.settings.usage_api_key)
94
100
  _add_values_command(commands, "remote-instructions", services.settings.remote_instructions)
95
101
  _add_values_command(commands, "remote-memory", services.settings.remote_memory)
96
102
  _add_optional_value_command(commands, "ollama-native", services.provider.ollama_native)
@@ -67,6 +67,8 @@ class ProviderPassthroughProjectionPorts:
67
67
  responses_request_max_bytes: Callable[[str, dict[str, Any]], int | None] = (
68
68
  lambda _provider, _config: None
69
69
  )
70
+ finalize_chat: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
71
+ finalize_responses: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
70
72
 
71
73
 
72
74
  @dataclass(frozen=True, slots=True)
@@ -249,6 +251,7 @@ class CodexBackendContext:
249
251
  urlopen=self.provider_transport.urlopen,
250
252
  timeout_seconds=self.provider_transport.timeout_seconds,
251
253
  copy_response_headers=self.provider_transport.copy_response_headers,
254
+ finalize_body=self.provider_projection.finalize_chat,
252
255
  )
253
256
  )
254
257
 
@@ -279,6 +282,7 @@ class CodexBackendContext:
279
282
  request_max_bytes=self.provider_projection.responses_request_max_bytes,
280
283
  estimate_tokens=self.replay.estimate_tokens,
281
284
  compact_responses=self.replay.compact_responses,
285
+ finalize_body=self.provider_projection.finalize_responses,
282
286
  )
283
287
  )
284
288
 
@@ -27,6 +27,13 @@ CODEX_EMPTY_REASONING_CONTINUATION_NUDGE = (
27
27
  "or provide the concrete final answer if no tool is needed."
28
28
  )
29
29
 
30
+ CODEX_REPEATED_TOOL_CONTINUATION_NUDGE = (
31
+ "The runtime stopped an exact tool call because the same call and result are "
32
+ "already present twice in this turn. Do not repeat that call. Use the existing "
33
+ "result, choose a different action if one is required, or provide the concrete "
34
+ "final answer now."
35
+ )
36
+
30
37
  RUNTIME_REASONING_ONLY_NOTICE_PREFIXES = (
31
38
  "[ciel-runtime] Upstream model returned reasoning without a final answer or tool call.",
32
39
  "[ciel-runtime] Upstream model exhausted its output budget during reasoning",
@@ -37,6 +44,13 @@ RUNTIME_EMPTY_END_TURN_NOTICE_PREFIX = (
37
44
  "tool call."
38
45
  )
39
46
 
47
+ RUNTIME_REPEATED_TOOL_NOTICE_PREFIX = (
48
+ "[ciel-runtime] Stopped an identical completed tool call from repeating."
49
+ )
50
+
51
+ RUNTIME_CONTROL_MESSAGE_KEY = "ciel_runtime_control"
52
+ RUNTIME_REPEATED_TOOL_RECOVERY = "repeated_tool_call_recovery"
53
+
40
54
  KIMI_FOLLOWUP_PROMISE_RE = re.compile(
41
55
  r"(?:겠습니다|할게요|해볼게요|하겠습니다|"
42
56
  r"i(?:'|’)ll\b[^\n]*|i\s+will\b[^\n]*|let\s+me\b[^\n]*)[.!?。!?]?\s*$",
@@ -97,6 +111,21 @@ def message_has_only_empty_end_turn_notice(message: dict[str, Any]) -> bool:
97
111
  )
98
112
 
99
113
 
114
+ def message_has_only_repeated_tool_notice(message: dict[str, Any]) -> bool:
115
+ """Identify the runtime's internal repeated-tool guard projection."""
116
+
117
+ texts = [
118
+ str(block.get("text") or "").strip()
119
+ for block in message.get("content") or []
120
+ if isinstance(block, dict)
121
+ and block.get("type") == "text"
122
+ and str(block.get("text") or "").strip()
123
+ ]
124
+ return bool(texts) and all(
125
+ text.startswith(RUNTIME_REPEATED_TOOL_NOTICE_PREFIX) for text in texts
126
+ )
127
+
128
+
100
129
  def kimi_message_promises_followup(message: dict[str, Any]) -> bool:
101
130
  """Recognize Kimi ending a reasoning turn with an unperformed next action."""
102
131
 
@@ -129,8 +158,24 @@ def message_without_empty_end_turn_notice(message: dict[str, Any]) -> dict[str,
129
158
  return projected
130
159
 
131
160
 
161
+ def message_without_repeated_tool_notice(message: dict[str, Any]) -> dict[str, Any]:
162
+ if not message_has_only_repeated_tool_notice(message):
163
+ return message
164
+ projected = dict(message)
165
+ projected["content"] = [
166
+ dict(block) if isinstance(block, dict) else block
167
+ for block in message.get("content") or []
168
+ if not (isinstance(block, dict) and block.get("type") == "text")
169
+ ]
170
+ return projected
171
+
172
+
132
173
  def body_with_continuation_nudge(
133
- body: dict[str, Any], message: dict[str, Any], nudge: str = CODEX_CONTINUATION_NUDGE
174
+ body: dict[str, Any],
175
+ message: dict[str, Any],
176
+ nudge: str = CODEX_CONTINUATION_NUDGE,
177
+ *,
178
+ control: str | None = None,
134
179
  ) -> dict[str, Any]:
135
180
  """Replay the request with the stalled reply and an explicit continue turn."""
136
181
 
@@ -138,7 +183,10 @@ def body_with_continuation_nudge(
138
183
  assistant_text = message_text(message).strip()
139
184
  if assistant_text:
140
185
  messages.append({"role": "assistant", "content": [{"type": "text", "text": assistant_text}]})
141
- messages.append({"role": "user", "content": [{"type": "text", "text": nudge}]})
186
+ continuation = {"role": "user", "content": [{"type": "text", "text": nudge}]}
187
+ if control:
188
+ continuation[RUNTIME_CONTROL_MESSAGE_KEY] = control
189
+ messages.append(continuation)
142
190
  retried = dict(body)
143
191
  retried["messages"] = messages
144
192
  return retried
@@ -197,9 +245,9 @@ def recover_preamble_only_turn(
197
245
  return message
198
246
  text = message_text(message)
199
247
  empty_end_turn = message_has_only_empty_end_turn_notice(message)
200
- kimi_reasoning_only = (
201
- (provider or "").strip().lower() == "kimi"
202
- and message_has_reasoning(message)
248
+ repeated_tool_guard = message_has_only_repeated_tool_notice(message)
249
+ reasoning_only = (
250
+ message_has_reasoning(message)
203
251
  and (not text.strip() or message_has_only_reasoning_notice(message))
204
252
  )
205
253
  kimi_promised_followup = (
@@ -209,7 +257,8 @@ def recover_preamble_only_turn(
209
257
  )
210
258
  if (
211
259
  not empty_end_turn
212
- and not kimi_reasoning_only
260
+ and not repeated_tool_guard
261
+ and not reasoning_only
213
262
  and not kimi_promised_followup
214
263
  and not services.should_retry(body, text, [])
215
264
  ):
@@ -218,8 +267,10 @@ def recover_preamble_only_turn(
218
267
  reason = (
219
268
  "empty_end_turn"
220
269
  if empty_end_turn
270
+ else "repeated_tool_call"
271
+ if repeated_tool_guard
221
272
  else "reasoning_only"
222
- if kimi_reasoning_only
273
+ if reasoning_only
223
274
  else "promised_followup"
224
275
  if kimi_promised_followup
225
276
  else "preamble_only"
@@ -231,8 +282,11 @@ def recover_preamble_only_turn(
231
282
  )
232
283
  try:
233
284
  nudge = (
285
+ CODEX_REPEATED_TOOL_CONTINUATION_NUDGE
286
+ if repeated_tool_guard
287
+ else
234
288
  CODEX_EMPTY_REASONING_CONTINUATION_NUDGE
235
- if empty_end_turn or kimi_reasoning_only
289
+ if empty_end_turn or reasoning_only
236
290
  else CODEX_CONTINUATION_NUDGE
237
291
  )
238
292
  recovery_config = dict(pcfg)
@@ -244,12 +298,15 @@ def recover_preamble_only_turn(
244
298
  recovery_config,
245
299
  body_with_continuation_nudge(
246
300
  body,
247
- message_without_empty_end_turn_notice(message)
301
+ message_without_repeated_tool_notice(message)
302
+ if repeated_tool_guard
303
+ else message_without_empty_end_turn_notice(message)
248
304
  if empty_end_turn
249
305
  else message_without_reasoning_notice(message)
250
- if kimi_reasoning_only
306
+ if reasoning_only
251
307
  else message,
252
308
  nudge,
309
+ control=(RUNTIME_REPEATED_TOOL_RECOVERY if repeated_tool_guard else None),
253
310
  ),
254
311
  )
255
312
  except Exception as exc: # noqa: BLE001 - recovery must never fail the turn
@@ -260,8 +317,11 @@ def recover_preamble_only_turn(
260
317
  return message
261
318
  if not isinstance(retried, dict):
262
319
  return message
263
- if empty_end_turn or kimi_reasoning_only:
264
- if not message_has_tool_use(retried) and not message_text(retried).strip():
320
+ if empty_end_turn or reasoning_only or repeated_tool_guard:
321
+ if (
322
+ message_has_only_repeated_tool_notice(retried)
323
+ or (not message_has_tool_use(retried) and not message_text(retried).strip())
324
+ ):
265
325
  return message
266
326
  return retried
267
327
  if not message_has_tool_use(retried):
@@ -286,6 +346,7 @@ def _merged(original: dict[str, Any], retried: dict[str, Any]) -> dict[str, Any]
286
346
  __all__ = [
287
347
  "CODEX_CONTINUATION_NUDGE",
288
348
  "CODEX_EMPTY_REASONING_CONTINUATION_NUDGE",
349
+ "CODEX_REPEATED_TOOL_CONTINUATION_NUDGE",
289
350
  "RUNTIME_EMPTY_END_TURN_NOTICE_PREFIX",
290
351
  "CodexTurnRecoveryServices",
291
352
  "body_with_codex_compat_instructions",
@@ -294,8 +355,10 @@ __all__ = [
294
355
  "message_has_reasoning",
295
356
  "message_has_only_reasoning_notice",
296
357
  "message_has_only_empty_end_turn_notice",
358
+ "message_has_only_repeated_tool_notice",
297
359
  "kimi_message_promises_followup",
298
360
  "message_without_empty_end_turn_notice",
361
+ "message_without_repeated_tool_notice",
299
362
  "message_without_reasoning_notice",
300
363
  "message_text",
301
364
  "recover_preamble_only_turn",
@@ -34,6 +34,52 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
34
34
  migrations = {}
35
35
  cfg["migrations"] = migrations
36
36
 
37
+ marker = "ox_alpha_provider_catalogs_20260823"
38
+ if not migrations.get(marker):
39
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
40
+ additions = {
41
+ "openrouter": ("stealth/ox-alpha", None),
42
+ "opencode": ("x-preview-f-free", "openai-chat"),
43
+ "opencode-go": ("ox-alpha-free", "openai-chat"),
44
+ }
45
+ for provider_name, (model, endpoint) in additions.items():
46
+ pcfg = providers.get(provider_name)
47
+ if not isinstance(pcfg, dict):
48
+ continue
49
+ custom = pcfg.get("custom_models")
50
+ if not isinstance(custom, list):
51
+ custom = []
52
+ pcfg["custom_models"] = custom
53
+ known = {
54
+ normalize_model_id(provider_name, str(item))
55
+ for item in custom
56
+ if str(item).strip()
57
+ }
58
+ if normalize_model_id(provider_name, model) not in known:
59
+ custom.append(model)
60
+ if endpoint:
61
+ model_endpoints = pcfg.get("model_endpoints")
62
+ if not isinstance(model_endpoints, dict):
63
+ model_endpoints = {}
64
+ pcfg["model_endpoints"] = model_endpoints
65
+ model_endpoints.setdefault(model, endpoint)
66
+ migrations[marker] = True
67
+
68
+ marker = "opencode_catalog_protocol_refresh_20260823"
69
+ if not migrations.get(marker):
70
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
71
+ zen = providers.get("opencode")
72
+ if isinstance(zen, dict):
73
+ custom = zen.get("custom_models")
74
+ if isinstance(custom, list):
75
+ zen["custom_models"] = [
76
+ model
77
+ for model in custom
78
+ if normalize_model_id("opencode", str(model))
79
+ != "qwen3.6-plus-free"
80
+ ]
81
+ migrations[marker] = True
82
+
37
83
  marker = "alibaba_provider_catalogs_20260806"
38
84
  if not migrations.get(marker):
39
85
  providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
@@ -73,6 +119,62 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
73
119
  custom.append(model)
74
120
  migrations[marker] = True
75
121
 
122
+ marker = "alibaba_qwen38_singapore_20260821"
123
+ if not migrations.get(marker):
124
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
125
+ model_studio = providers.get("alims-intl")
126
+ if isinstance(model_studio, dict):
127
+ custom = model_studio.get("custom_models")
128
+ if not isinstance(custom, list):
129
+ custom = []
130
+ model_studio["custom_models"] = custom
131
+ known = {
132
+ normalize_model_id("alims-intl", str(model))
133
+ for model in custom
134
+ if str(model).strip()
135
+ }
136
+ if normalize_model_id("alims-intl", "qwen3.8-max") not in known:
137
+ custom.insert(0, "qwen3.8-max")
138
+ model_studio["region"] = "ap-southeast-1"
139
+ old_default_profile = (
140
+ normalize_model_id(
141
+ "alims-intl", str(model_studio.get("current_model") or "")
142
+ )
143
+ == "qwen3.7-max"
144
+ and normalize_model_id(
145
+ "alims-intl", str(model_studio.get("opus_model") or "")
146
+ )
147
+ == "qwen3.7-max"
148
+ and (positive_int(model_studio.get("max_output_tokens")) or 0) == 65_536
149
+ and str(model_studio.get("effort_level") or "").strip().lower()
150
+ == "high"
151
+ )
152
+ if old_default_profile:
153
+ model_studio.update(
154
+ {
155
+ "current_model": "qwen3.8-max",
156
+ "opus_model": "qwen3.8-max",
157
+ "context_window": 1_000_000,
158
+ "max_model_len": 1_000_000,
159
+ "max_output_tokens": 131_072,
160
+ "effort_level": "xhigh",
161
+ }
162
+ )
163
+ for provider_name in ("alitoken", "alitoken-individual"):
164
+ pcfg = providers.get(provider_name)
165
+ if not isinstance(pcfg, dict):
166
+ continue
167
+ selected = normalize_model_id(
168
+ provider_name, str(pcfg.get("current_model") or "")
169
+ )
170
+ if selected != "qwen3.8-max":
171
+ continue
172
+ if (positive_int(pcfg.get("context_window")) or 0) == 1_048_576:
173
+ pcfg["context_window"] = 1_000_000
174
+ if (positive_int(pcfg.get("max_model_len")) or 0) == 1_048_576:
175
+ pcfg["max_model_len"] = 1_000_000
176
+ migrations[marker] = True
177
+
76
178
  marker = "alibaba_token_plan_singapore_20260806"
77
179
  if not migrations.get(marker):
78
180
  providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
@@ -310,16 +412,6 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
310
412
 
311
413
  marker = "opencode_zen_qwen36_plus_free_model_20260614"
312
414
  if not migrations.get(marker):
313
- providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
314
- pcfg = providers.get("opencode")
315
- if isinstance(pcfg, dict):
316
- custom = pcfg.get("custom_models")
317
- if not isinstance(custom, list):
318
- custom = []
319
- pcfg["custom_models"] = custom
320
- normalized_custom = {normalize_model_id("opencode", str(mid)) for mid in custom if str(mid).strip()}
321
- if "qwen3.6-plus-free" not in normalized_custom:
322
- custom.append("qwen3.6-plus-free")
323
415
  migrations[marker] = True
324
416
 
325
417
  marker = "opencode_qwen36_plus_parameters_20260614"
@@ -112,6 +112,15 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
112
112
  "web_backends": {},
113
113
  "workspace_mcp": {"servers": {}},
114
114
  "external_event_receivers": {},
115
+ "transcript_events": {
116
+ "enabled": False,
117
+ "url": "",
118
+ "authorization": "",
119
+ "timeout_seconds": 5,
120
+ "poll_interval_ms": 1000,
121
+ "max_batch_bytes": 1048576,
122
+ "start_mode": "tail",
123
+ },
115
124
  "remote_instructions": {
116
125
  "enabled": False,
117
126
  "claude_url": "",