@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.
- package/CHANGELOG.md +108 -0
- package/README.md +150 -57
- package/ciel-runtime-stop.cmd +1 -0
- package/ciel-runtime.cmd +1 -0
- package/ciel-runtimectl.cmd +1 -0
- package/ciel_runtime.py +56 -53
- package/ciel_runtime_support/anthropic_tool_turns.py +81 -0
- package/ciel_runtime_support/architecture.py +17 -0
- package/ciel_runtime_support/channel_injection.py +113 -3
- package/ciel_runtime_support/channel_message_prompt.py +15 -0
- package/ciel_runtime_support/channel_pending_injection.py +28 -4
- package/ciel_runtime_support/channel_pending_poll.py +2 -0
- package/ciel_runtime_support/channel_terminal_context.py +6 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +2 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +8 -2
- package/ciel_runtime_support/channel_wake_claim_repository.py +7 -0
- package/ciel_runtime_support/channel_wake_context.py +7 -3
- package/ciel_runtime_support/cli_parser.py +6 -0
- package/ciel_runtime_support/codex_backend_context.py +4 -0
- package/ciel_runtime_support/codex_turn_recovery.py +75 -12
- package/ciel_runtime_support/config_migrations.py +102 -10
- package/ciel_runtime_support/config_repository.py +9 -0
- package/ciel_runtime_support/event_settings_cli.py +284 -2
- package/ciel_runtime_support/openai_chat_passthrough.py +2 -0
- package/ciel_runtime_support/protocols/openai_responses.py +22 -18
- package/ciel_runtime_support/provider_adapters.py +7 -0
- package/ciel_runtime_support/provider_policy.py +10 -1
- package/ciel_runtime_support/provider_request_builder.py +9 -2
- package/ciel_runtime_support/provider_responses_passthrough.py +4 -0
- package/ciel_runtime_support/providers/__init__.py +2 -0
- package/ciel_runtime_support/providers/alibaba.py +33 -10
- package/ciel_runtime_support/providers/constants.py +1 -0
- package/ciel_runtime_support/providers/opencode.py +25 -5
- package/ciel_runtime_support/providers/opencode_go.py +3 -3
- package/ciel_runtime_support/providers/openrouter.py +76 -3
- package/ciel_runtime_support/providers/tabitoken.py +130 -0
- package/ciel_runtime_support/remote_instructions.py +20 -6
- package/ciel_runtime_support/remote_memory.py +515 -55
- package/ciel_runtime_support/responses_input_compatibility.py +14 -0
- package/ciel_runtime_support/router_http.py +5 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +9 -2
- package/ciel_runtime_support/runtime_paths.py +2 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +7 -4
- package/ciel_runtime_support/transcript_delta_delivery.py +333 -0
- package/ciel_runtime_support/usage_events.py +45 -3
- package/ciel_runtime_support/usage_service.py +871 -0
- package/ciel_runtime_support/windows_conpty.py +108 -2
- package/ciel_runtime_support/workspace_mcp.py +4 -0
- package/docs/CLI-Reference.md +11 -0
- package/docs/Configuration.md +7 -2
- package/docs/MCP-Channels.md +7 -3
- package/docs/Module-Map.md +4 -0
- package/docs/Providers.md +32 -0
- package/docs/Remote-Memory.md +29 -10
- package/docs/Test-Suite.md +1 -0
- package/docs/journal/2026/08/20/kevin-wake-orphan-tool-repair.md +161 -0
- package/docs/journal/2026/08/20/nightly-push-local-deployment.md +59 -2
- package/docs/journal/2026/08/20/wing-remote-memory-replication-verification.md +107 -0
- package/docs/journal/2026/08/21/alibaba-singapore-qwen38-official-alignment.md +64 -0
- package/docs/journal/2026/08/21/codex-visible-sse-wake-submit-regression.md +85 -0
- package/docs/journal/2026/08/21/external-sse-wake-only-change-analysis.md +131 -0
- package/docs/journal/2026/08/21/hyundai-invalid-replayed-tool-name-repair.okf +88 -0
- package/docs/journal/2026/08/21/local-stale-temporary-runtime-pin-repair.okf +118 -0
- package/docs/journal/2026/08/21/transcript-compaction-external-events.okf +131 -0
- package/docs/journal/2026/08/21/wing-ai-net-mcp-boundary-diagnosis.okf +129 -0
- package/docs/journal/2026/08/21/wing-cold-start-wake-submit-repair.okf +161 -0
- package/docs/journal/2026/08/21/wing-powershell-memory-runaway.okf +101 -0
- package/docs/journal/2026/08/21/wing-repeated-reasoning-tool-turn-repair.okf +112 -0
- package/docs/journal/2026/08/21/wing-repeated-tool-guard-finalization.okf +81 -0
- package/docs/journal/2026/08/21/wing-working-directory-memory-deployment.md +110 -0
- package/docs/journal/2026/08/21/wing-workspace-state-memory-prompt-injection.md +88 -0
- package/docs/journal/2026/08/22/alibaba-singapore-dual-endpoint-wire-alignment.okf +142 -0
- package/docs/journal/2026/08/22/delluhiold-claude-compaction-empty-tool-name.okf +168 -0
- package/docs/journal/2026/08/22/remote-memory-relative-prompt-paths.okf +79 -0
- package/docs/journal/2026/08/22/remote-memory-root-system-prompt.okf +121 -0
- package/docs/journal/2026/08/23/claude-web-chat-stateless-mcp-repair.okf +97 -0
- package/docs/journal/2026/08/23/cross-runtime-token-usage-interfaces-research.okf +378 -0
- package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +81 -0
- package/docs/journal/2026/08/23/mia-openrouter-ox-alpha-latency-diagnosis.okf +146 -0
- package/docs/journal/2026/08/23/openrouter-opencode-ox-alpha-support.okf +114 -0
- package/docs/journal/2026/08/23/tabitoken-provider-model-catalog.okf +97 -0
- package/docs/journal/2026/08/23/usage-stream-snapshot-implementation.okf +134 -0
- package/docs/usage-observability.md +141 -0
- package/install.ps1 +24 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
]
|