@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
@@ -15,15 +15,25 @@ import urllib.parse
15
15
  import urllib.request
16
16
  import uuid
17
17
 
18
+ from .architecture import MessageProtocol
19
+ from .prompt_injection import PromptInjector, append_anthropic_system_texts
18
20
  from .remote_instructions import (
19
21
  RUNTIME_FILES,
22
+ configured_url as configured_instruction_url,
20
23
  expand_environment_references,
24
+ normalized_instruction_sha256,
25
+ settings as instruction_settings,
21
26
  target_file,
22
27
  )
23
28
 
24
29
 
25
30
  MEMORY_POINTER_BEGIN = "<!-- ciel-runtime:remote-memory:begin -->"
26
31
  MEMORY_POINTER_END = "<!-- ciel-runtime:remote-memory:end -->"
32
+ MEMORY_REFERENCE_INSTRUCTION = (
33
+ "Memory guidance: Resolve these paths from the current workspace root. "
34
+ "Read the memory index first and use the relevant files under the memory "
35
+ "root as context for your work."
36
+ )
27
37
  DEFAULT_DIRECTORY = ".ciel/memory"
28
38
  DEFAULT_MAX_MANIFEST_BYTES = 1_048_576
29
39
  DEFAULT_MAX_FILE_BYTES = 4_194_304
@@ -44,6 +54,12 @@ _FORMAT_ALIASES = {
44
54
  "txt": "text",
45
55
  }
46
56
  _SHA256 = re.compile(r"^[0-9a-fA-F]{64}$")
57
+ _MEMORY_POINTER_PATTERN = re.compile(
58
+ re.escape(MEMORY_POINTER_BEGIN)
59
+ + r".*?"
60
+ + re.escape(MEMORY_POINTER_END),
61
+ re.DOTALL,
62
+ )
47
63
 
48
64
 
49
65
  def settings(config: Mapping[str, Any]) -> dict[str, Any]:
@@ -75,8 +91,9 @@ def _safe_relative_path(value: Any, *, field: str) -> PurePosixPath:
75
91
 
76
92
 
77
93
  def memory_directory(workspace: Path, config: Mapping[str, Any]) -> Path:
94
+ configured = str(settings(config).get("directory") or DEFAULT_DIRECTORY).strip()
78
95
  relative = _safe_relative_path(
79
- settings(config).get("directory") or DEFAULT_DIRECTORY,
96
+ configured,
80
97
  field="remote_memory.directory",
81
98
  )
82
99
  root = workspace.resolve()
@@ -194,36 +211,330 @@ def parse_manifest(
194
211
  return RemoteMemoryManifest(index_path, tuple(files))
195
212
 
196
213
 
197
- def _managed_pointer_block(index_address: str) -> str:
214
+ def _managed_pointer_block(index_address: str, root_address: str = "") -> str:
215
+ resolved_root = str(root_address or "").strip()
216
+ if not resolved_root:
217
+ resolved_root = PurePosixPath(index_address).parent.as_posix()
198
218
  return (
199
219
  f"{MEMORY_POINTER_BEGIN}\n"
220
+ f"Memory root: {resolved_root}\n"
200
221
  f"Memory index: {index_address}\n"
222
+ f"{MEMORY_REFERENCE_INSTRUCTION}\n"
201
223
  f"{MEMORY_POINTER_END}"
202
224
  )
203
225
 
204
226
 
205
- def update_memory_pointer(path: Path, index_address: str = "") -> bool:
227
+ def without_memory_pointer(value: str) -> str:
228
+ """Remove Ciel's managed memory block from trusted instruction text."""
229
+
230
+ return _MEMORY_POINTER_PATTERN.sub("", value).strip()
231
+
232
+
233
+ def _without_pointer_from_content(value: Any) -> tuple[Any, str]:
234
+ """Scrub managed blocks from a privileged text/content-part value."""
235
+
236
+ if isinstance(value, str):
237
+ matches = tuple(_MEMORY_POINTER_PATTERN.finditer(value))
238
+ return without_memory_pointer(value), matches[-1].group(0) if matches else ""
239
+ if isinstance(value, dict):
240
+ copied = dict(value)
241
+ text_key = next(
242
+ (
243
+ key
244
+ for key in ("text", "input_text", "output_text")
245
+ if isinstance(copied.get(key), str)
246
+ ),
247
+ "",
248
+ )
249
+ if not text_key:
250
+ return copied, ""
251
+ copied[text_key], pointer = _without_pointer_from_content(copied[text_key])
252
+ if not copied[text_key] and str(copied.get("type") or "") in {
253
+ "",
254
+ "text",
255
+ "input_text",
256
+ "output_text",
257
+ }:
258
+ return "", pointer
259
+ return copied, pointer
260
+ if not isinstance(value, list):
261
+ return value, ""
262
+ cleaned: list[Any] = []
263
+ pointer = ""
264
+ for part in value:
265
+ if isinstance(part, str):
266
+ next_part, found = _without_pointer_from_content(part)
267
+ if found:
268
+ pointer = found
269
+ if next_part:
270
+ cleaned.append(next_part)
271
+ continue
272
+ if not isinstance(part, dict):
273
+ cleaned.append(part)
274
+ continue
275
+ copied = dict(part)
276
+ text_key = next(
277
+ (
278
+ key
279
+ for key in ("text", "input_text", "output_text")
280
+ if isinstance(copied.get(key), str)
281
+ ),
282
+ "",
283
+ )
284
+ if text_key:
285
+ copied[text_key], found = _without_pointer_from_content(copied[text_key])
286
+ if found:
287
+ pointer = found
288
+ if not copied[text_key] and str(copied.get("type") or "") in {
289
+ "",
290
+ "text",
291
+ "input_text",
292
+ "output_text",
293
+ }:
294
+ continue
295
+ cleaned.append(copied)
296
+ return cleaned, pointer
297
+
298
+
299
+ def _append_pointer_to_content(value: Any, pointer: str) -> Any:
300
+ if isinstance(value, list):
301
+ return [*value, {"type": "text", "text": pointer}]
302
+ current = str(value or "").rstrip()
303
+ return f"{current}\n\n{pointer}" if current else pointer
304
+
305
+
306
+ def _messages_without_memory_pointer(
307
+ messages: Any,
308
+ ) -> tuple[list[Any], str, list[int]]:
309
+ if not isinstance(messages, list):
310
+ return [], "", []
311
+ projected: list[Any] = []
312
+ pointer = ""
313
+ privileged: list[int] = []
314
+ for message in messages:
315
+ if not isinstance(message, dict):
316
+ projected.append(message)
317
+ continue
318
+ copied = dict(message)
319
+ if str(copied.get("role") or "") in {"system", "developer"}:
320
+ if "content" in copied:
321
+ copied["content"], found = _without_pointer_from_content(
322
+ copied.get("content")
323
+ )
324
+ elif isinstance(copied.get("text"), str):
325
+ copied["text"], found = _without_pointer_from_content(copied["text"])
326
+ else:
327
+ found = ""
328
+ if found:
329
+ pointer = found
330
+ privileged.append(len(projected))
331
+ projected.append(copied)
332
+ return projected, pointer, privileged
333
+
334
+
335
+ def _anthropic_system_with_pointer_last(system: Any, prompt: str) -> Any:
336
+ cleaned, _pointer = _without_pointer_from_content(system)
337
+ return append_anthropic_system_texts(cleaned, [prompt] if prompt else [])
338
+
339
+
340
+ def move_memory_pointer_to_system_end(
341
+ messages: list[dict[str, Any]],
342
+ fallback_pointer: str | None = None,
343
+ ) -> list[dict[str, Any]]:
344
+ """Keep the managed pointer at the tail of the final wire system text.
345
+
346
+ ``fallback_pointer`` restores the trusted pointer when an intermediate
347
+ projection truncated the tail of a long system prompt.
348
+ """
349
+
350
+ fallback_match = _MEMORY_POINTER_PATTERN.search(fallback_pointer or "")
351
+ pointer = fallback_match.group(0) if fallback_match else ""
352
+ projected, existing_pointer, system_indexes = _messages_without_memory_pointer(
353
+ messages
354
+ )
355
+ if not pointer and fallback_pointer is None:
356
+ pointer = existing_pointer
357
+ if not pointer:
358
+ return projected
359
+ if not system_indexes:
360
+ return [{"role": "system", "content": pointer}, *projected]
361
+ target = system_indexes[-1]
362
+ projected[target]["content"] = _append_pointer_to_content(
363
+ projected[target].get("content"),
364
+ pointer,
365
+ )
366
+ return projected
367
+
368
+
369
+ def _current_memory_addresses(
370
+ state_dir: Path,
371
+ config: Mapping[str, Any],
372
+ workspace: Path | None = None,
373
+ ) -> tuple[str, str]:
374
+ """Return verified workspace-relative root and index paths."""
375
+
376
+ if not bool(settings(config).get("enabled", False)):
377
+ return "", ""
378
+ try:
379
+ state = json.loads((state_dir / "remote-memory.json").read_text(encoding="utf-8"))
380
+ except (FileNotFoundError, OSError, UnicodeError, json.JSONDecodeError):
381
+ return "", ""
382
+ if not isinstance(state, dict):
383
+ return "", ""
384
+ try:
385
+ raw_workspace = str(state.get("workspace") or "").strip()
386
+ if not raw_workspace:
387
+ return "", ""
388
+ stored_workspace = Path(raw_workspace).resolve()
389
+ root_relative = _safe_relative_path(state.get("root"), field="root")
390
+ index_relative = _safe_relative_path(state.get("index"), field="index")
391
+ except (OSError, ValueError):
392
+ return "", ""
393
+ if workspace is not None and stored_workspace != workspace.resolve():
394
+ return "", ""
395
+ root = stored_workspace.joinpath(*root_relative.parts).resolve()
396
+ if stored_workspace not in root.parents:
397
+ return "", ""
398
+ path = root.joinpath(*index_relative.parts).resolve()
399
+ if root not in path.parents:
400
+ return "", ""
401
+ if not root.is_dir() or not path.is_file():
402
+ return "", ""
403
+ return root_relative.as_posix(), root_relative.joinpath(index_relative).as_posix()
404
+
405
+
406
+ def current_memory_root_address(
407
+ state_dir: Path,
408
+ config: Mapping[str, Any],
409
+ workspace: Path | None = None,
410
+ ) -> str:
411
+ """Return the verified workspace-relative memory root for prompt injection."""
412
+
413
+ root_address, _index_address = _current_memory_addresses(
414
+ state_dir,
415
+ config,
416
+ workspace,
417
+ )
418
+ return root_address
419
+
420
+
421
+ def current_memory_index_address(
422
+ state_dir: Path,
423
+ config: Mapping[str, Any],
424
+ workspace: Path | None = None,
425
+ ) -> str:
426
+ """Return a verified workspace-relative index path for prompt injection."""
427
+
428
+ _root_address, index_address = _current_memory_addresses(
429
+ state_dir,
430
+ config,
431
+ workspace,
432
+ )
433
+ return index_address
434
+
435
+
436
+ def current_memory_prompt(
437
+ state_dir: Path,
438
+ config: Mapping[str, Any],
439
+ workspace: Path | None = None,
440
+ ) -> str:
441
+ root_address, index_address = _current_memory_addresses(
442
+ state_dir,
443
+ config,
444
+ workspace,
445
+ )
446
+ return (
447
+ _managed_pointer_block(index_address, root_address)
448
+ if root_address and index_address
449
+ else ""
450
+ )
451
+
452
+
453
+ def inject_current_memory_prompt(
454
+ body: dict[str, Any],
455
+ protocol: MessageProtocol,
456
+ state_dir: Path,
457
+ config: Mapping[str, Any],
458
+ workspace: Path | None = None,
459
+ ) -> dict[str, Any]:
460
+ prompt = current_memory_prompt(state_dir, config, workspace)
461
+ if protocol == "anthropic_messages":
462
+ projected = dict(body)
463
+ if "system" in body or prompt:
464
+ projected["system"] = _anthropic_system_with_pointer_last(
465
+ body.get("system"),
466
+ prompt,
467
+ )
468
+ messages, _pointer, _privileged = _messages_without_memory_pointer(
469
+ body.get("messages")
470
+ )
471
+ if isinstance(body.get("messages"), list):
472
+ projected["messages"] = messages
473
+ return body if projected == body else projected
474
+ if protocol == "openai_responses":
475
+ projected = dict(body)
476
+ instructions = body.get("instructions")
477
+ current = (
478
+ without_memory_pointer(instructions)
479
+ if isinstance(instructions, str)
480
+ else instructions
481
+ )
482
+ if current or prompt or "instructions" in body:
483
+ projected["instructions"] = (
484
+ f"{current}\n\n{prompt}" if current and prompt else current or prompt
485
+ )
486
+ inputs, _pointer, _privileged = _messages_without_memory_pointer(
487
+ body.get("input")
488
+ )
489
+ if isinstance(body.get("input"), list):
490
+ projected["input"] = inputs
491
+ elif isinstance(body.get("input"), dict):
492
+ projected["input"] = _messages_without_memory_pointer(
493
+ [body["input"]]
494
+ )[0][0]
495
+ return body if projected == body else projected
496
+ if protocol in {"openai_chat", "ollama_chat"}:
497
+ messages = body.get("messages")
498
+ projected = dict(body)
499
+ projected["messages"] = move_memory_pointer_to_system_end(
500
+ messages if isinstance(messages, list) else [],
501
+ prompt,
502
+ )
503
+ return body if projected == body else projected
504
+ if protocol == "google_generative":
505
+ projected = dict(body)
506
+ key = "system_instruction" if "system_instruction" in body else "systemInstruction"
507
+ current = body.get(key)
508
+ if isinstance(current, dict):
509
+ system_instruction = dict(current)
510
+ parts, _pointer = _without_pointer_from_content(current.get("parts"))
511
+ system_instruction["parts"] = parts
512
+ projected[key] = system_instruction
513
+ elif isinstance(current, str):
514
+ projected[key] = without_memory_pointer(current)
515
+ if prompt:
516
+ projected = PromptInjector().inject(projected, protocol, [prompt])
517
+ return body if projected == body else projected
518
+ return PromptInjector().inject(body, protocol, [prompt]) if prompt else body
519
+
520
+
521
+ def update_memory_pointer(
522
+ path: Path,
523
+ index_address: str = "",
524
+ root_address: str = "",
525
+ ) -> bool:
206
526
  """Replace Ciel's memory pointer at the bottom without changing user text."""
207
527
 
208
528
  try:
209
529
  current = path.read_text(encoding="utf-8")
210
530
  except FileNotFoundError:
211
531
  current = ""
212
- start = current.find(MEMORY_POINTER_BEGIN)
213
- end = (
214
- current.find(MEMORY_POINTER_END, start + len(MEMORY_POINTER_BEGIN))
215
- if start >= 0
216
- else -1
217
- )
218
- if not index_address and (start < 0 or end < 0):
532
+ if not index_address and not _MEMORY_POINTER_PATTERN.search(current):
219
533
  return False
220
- if start >= 0 and end >= 0:
221
- current = (
222
- current[:start] + current[end + len(MEMORY_POINTER_END) :]
223
- ).strip()
534
+ current = without_memory_pointer(current)
224
535
  next_text = current
225
536
  if index_address:
226
- block = _managed_pointer_block(index_address)
537
+ block = _managed_pointer_block(index_address, root_address)
227
538
  next_text = f"{current}\n\n{block}".strip() if current else block
228
539
  if next_text:
229
540
  next_text += "\n"
@@ -244,10 +555,72 @@ def project_memory_pointer(
244
555
  workspace: Path,
245
556
  runtime: str,
246
557
  index_address: str = "",
558
+ root_address: str = "",
247
559
  ) -> bool:
248
560
  if runtime not in RUNTIME_FILES:
249
561
  raise ValueError(f"unsupported instruction runtime: {runtime}")
250
- return update_memory_pointer(target_file(workspace, runtime), index_address)
562
+ return update_memory_pointer(
563
+ target_file(workspace, runtime),
564
+ index_address,
565
+ root_address,
566
+ )
567
+
568
+
569
+ def _can_project_native_pointer(
570
+ config: Mapping[str, Any],
571
+ state_dir: Path,
572
+ workspace: Path,
573
+ runtime: str,
574
+ ) -> bool:
575
+ instruction = instruction_settings(dict(config))
576
+ configured = configured_instruction_url(dict(config), runtime)
577
+ target = target_file(workspace, runtime)
578
+ if not bool(instruction.get("enabled", False)) or not configured or not target.is_file():
579
+ return False
580
+ safe_runtime = runtime.replace("-", "_")
581
+ try:
582
+ state = json.loads(
583
+ (state_dir / f"remote-instructions-{safe_runtime}.json").read_text(
584
+ encoding="utf-8"
585
+ )
586
+ )
587
+ except (FileNotFoundError, OSError, UnicodeError, json.JSONDecodeError):
588
+ return False
589
+ if (
590
+ not isinstance(state, dict)
591
+ or str(state.get("url") or "") != configured
592
+ or str(state.get("target") or "") != target.name
593
+ ):
594
+ return False
595
+ try:
596
+ raw = target.read_bytes()
597
+ except OSError:
598
+ return False
599
+ downloaded_digest = str(state.get("sha256") or "")
600
+ if downloaded_digest and hashlib.sha256(raw).hexdigest() == downloaded_digest:
601
+ return True
602
+ normalized_digest = str(state.get("normalized_sha256") or "")
603
+ if not normalized_digest:
604
+ return False
605
+ try:
606
+ managed_content = without_memory_pointer(raw.decode("utf-8"))
607
+ except UnicodeError:
608
+ return False
609
+ return normalized_instruction_sha256(managed_content) == normalized_digest
610
+
611
+
612
+ def _shared_target_has_projectable_pointer(
613
+ config: Mapping[str, Any],
614
+ state_dir: Path,
615
+ workspace: Path,
616
+ runtime: str,
617
+ ) -> bool:
618
+ filename = RUNTIME_FILES[runtime]
619
+ return any(
620
+ _can_project_native_pointer(config, state_dir, workspace, candidate)
621
+ for candidate, candidate_filename in RUNTIME_FILES.items()
622
+ if candidate_filename == filename
623
+ )
251
624
 
252
625
 
253
626
  @dataclass(frozen=True, slots=True)
@@ -264,7 +637,7 @@ class RemoteMemorySynchronizer:
264
637
  manifest_url = str(remote.get("manifest_url") or "").strip()
265
638
  workspace = self.workspace().resolve()
266
639
  if not bool(remote.get("enabled", False)):
267
- project_memory_pointer(workspace, runtime, "")
640
+ self._remove_legacy_pointer(runtime, workspace)
268
641
  return RemoteMemoryResult(manifest_url, None, None, "", "disabled")
269
642
  try:
270
643
  manifest_url = _http_url(
@@ -323,6 +696,22 @@ class RemoteMemorySynchronizer:
323
696
  manifest_url=manifest_url,
324
697
  max_files=max_files,
325
698
  )
699
+ def commit_state(index_address: str) -> None:
700
+ self._write_state(
701
+ {
702
+ "manifest_url": manifest_url,
703
+ "workspace": str(workspace),
704
+ "root": root.relative_to(workspace).as_posix(),
705
+ "index": manifest.index.as_posix(),
706
+ "index_address": index_address,
707
+ "files": [
708
+ {"path": item.path.as_posix(), "format": item.format}
709
+ for item in manifest.files
710
+ ],
711
+ "reason": reason,
712
+ }
713
+ )
714
+
326
715
  index_address = self._replace_tree(
327
716
  root,
328
717
  manifest,
@@ -331,22 +720,9 @@ class RemoteMemorySynchronizer:
331
720
  file_limit=file_limit,
332
721
  total_limit=total_limit,
333
722
  authorization=authorization,
723
+ commit=commit_state,
334
724
  )
335
725
  index_path = root.joinpath(*manifest.index.parts)
336
- project_memory_pointer(workspace, runtime, index_address)
337
- self._write_state(
338
- {
339
- "manifest_url": manifest_url,
340
- "root": root.relative_to(workspace).as_posix(),
341
- "index": manifest.index.as_posix(),
342
- "index_address": index_address,
343
- "files": [
344
- {"path": item.path.as_posix(), "format": item.format}
345
- for item in manifest.files
346
- ],
347
- "reason": reason,
348
- }
349
- )
350
726
  except (
351
727
  json.JSONDecodeError,
352
728
  OSError,
@@ -359,6 +735,32 @@ class RemoteMemorySynchronizer:
359
735
  manifest_url,
360
736
  f"{type(exc).__name__}: {exc}",
361
737
  )
738
+ projection_detail = ""
739
+ try:
740
+ if _shared_target_has_projectable_pointer(
741
+ config,
742
+ self.state_dir,
743
+ workspace,
744
+ runtime,
745
+ ):
746
+ project_memory_pointer(
747
+ workspace,
748
+ runtime,
749
+ index_path.relative_to(workspace).as_posix(),
750
+ root.relative_to(workspace).as_posix(),
751
+ )
752
+ else:
753
+ project_memory_pointer(workspace, runtime, "")
754
+ except (OSError, UnicodeError, ValueError) as exc:
755
+ projection_detail = (
756
+ "memory pointer projection failed: "
757
+ f"{type(exc).__name__}: {exc}"
758
+ )
759
+ self.log(
760
+ "WARN",
761
+ f"remote_memory_pointer_failed runtime={runtime} "
762
+ f"error={type(exc).__name__}: {exc}",
763
+ )
362
764
  self.log(
363
765
  "INFO",
364
766
  f"remote_memory_updated runtime={runtime} files={len(manifest.files)} "
@@ -371,27 +773,56 @@ class RemoteMemorySynchronizer:
371
773
  index_address,
372
774
  "updated",
373
775
  len(manifest.files),
776
+ projection_detail,
374
777
  )
375
778
 
376
779
  def current_index_address(self) -> str:
377
- try:
378
- state = json.loads(self._state_path().read_text(encoding="utf-8"))
379
- except (FileNotFoundError, OSError, json.JSONDecodeError):
380
- return ""
381
- if not isinstance(state, dict):
382
- return ""
383
- address = str(state.get("index_address") or "")
384
- try:
385
- relative = _safe_relative_path(address, field="index_address")
386
- except ValueError:
387
- return ""
388
- path = self.workspace().resolve().joinpath(*relative.parts)
389
- return relative.as_posix() if path.is_file() else ""
780
+ return current_memory_index_address(
781
+ self.state_dir,
782
+ self.load_config(),
783
+ self.workspace().resolve(),
784
+ )
785
+
786
+ def current_prompt_text(self) -> str:
787
+ return current_memory_prompt(
788
+ self.state_dir,
789
+ self.load_config(),
790
+ self.workspace().resolve(),
791
+ )
390
792
 
391
793
  def project_current_pointer(self, runtime: str) -> bool:
392
- remote = settings(self.load_config())
393
- address = self.current_index_address() if remote.get("enabled") else ""
394
- return project_memory_pointer(self.workspace().resolve(), runtime, address)
794
+ """Project the verified current memory location into native instructions."""
795
+
796
+ config = self.load_config()
797
+ workspace = self.workspace().resolve()
798
+ if not _shared_target_has_projectable_pointer(
799
+ config,
800
+ self.state_dir,
801
+ workspace,
802
+ runtime,
803
+ ):
804
+ return project_memory_pointer(workspace, runtime, "")
805
+ root_address, index_address = _current_memory_addresses(
806
+ self.state_dir,
807
+ config,
808
+ workspace,
809
+ )
810
+ return project_memory_pointer(
811
+ workspace,
812
+ runtime,
813
+ index_address,
814
+ root_address,
815
+ )
816
+
817
+ def _remove_legacy_pointer(self, runtime: str, workspace: Path) -> None:
818
+ try:
819
+ project_memory_pointer(workspace, runtime, "")
820
+ except (OSError, UnicodeError, ValueError) as exc:
821
+ self.log(
822
+ "WARN",
823
+ f"remote_memory_legacy_pointer_cleanup_failed runtime={runtime} "
824
+ f"error={type(exc).__name__}: {exc}",
825
+ )
395
826
 
396
827
  def _download(
397
828
  self,
@@ -426,6 +857,7 @@ class RemoteMemorySynchronizer:
426
857
  file_limit: int,
427
858
  total_limit: int,
428
859
  authorization: str,
860
+ commit: Callable[[str], None],
429
861
  ) -> str:
430
862
  parent = root.parent
431
863
  parent.mkdir(parents=True, exist_ok=True)
@@ -468,13 +900,27 @@ class RemoteMemorySynchronizer:
468
900
  if moved_previous and backup.exists() and not root.exists():
469
901
  os.replace(backup, root)
470
902
  raise
903
+ index_address = str(root.joinpath(*manifest.index.parts).resolve())
904
+ try:
905
+ commit(index_address)
906
+ except Exception:
907
+ os.replace(root, staging)
908
+ if moved_previous and backup.exists():
909
+ os.replace(backup, root)
910
+ raise
471
911
  if backup.exists():
472
- shutil.rmtree(backup)
912
+ try:
913
+ shutil.rmtree(backup)
914
+ except OSError as exc:
915
+ self.log(
916
+ "WARN",
917
+ "remote_memory_backup_cleanup_failed "
918
+ f"path={backup} error={type(exc).__name__}: {exc}",
919
+ )
473
920
  finally:
474
921
  if staging.exists():
475
922
  shutil.rmtree(staging)
476
- workspace = self.workspace().resolve()
477
- return root.joinpath(*manifest.index.parts).relative_to(workspace).as_posix()
923
+ return index_address
478
924
 
479
925
  def _state_path(self) -> Path:
480
926
  return self.state_dir / "remote-memory.json"
@@ -514,12 +960,12 @@ def sync_instruction_with_memory_pointer(
514
960
  memory_synchronizer: Callable[[], RemoteMemorySynchronizer],
515
961
  log: Callable[[str, str], Any],
516
962
  ) -> Any:
517
- """Refresh native instructions and restore the managed memory pointer."""
963
+ """Refresh native instructions and restore the verified memory pointer."""
518
964
 
519
965
  result = instruction_synchronizer().sync(runtime, reason=reason)
520
966
  try:
521
967
  memory_synchronizer().project_current_pointer(runtime)
522
- except (OSError, ValueError) as exc:
968
+ except (OSError, UnicodeError, ValueError) as exc:
523
969
  log(
524
970
  "WARN",
525
971
  f"remote_memory_pointer_failed runtime={runtime} "
@@ -544,11 +990,18 @@ def sync_launch_assets(
544
990
  def sync_all_memory_pointers(
545
991
  synchronizer: RemoteMemorySynchronizer,
546
992
  ) -> list[str]:
547
- """Download once and project the resulting index into every native file."""
993
+ """Download once and project the verified pointer for every runtime."""
548
994
 
549
995
  result = synchronizer.sync("codex", reason="manual")
550
- for runtime in ("claude", "agy", "kimi", "grok"):
551
- synchronizer.project_current_pointer(runtime)
996
+ for runtime in ("codex-app-server", "claude", "agy", "kimi", "grok"):
997
+ try:
998
+ synchronizer.project_current_pointer(runtime)
999
+ except (OSError, UnicodeError, ValueError) as exc:
1000
+ synchronizer.log(
1001
+ "WARN",
1002
+ f"remote_memory_pointer_failed runtime={runtime} "
1003
+ f"error={type(exc).__name__}: {exc}",
1004
+ )
552
1005
  if result.status == "disabled":
553
1006
  return ["Remote memory is disabled."]
554
1007
  detail = (
@@ -564,11 +1017,17 @@ __all__ = [
564
1017
  "DEFAULT_DIRECTORY",
565
1018
  "MEMORY_POINTER_BEGIN",
566
1019
  "MEMORY_POINTER_END",
1020
+ "MEMORY_REFERENCE_INSTRUCTION",
567
1021
  "RemoteMemoryFile",
568
1022
  "RemoteMemoryManifest",
569
1023
  "RemoteMemoryResult",
570
1024
  "RemoteMemorySynchronizer",
1025
+ "current_memory_index_address",
1026
+ "current_memory_prompt",
1027
+ "current_memory_root_address",
1028
+ "inject_current_memory_prompt",
571
1029
  "memory_directory",
1030
+ "move_memory_pointer_to_system_end",
572
1031
  "parse_manifest",
573
1032
  "project_memory_pointer",
574
1033
  "settings",
@@ -576,4 +1035,5 @@ __all__ = [
576
1035
  "sync_instruction_with_memory_pointer",
577
1036
  "sync_launch_assets",
578
1037
  "update_memory_pointer",
1038
+ "without_memory_pointer",
579
1039
  ]