@oneciel-ai/ciel-runtime 0.2.37 → 0.2.39

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 (142) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +5 -2
  3. package/ciel_runtime.py +94 -96
  4. package/ciel_runtime_support/anthropic_model_policy.py +20 -3
  5. package/ciel_runtime_support/architecture.py +5 -0
  6. package/ciel_runtime_support/channel_inflight.py +4 -4
  7. package/ciel_runtime_support/channel_injection.py +56 -1
  8. package/ciel_runtime_support/channel_mcp_tools.py +34 -0
  9. package/ciel_runtime_support/channel_message_policy.py +9 -1
  10. package/ciel_runtime_support/channel_message_prompt.py +23 -0
  11. package/ciel_runtime_support/channel_pending_injection.py +86 -32
  12. package/ciel_runtime_support/channel_pending_poll.py +6 -2
  13. package/ciel_runtime_support/channel_runtime_environment.py +0 -11
  14. package/ciel_runtime_support/channel_terminal_context.py +0 -6
  15. package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
  16. package/ciel_runtime_support/channel_transcript_repository.py +16 -1
  17. package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
  18. package/ciel_runtime_support/channel_wake_context.py +6 -13
  19. package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
  20. package/ciel_runtime_support/chat_http_controller.py +88 -5
  21. package/ciel_runtime_support/claude_environment.py +100 -3
  22. package/ciel_runtime_support/claude_launch_assembly.py +4 -0
  23. package/ciel_runtime_support/claude_session_socket.py +181 -0
  24. package/ciel_runtime_support/cli_application_context.py +18 -10
  25. package/ciel_runtime_support/cli_dispatch.py +19 -3
  26. package/ciel_runtime_support/cli_parser.py +2 -0
  27. package/ciel_runtime_support/cli_usage.py +3 -2
  28. package/ciel_runtime_support/codex_backend_context.py +8 -3
  29. package/ciel_runtime_support/codex_completion_gate.py +198 -0
  30. package/ciel_runtime_support/codex_turn_recovery.py +240 -83
  31. package/ciel_runtime_support/compatibility_protocol.py +5 -2
  32. package/ciel_runtime_support/config_migrations.py +79 -0
  33. package/ciel_runtime_support/config_repository.py +7 -0
  34. package/ciel_runtime_support/context_summary_policy.py +24 -4
  35. package/ciel_runtime_support/external_event_menu.py +107 -0
  36. package/ciel_runtime_support/external_event_receiver.py +19 -1
  37. package/ciel_runtime_support/launch_state.py +2 -0
  38. package/ciel_runtime_support/managed_tool_injection.py +81 -0
  39. package/ciel_runtime_support/muse_runtime_context.py +291 -0
  40. package/ciel_runtime_support/ollama_thinking.py +12 -26
  41. package/ciel_runtime_support/prelaunch.py +13 -0
  42. package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
  43. package/ciel_runtime_support/prompt_compaction.py +39 -0
  44. package/ciel_runtime_support/provider_files_proxy.py +248 -0
  45. package/ciel_runtime_support/provider_model_identity.py +10 -2
  46. package/ciel_runtime_support/provider_option_cli.py +1 -1
  47. package/ciel_runtime_support/provider_request_access.py +54 -1
  48. package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
  49. package/ciel_runtime_support/providers/alibaba.py +34 -1
  50. package/ciel_runtime_support/providers/anthropic.py +18 -1
  51. package/ciel_runtime_support/providers/meta.py +252 -21
  52. package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
  53. package/ciel_runtime_support/remote_bridge.py +4 -0
  54. package/ciel_runtime_support/remote_instructions.py +5 -0
  55. package/ciel_runtime_support/remote_memory.py +1 -1
  56. package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
  57. package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
  58. package/ciel_runtime_support/responses_input_compatibility.py +19 -3
  59. package/ciel_runtime_support/router_http.py +301 -4
  60. package/ciel_runtime_support/router_observability_context.py +26 -0
  61. package/ciel_runtime_support/router_server_context.py +1 -0
  62. package/ciel_runtime_support/runtime_adapters.py +31 -0
  63. package/ciel_runtime_support/runtime_constants.py +23 -1
  64. package/ciel_runtime_support/runtime_input_gateway.py +85 -5
  65. package/ciel_runtime_support/runtime_input_status.py +150 -0
  66. package/ciel_runtime_support/runtime_launch.py +35 -4
  67. package/ciel_runtime_support/runtime_paths.py +1 -0
  68. package/ciel_runtime_support/speech_http_controller.py +3 -2
  69. package/ciel_runtime_support/streaming_anthropic.py +3 -0
  70. package/ciel_runtime_support/tool_call_events.py +98 -0
  71. package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
  72. package/ciel_runtime_support/ui_text.py +1 -0
  73. package/ciel_runtime_support/web_search_result_events.py +130 -0
  74. package/ciel_runtime_support/web_ui.py +1 -0
  75. package/ciel_runtime_support/windows_conpty.py +92 -8
  76. package/ciel_runtime_support/workspace_mcp.py +4 -1
  77. package/ciel_runtime_support/workspace_state.py +1 -0
  78. package/docs/Configuration.md +20 -4
  79. package/docs/MCP-Channels.md +79 -6
  80. package/docs/Managed-Tool-Injection.md +25 -0
  81. package/docs/Module-Map.md +4 -0
  82. package/docs/Muse-Code.md +73 -0
  83. package/docs/Observability.md +39 -0
  84. package/docs/Providers.md +29 -2
  85. package/docs/Remote-Bridge.md +12 -0
  86. package/docs/Router.md +1 -0
  87. package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
  88. package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
  89. package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
  90. package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
  91. package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
  92. package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
  93. package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
  94. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
  95. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
  96. package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
  97. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
  98. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
  99. package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
  100. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
  101. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
  102. package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
  103. package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
  104. package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
  105. package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
  106. package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
  107. package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
  108. package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
  109. package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
  110. package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
  111. package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
  112. package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
  113. package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
  114. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
  115. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
  116. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
  117. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
  118. package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
  119. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
  120. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
  121. package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
  122. package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
  123. package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
  124. package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
  125. package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
  126. package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
  127. package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
  128. package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
  129. package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
  130. package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
  131. package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
  132. package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
  133. package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
  134. package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
  135. package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
  136. package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
  137. package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
  138. package/docs/journal/2026/09/07/fixes/codex/native-web-transport.okf +39 -0
  139. package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
  140. package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
  141. package/docs/journal/2026/09/07/release/stable/0.2.39.okf +10 -0
  142. package/package.json +1 -1
@@ -6,7 +6,7 @@ from dataclasses import dataclass
6
6
  from typing import Any, Callable, Literal
7
7
 
8
8
 
9
- InflightAction = Literal["none", "completed", "unseen_retry", "stale", "waiting"]
9
+ InflightAction = Literal["none", "completed", "failed", "waiting"]
10
10
 
11
11
 
12
12
  @dataclass(frozen=True, slots=True)
@@ -71,9 +71,9 @@ def advance_channel_inflight(
71
71
  effects.release_wake(message_id)
72
72
  effects.log(
73
73
  "WARN",
74
- f"{policy.log_namespace}_unseen_retry message_id={message_id} age={age:.1f}s",
74
+ f"{policy.log_namespace}_unseen_failed message_id={message_id} age={age:.1f}s",
75
75
  )
76
- return _reset("unseen_retry", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
76
+ return _reset("failed", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
77
77
  if policy.is_stale(state, snapshot.started_at, snapshot.now):
78
78
  if policy.commit_cursor_on_stale and snapshot.cursor is not None:
79
79
  effects.commit_cursor(snapshot.cursor)
@@ -83,7 +83,7 @@ def advance_channel_inflight(
83
83
  f"{policy.log_namespace}_{policy.stale_event} message_id={message_id} state={state} "
84
84
  f"age={age:.1f}s cursor={snapshot.cursor or '-'}",
85
85
  )
86
- return _reset("stale", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
86
+ return _reset("failed", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
87
87
  if snapshot.now - snapshot.logged_at >= policy.waiting_log_interval:
88
88
  effects.log(
89
89
  "INFO",
@@ -33,6 +33,13 @@ class InputTransport(Protocol):
33
33
  expected_prompt: str | None = None,
34
34
  ) -> bool | None: ...
35
35
 
36
+ def clear_unsubmitted_prompt(
37
+ self,
38
+ clear_input: bytes,
39
+ expected_prompt: str,
40
+ timeout_seconds: float = 2.0,
41
+ ) -> bool: ...
42
+
36
43
 
37
44
  @dataclass(frozen=True)
38
45
  class RuntimeInjectionPolicy:
@@ -139,9 +146,16 @@ class ChannelPromptInjector:
139
146
  policy,
140
147
  )
141
148
  if prompt_ready is False:
149
+ cleared = self._clear_unsubmitted_prompt(
150
+ transport,
151
+ policy.clear_input,
152
+ prompt_text,
153
+ policy.input_drain_timeout_seconds,
154
+ )
142
155
  self._log(
143
156
  "WARN",
144
- "channel_input_submit_deferred reason=prompt_render_timeout",
157
+ "channel_input_submit_failed reason=prompt_render_timeout "
158
+ f"draft_clear={'confirmed' if cleared else 'unverified'} retry=disabled",
145
159
  )
146
160
  return False
147
161
 
@@ -178,6 +192,36 @@ class ChannelPromptInjector:
178
192
  )
179
193
  return False
180
194
 
195
+ def _clear_unsubmitted_prompt(
196
+ self,
197
+ transport: InputTransport,
198
+ clear_input: bytes,
199
+ prompt: str,
200
+ timeout_seconds: float,
201
+ ) -> bool:
202
+ clear = getattr(transport, "clear_unsubmitted_prompt", None)
203
+ if not callable(clear):
204
+ self._log(
205
+ "WARN",
206
+ "channel_input_draft_clear result=unavailable retry=disabled",
207
+ )
208
+ return False
209
+ try:
210
+ cleared = bool(clear(clear_input, prompt, timeout_seconds))
211
+ except Exception as exc:
212
+ self._log(
213
+ "ERROR",
214
+ "channel_input_draft_clear result=error retry=disabled "
215
+ f"error={type(exc).__name__}: {exc}",
216
+ )
217
+ return False
218
+ self._log(
219
+ "INFO" if cleared else "WARN",
220
+ "channel_input_draft_clear "
221
+ f"result={'confirmed' if cleared else 'unverified'} retry=disabled",
222
+ )
223
+ return cleared
224
+
181
225
  def _submission_snapshot(self, transport: InputTransport) -> str | None:
182
226
  """Use the host snapshot when available, then the transport's own view."""
183
227
 
@@ -334,6 +378,17 @@ class CallableInputTransport:
334
378
  )
335
379
  )
336
380
 
381
+ def clear_unsubmitted_prompt(
382
+ self,
383
+ clear_input: bytes,
384
+ expected_prompt: str,
385
+ timeout_seconds: float = 2.0,
386
+ ) -> bool:
387
+ clear = getattr(self._target, "clear_unsubmitted_prompt", None)
388
+ if not callable(clear):
389
+ return False
390
+ return bool(clear(clear_input, expected_prompt, timeout_seconds))
391
+
337
392
 
338
393
  __all__ = [
339
394
  "CallableInputTransport",
@@ -20,10 +20,37 @@ class ChannelMcpToolServices:
20
20
  handle_llm_options: Callable[[str, str], tuple[list[str], bool]]
21
21
  read_runtime_inputs: Callable[..., list[dict[str, Any]]] | None = None
22
22
  telemetry_logs: Callable[[str, dict[str, Any]], dict[str, Any]] | None = None
23
+ submit_input: Callable[[dict[str, Any]], dict[str, Any]] | None = None
23
24
 
24
25
 
25
26
  def channel_mcp_tool_schemas() -> list[dict[str, Any]]:
26
27
  return [
28
+ {
29
+ "name": "submit_input",
30
+ "description": (
31
+ "Submit an external message to the active Ciel Runtime session. "
32
+ "Claude sessions use their authenticated session socket by default; "
33
+ "callers may explicitly select session_socket, tty, or router."
34
+ ),
35
+ "inputSchema": {
36
+ "type": "object",
37
+ "properties": {
38
+ "message": {"type": "string"},
39
+ "channel": {"type": "string"},
40
+ "sender_id": {"type": "string"},
41
+ "thread_id": {"type": "string"},
42
+ "input_transport": {
43
+ "type": "string",
44
+ "enum": ["session_socket", "tty", "router"],
45
+ },
46
+ "response_mode": {
47
+ "type": "string",
48
+ "enum": ["web_chat", "tty", "mcp"],
49
+ },
50
+ },
51
+ "required": ["message"],
52
+ },
53
+ },
27
54
  {
28
55
  "name": "compact_session",
29
56
  "description": (
@@ -213,6 +240,13 @@ def dispatch_channel_mcp_tool(
213
240
  "expires_at": request.get("expires_at"),
214
241
  },
215
242
  )
243
+ if name == "submit_input":
244
+ if services.submit_input is None:
245
+ return channel_mcp_tool_response(request_id, "runtime input submission is unavailable", True)
246
+ if not str(args.get("message") or args.get("content") or args.get("text") or "").strip():
247
+ return channel_mcp_tool_response(request_id, "submit_input requires message", True)
248
+ saved = services.submit_input(args)
249
+ return _json_response(request_id, {"ok": True, "message": saved})
216
250
  if name == "send_message":
217
251
  return _send_message(request_id, args, services)
218
252
  if name == "send_file":
@@ -125,13 +125,21 @@ def web_chat_input_mode(message: dict[str, Any]) -> str:
125
125
  return "voice" if value == "voice" else "text"
126
126
 
127
127
 
128
+ def message_raw_injection(message: dict[str, Any]) -> bool:
129
+ """Return whether only the exact admitted message text may reach the model."""
130
+
131
+ return _metadata(message).get("raw_injection") is True
132
+
133
+
128
134
  def message_input_transport(message: dict[str, Any]) -> str:
129
135
  """Return the per-request path used to enter the active model turn."""
130
136
 
131
137
  # Messages admitted before input_transport existed followed the router
132
138
  # delivery policy selected at launch. Preserve that persisted compatibility;
133
- # the HTTP API now always stamps its explicit default (tty).
139
+ # current input APIs always stamp their runtime-selected transport.
134
140
  value = str(_metadata(message).get("input_transport") or "router").strip().lower().replace("-", "_")
141
+ if value in {"session_socket", "socket", "claude_socket", "messaging_socket"}:
142
+ return "session_socket"
135
143
  if value in {"router", "llm", "context", "inband", "in_band"}:
136
144
  return "router"
137
145
  return "tty"
@@ -14,6 +14,7 @@ from ciel_runtime_support.channel_message_policy import (
14
14
  message_is_web_chat_request,
15
15
  message_is_external_event,
16
16
  message_meta_sources,
17
+ message_raw_injection,
17
18
  message_response_mcp,
18
19
  message_response_mode,
19
20
  string_list,
@@ -172,6 +173,8 @@ def _web_chat_reply_routes(
172
173
  routes: list[dict[str, str]] = []
173
174
  seen: set[tuple[str, str, str]] = set()
174
175
  for message in messages:
176
+ if message_raw_injection(message):
177
+ continue
175
178
  if not message_is_web_chat_request(message):
176
179
  continue
177
180
  if message_response_mode(message) != "web_chat":
@@ -257,6 +260,8 @@ def _mcp_reply_instruction(messages: list[dict[str, Any]]) -> str:
257
260
  instructions: list[str] = []
258
261
  seen: set[tuple[str, str, str]] = set()
259
262
  for message in messages:
263
+ if message_raw_injection(message):
264
+ continue
260
265
  if message_response_mode(message) != "mcp":
261
266
  continue
262
267
  hint = message_response_mcp(message)
@@ -321,6 +326,12 @@ def _format_cielarvis_internal_prompt(messages: list[dict[str, Any]]) -> str | N
321
326
 
322
327
 
323
328
  def format_web_chat_wake_batch_prompt(messages: list[dict[str, Any]]) -> str:
329
+ if len(messages) == 1 and message_raw_injection(messages[0]):
330
+ return str(
331
+ messages[0].get("message")
332
+ if messages[0].get("message") is not None
333
+ else ""
334
+ )
324
335
  if compact := _format_cielarvis_internal_prompt(messages):
325
336
  return compact
326
337
  if messages and all(
@@ -406,6 +417,12 @@ def wake_message_is_noise(message: dict[str, Any]) -> bool:
406
417
 
407
418
 
408
419
  def format_wake_batch_prompt(messages: list[dict[str, Any]]) -> str:
420
+ if len(messages) == 1 and message_raw_injection(messages[0]):
421
+ return str(
422
+ messages[0].get("message")
423
+ if messages[0].get("message") is not None
424
+ else ""
425
+ )
409
426
  if len(messages) == 1:
410
427
  return format_wake_prompt(messages[0])
411
428
  parts: list[str] = []
@@ -493,6 +510,12 @@ def message_llm_display_text(message: dict[str, Any]) -> str:
493
510
 
494
511
 
495
512
  def format_llm_batch_prompt(messages: list[dict[str, Any]]) -> str:
513
+ if len(messages) == 1 and message_raw_injection(messages[0]):
514
+ return str(
515
+ messages[0].get("message")
516
+ if messages[0].get("message") is not None
517
+ else ""
518
+ )
496
519
  prompt = "\n\n".join(message_llm_display_text(message) for message in messages)
497
520
  instruction = _response_instruction(messages)
498
521
  return f"{prompt}\n\n{instruction}" if instruction else prompt
@@ -10,6 +10,7 @@ from ciel_runtime_support.channel_message_policy import (
10
10
  message_is_external_event,
11
11
  message_response_mcp,
12
12
  message_response_mode,
13
+ message_raw_injection,
13
14
  web_chat_input_mode,
14
15
  )
15
16
 
@@ -44,11 +45,8 @@ class ChannelInjectionWakeStore:
44
45
  claim_prompt: Callable[[int, str], bool]
45
46
  clear_claim: Callable[[int], Any]
46
47
  release_stale: Callable[[int, bool], Any]
47
- mark_delivered: Callable[[int], bool]
48
- record_prompts: Callable[[list[dict[str, Any]], str], Any]
49
- rollback: Callable[[list[dict[str, Any]], list[int]], Any]
48
+ lifecycle: Any
50
49
  commit_cursor: Callable[[int], Any]
51
- body_fallback: Callable[[int], bool] = lambda _message_id: False
52
50
 
53
51
 
54
52
  @dataclass(frozen=True, slots=True)
@@ -57,6 +55,7 @@ class ChannelInjectionIO:
57
55
  read_messages: Callable[..., list[dict[str, Any]]]
58
56
  write_prompt: Callable[..., Any]
59
57
  log: Callable[[str, str], Any]
58
+ write_session_socket: Callable[[str, list[dict[str, Any]]], bool] | None = None
60
59
 
61
60
 
62
61
  @dataclass(frozen=True, slots=True)
@@ -103,12 +102,8 @@ def inject_pending_channel_messages(
103
102
  wake_store = services.wake_store
104
103
  io = services.io
105
104
  with io.inject_lock:
106
- if state.active_tool_call():
107
- io.log("INFO", f"channel_stdin_proxy_deferred cursor={last_id} reason=active_tool_call")
108
- return last_id
109
- if state.active_turn():
110
- io.log("INFO", f"channel_stdin_proxy_deferred cursor={last_id} reason=active_turn")
111
- return last_id
105
+ active_tool_call = state.active_tool_call()
106
+ active_turn = state.active_turn()
112
107
  if not web_chat_only:
113
108
  last_id = state.recover_cursor(last_id)
114
109
  pending: list[dict[str, Any]] = []
@@ -118,6 +113,7 @@ def inject_pending_channel_messages(
118
113
  batch_limit = services.policy.wake_batch_limit() if wake_for_llm_delivery else 1
119
114
  pending_batch_key: tuple[str, str, str] | None = None
120
115
  pending_uses_router = False
116
+ pending_uses_session_socket = False
121
117
  seen_event_keys: set[tuple[str, ...]] = set()
122
118
  for message in candidates:
123
119
  previous_last_id = last_id
@@ -126,6 +122,17 @@ def inject_pending_channel_messages(
126
122
  continue
127
123
  last_id = max(last_id, message_id)
128
124
  channel = message.get("channel")
125
+ failure_reason = wake_store.lifecycle.failure_reason(message_id)
126
+ if failure_reason:
127
+ io.log(
128
+ "ERROR",
129
+ "channel_stdin_proxy_blocked "
130
+ f"cursor={previous_last_id} message_id={message_id} "
131
+ f"reason=prior_submission_failed detail={failure_reason}",
132
+ )
133
+ if pending:
134
+ break
135
+ return previous_last_id
129
136
  if web_chat_only and not state.message_is_web_chat(message):
130
137
  io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason=not_web_chat")
131
138
  continue
@@ -138,11 +145,28 @@ def inject_pending_channel_messages(
138
145
  )
139
146
  continue
140
147
  metadata = message.get("meta") if isinstance(message.get("meta"), dict) else {}
148
+ input_transport = message_input_transport(message)
149
+ requested_session_socket = input_transport == "session_socket"
141
150
  requested_router = (
142
- message_input_transport(message) == "router"
151
+ input_transport == "router"
143
152
  if "input_transport" in metadata
144
153
  else wake_for_llm_delivery
145
154
  )
155
+ if requested_session_socket and io.write_session_socket is None:
156
+ io.log(
157
+ "WARN",
158
+ f"channel_stdin_proxy_transport_fallback cursor={previous_last_id} "
159
+ f"message_id={message_id} channel={channel} "
160
+ "requested=session_socket fallback=tty "
161
+ "reason=session_socket_transport_unavailable",
162
+ )
163
+ requested_session_socket = False
164
+ if not requested_session_socket and (active_tool_call or active_turn):
165
+ reason = "active_tool_call" if active_tool_call else "active_turn"
166
+ io.log("INFO", f"channel_stdin_proxy_deferred cursor={previous_last_id} reason={reason}")
167
+ if pending:
168
+ break
169
+ return previous_last_id
146
170
  if requested_router and not wake_for_llm_delivery:
147
171
  io.log(
148
172
  "WARN",
@@ -153,13 +177,6 @@ def inject_pending_channel_messages(
153
177
  break
154
178
  return previous_last_id
155
179
  candidate_uses_router = requested_router
156
- if candidate_uses_router and wake_store.body_fallback(message_id):
157
- io.log(
158
- "INFO",
159
- f"channel_stdin_proxy_skipped_noise message_id={message_id} "
160
- f"channel={channel} reason=stdin_wake_body_fallback",
161
- )
162
- continue
163
180
  skip_reason = state.message_skip_reason(message)
164
181
  if skip_reason:
165
182
  io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason={skip_reason}")
@@ -213,21 +230,30 @@ def inject_pending_channel_messages(
213
230
  + ":".join(response_mcp.get(key, "") for key in ("server", "tool", "hint"))
214
231
  )
215
232
  batch_key_base = (
233
+ ("raw", str(message_id))
234
+ if message_raw_injection(message)
235
+ else
216
236
  ("web_chat", web_chat_input_mode(message) + ":" + response_key)
217
237
  if state.message_is_web_chat(message)
218
238
  else ("external_event", str((message.get("meta") or {}).get("receiver_id") or ""))
219
239
  if message_is_external_event(message)
220
240
  else ("channel", "")
221
241
  )
222
- batch_key = (batch_key_base[0], batch_key_base[1], "router" if candidate_uses_router else "tty")
242
+ transport_key = (
243
+ "session_socket"
244
+ if requested_session_socket
245
+ else "router" if candidate_uses_router else "tty"
246
+ )
247
+ batch_key = (batch_key_base[0], batch_key_base[1], transport_key)
223
248
  if pending and pending_batch_key != batch_key:
224
249
  break
225
- if not candidate_uses_router and not wake_store.mark_delivered(message_id):
250
+ if not candidate_uses_router and not wake_store.lifecycle.mark_delivered(message_id):
226
251
  io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason=stdin_wake_delivered")
227
252
  continue
228
253
  pending.append(message)
229
254
  pending_batch_key = batch_key
230
255
  pending_uses_router = candidate_uses_router
256
+ pending_uses_session_socket = requested_session_socket
231
257
  if event_key:
232
258
  seen_event_keys.add(event_key)
233
259
  if candidate_uses_router and len(pending) == 1:
@@ -235,6 +261,11 @@ def inject_pending_channel_messages(
235
261
  last_id = message_id
236
262
  if len(pending) >= batch_limit:
237
263
  break
264
+ if not candidate_uses_router:
265
+ # A TUI/session-socket draft carries exactly one request. This
266
+ # prevents a later request from being appended to an earlier,
267
+ # not-yet-confirmed draft.
268
+ break
238
269
  if not pending:
239
270
  return last_id
240
271
  if pending_uses_router:
@@ -262,17 +293,39 @@ def inject_pending_channel_messages(
262
293
  claimed_ids.append(claim_id)
263
294
  submit_bytes = prompts.enter_bytes(enter_bytes)
264
295
  try:
265
- submitted = io.write_prompt(
266
- master_fd,
267
- prompt,
268
- submit_bytes,
269
- submit_retry_count=submit_retry_count,
270
- confirm_submit=confirm_submit,
271
- bracketed_paste=bracketed_paste,
272
- submit_delay_seconds=submit_delay_seconds,
273
- )
296
+ if pending_uses_session_socket:
297
+ submitted = bool(io.write_session_socket and io.write_session_socket(prompt, pending))
298
+ if submitted is False and not (active_tool_call or active_turn):
299
+ ids = ",".join(str(message.get("id") or "") for message in pending)
300
+ io.log(
301
+ "WARN",
302
+ "channel_stdin_proxy_transport_fallback "
303
+ f"message_ids={ids} requested=session_socket fallback=tty "
304
+ "reason=session_socket_submit_failed",
305
+ )
306
+ submitted = io.write_prompt(
307
+ master_fd,
308
+ prompt,
309
+ submit_bytes,
310
+ submit_retry_count=submit_retry_count,
311
+ confirm_submit=confirm_submit,
312
+ bracketed_paste=bracketed_paste,
313
+ submit_delay_seconds=submit_delay_seconds,
314
+ )
315
+ if submitted is not False:
316
+ pending_uses_session_socket = False
317
+ else:
318
+ submitted = io.write_prompt(
319
+ master_fd,
320
+ prompt,
321
+ submit_bytes,
322
+ submit_retry_count=submit_retry_count,
323
+ confirm_submit=confirm_submit,
324
+ bracketed_paste=bracketed_paste,
325
+ submit_delay_seconds=submit_delay_seconds,
326
+ )
274
327
  if submitted is False:
275
- wake_store.rollback(pending, claimed_ids)
328
+ wake_store.lifecycle.fail(pending, claimed_ids, "prompt_not_submitted")
276
329
  ids = ",".join(str(message.get("id") or "") for message in pending)
277
330
  io.log(
278
331
  "WARN",
@@ -280,9 +333,9 @@ def inject_pending_channel_messages(
280
333
  f"message_ids={ids} reason=prompt_not_submitted",
281
334
  )
282
335
  return return_last_id if pending_uses_router else previous_last_id
283
- wake_store.record_prompts(pending, prompt)
336
+ wake_store.lifecycle.record_prompts(pending, prompt)
284
337
  except Exception:
285
- wake_store.rollback(pending, claimed_ids)
338
+ wake_store.lifecycle.fail(pending, claimed_ids, "submission_exception")
286
339
  raise
287
340
  if not web_chat_only:
288
341
  if commit_cursor:
@@ -295,6 +348,7 @@ def inject_pending_channel_messages(
295
348
  "INFO",
296
349
  f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids} "
297
350
  f"channels={channels} enter={prompts.enter_label(submit_bytes)} "
351
+ f"transport={'session_socket' if pending_uses_session_socket else 'router' if pending_uses_router else 'tty'} "
298
352
  f"commit_cursor={commit_cursor} "
299
353
  f"display_body={bool(pending_uses_router and display_llm_delivery_body)}",
300
354
  )
@@ -140,7 +140,11 @@ def poll_pending_channel_messages(
140
140
  web_chat_only=options.web_chat_only,
141
141
  wake_for_llm_delivery=options.wake_for_llm_delivery,
142
142
  display_llm_delivery_body=options.display_llm_delivery_body,
143
- commit_cursor=False,
143
+ # A runtime without transcript confirmation (for example Muse Code)
144
+ # uses a single fire-and-forget TTY write. Persist its cursor in that
145
+ # same operation; otherwise the generic inflight verifier sees every
146
+ # successful delivery as "missing" and replays it.
147
+ commit_cursor=not options.confirm_submit,
144
148
  injected_message_ids=injected_ids,
145
149
  submit_retry_count=options.submit_retry_count,
146
150
  confirm_submit=options.confirm_submit,
@@ -148,7 +152,7 @@ def poll_pending_channel_messages(
148
152
  submit_delay_seconds=options.submit_delay_seconds,
149
153
  skip_blocking_wake_states=state.inflight_message_id is not None,
150
154
  )
151
- if injected_ids:
155
+ if injected_ids and options.confirm_submit:
152
156
  state.inflight_message_id = injected_ids[-1]
153
157
  state.inflight.attempts += 1
154
158
  # The injector deliberately returns the cursor preceding an LLM-delivery
@@ -113,17 +113,6 @@ class ChannelRuntimeEnvironmentPolicy:
113
113
  maximum=60.0,
114
114
  )
115
115
 
116
- def windows_wake_max_attempts(self) -> int:
117
- """Bound full-prompt attempts before switching to request-body delivery."""
118
- return self._bounded_int(
119
- self.environment.get(
120
- "CIEL_RUNTIME_WINDOWS_CHANNEL_WAKE_MAX_ATTEMPTS"
121
- ),
122
- default=2,
123
- minimum=1,
124
- maximum=4,
125
- )
126
-
127
116
  @staticmethod
128
117
  def inflight_is_stale(
129
118
  state: str,
@@ -42,7 +42,6 @@ class ChannelTerminalPolicyPorts:
42
42
  unseen_retry_seconds: Callable[[], float]
43
43
  inflight_is_stale: Callable[..., bool]
44
44
  log: Callable[[str, str], None]
45
- windows_wake_max_attempts: Callable[[], int]
46
45
 
47
46
 
48
47
  @dataclass(frozen=True, slots=True)
@@ -55,7 +54,6 @@ class ChannelTerminalPollingPorts:
55
54
  inject_pending: Callable[..., Any]
56
55
  wake_state: Callable[[int], Any]
57
56
  inflight_effects: Callable[[], Any]
58
- mark_body_fallback: Callable[[int, str], None]
59
57
  runtime_interaction: Callable[[], RuntimeInteractionEvent | None] = lambda: None
60
58
 
61
59
 
@@ -78,7 +76,6 @@ class ChannelTerminalWindowsPorts:
78
76
  startup_grace_seconds: Callable[[], float]
79
77
  reset_interval_seconds: Callable[[float], float]
80
78
  active_turn: Callable[[], bool]
81
- write_body_fallback: Callable[[Any, int, bytes], None]
82
79
  sleep: Callable[[float], None]
83
80
  open_conpty: Callable[[list[str], dict[str, str], Callable[[str, str], None]], Any | None]
84
81
 
@@ -121,7 +118,6 @@ class ChannelTerminalContext:
121
118
  unseen_retry_seconds=self.policy.unseen_retry_seconds,
122
119
  inflight_is_stale=self.policy.inflight_is_stale,
123
120
  log=self.policy.log,
124
- windows_wake_max_attempts=self.policy.windows_wake_max_attempts,
125
121
  )
126
122
 
127
123
  def polling_services(self) -> ChannelTerminalPolling:
@@ -134,7 +130,6 @@ class ChannelTerminalContext:
134
130
  inject_pending=self.polling.inject_pending,
135
131
  wake_state=self.polling.wake_state,
136
132
  inflight_effects=self.polling.inflight_effects,
137
- mark_body_fallback=self.polling.mark_body_fallback,
138
133
  runtime_interaction=self.polling.runtime_interaction,
139
134
  )
140
135
 
@@ -165,7 +160,6 @@ class ChannelTerminalContext:
165
160
  startup_grace_seconds=self.windows.startup_grace_seconds,
166
161
  reset_interval_seconds=self.windows.reset_interval_seconds,
167
162
  active_turn=self.windows.active_turn,
168
- write_body_fallback=self.windows.write_body_fallback,
169
163
  sleep=self.windows.sleep,
170
164
  open_conpty=self.windows.open_conpty,
171
165
  ),
@@ -62,7 +62,6 @@ class ChannelTerminalPolicy:
62
62
  unseen_retry_seconds: Callable[[], float]
63
63
  inflight_is_stale: Callable[..., bool]
64
64
  log: Callable[[str, str], None]
65
- windows_wake_max_attempts: Callable[[], int]
66
65
 
67
66
 
68
67
  @dataclass(frozen=True)
@@ -75,7 +74,6 @@ class ChannelTerminalPolling:
75
74
  inject_pending: Callable[..., Any]
76
75
  wake_state: Callable[[int], Any]
77
76
  inflight_effects: Callable[[], Any]
78
- mark_body_fallback: Callable[[int, str], None]
79
77
  runtime_interaction: Callable[[], RuntimeInteractionEvent | None] = lambda: None
80
78
 
81
79
  def input_busy(self) -> bool:
@@ -100,7 +98,6 @@ class ChannelWindowsConsole:
100
98
  startup_grace_seconds: Callable[[], float]
101
99
  reset_interval_seconds: Callable[[float], float]
102
100
  active_turn: Callable[[], bool]
103
- write_body_fallback: Callable[[Any, int, bytes], None]
104
101
  sleep: Callable[[float], None]
105
102
  open_conpty: Callable[[list[str], dict[str, str], Callable[[str, str], None]], Any | None]
106
103
 
@@ -113,15 +110,6 @@ class ChannelWindowsServices:
113
110
  console: ChannelWindowsConsole
114
111
 
115
112
 
116
- def windows_wake_requires_body_fallback(
117
- action: str, attempts: int, maximum_attempts: int
118
- ) -> bool:
119
- return bool(
120
- action in {"unseen_retry", "stale"}
121
- and attempts >= max(1, int(maximum_attempts))
122
- )
123
-
124
-
125
113
  def resolve_posix_child_exit_status(
126
114
  proc: Any,
127
115
  log: Callable[[str, str], None],
@@ -400,35 +388,8 @@ def run_windows_channel_terminal_proxy(
400
388
  pending_poll_state.last_id = inflight_update.last_id
401
389
  if inflight_update.action == "completed":
402
390
  pending_poll_state.inflight.attempts = 0
403
- elif inflight_update.action in {"unseen_retry", "stale"}:
404
- attempts = max(1, pending_poll_state.inflight.attempts)
405
- if windows_wake_requires_body_fallback(
406
- inflight_update.action,
407
- attempts,
408
- policy.windows_wake_max_attempts(),
409
- ):
410
- polling.mark_body_fallback(
411
- inflight_message_id,
412
- f"windows_console_{inflight_update.action}",
413
- )
414
- pending_poll_state.inflight.attempts = 0
415
- policy.log(
416
- "ERROR",
417
- "channel_windows_console_body_fallback "
418
- f"message_id={inflight_message_id} attempts={attempts} "
419
- f"reason={inflight_update.action}",
420
- )
421
- try:
422
- console.write_body_fallback(
423
- writer, inflight_message_id, channel_enter_bytes
424
- )
425
- except Exception as exc:
426
- policy.log(
427
- "ERROR",
428
- "channel_windows_console_body_fallback_wake_failed "
429
- f"message_id={inflight_message_id} "
430
- f"error={type(exc).__name__}: {exc}",
431
- )
391
+ elif inflight_update.action == "failed":
392
+ pending_poll_state.inflight.attempts = 0
432
393
  compact_poll_state = poll_pending_compaction(
433
394
  now,
434
395
  writer,
@@ -620,7 +581,7 @@ def run_posix_channel_terminal_proxy(
620
581
  unseen_retry_seconds=policy.unseen_retry_seconds(),
621
582
  waiting_log_interval=30.0,
622
583
  is_stale=policy.inflight_is_stale,
623
- commit_cursor_on_stale=True,
584
+ commit_cursor_on_stale=False,
624
585
  log_namespace="channel_stdin_proxy",
625
586
  stale_event="stale_inflight_skipped",
626
587
  ),