@asm-agent/coding-agent 0.8.7 → 0.9.0
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 +19 -0
- package/README.md +2 -7
- package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/_winjob.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/harness.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
- package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
- package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
- package/dist/asm-agent-runtime/test/__pycache__/test_agent_message_skill.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_harness.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_mcp.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_mcp_base.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_subagent_registry.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/__pycache__/test_winjob.cpython-313.pyc +0 -0
- package/dist/asm-agent-runtime/test/test_bash.py +74 -19
- package/dist/asm-agent-runtime/test/test_repl.py +67 -3
- package/dist/asm-agent-runtime/uv.lock +446 -155
- package/dist/bundle/{anthropic-XSMQHGYB.js → anthropic-ZOZNPFXS.js} +2 -2
- package/dist/bundle/{azure-openai-responses-XLQAIKKH.js → azure-openai-responses-TDHCU647.js} +2 -2
- package/dist/bundle/{bundled-modules-5OCN73BM.js → bundled-modules-Q2YODC4B.js} +3 -3
- package/dist/bundle/{chunk-IAQLPSMN.js → chunk-AMKLNEAK.js} +47 -47
- package/dist/bundle/{chunk-2LPSALBF.js → chunk-FPY7FFJS.js} +978 -1821
- package/dist/bundle/{chunk-E3YE2Q7A.js → chunk-G6SC4YYW.js} +4973 -1926
- package/dist/bundle/{chunk-YPNK4MRU.js → chunk-QLHAP3FT.js} +1 -1
- package/dist/bundle/{cli-main-QYX5R3LF.js → cli-main-277AG2BR.js} +3 -3
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{google-ZXJEPWJA.js → google-Q3AJWUOY.js} +1 -1
- package/dist/bundle/{google-vertex-WAF3RIJ6.js → google-vertex-2CSNOUBS.js} +1 -1
- package/dist/bundle/{main-EOUAQCJH.js → main-TNXWJXIU.js} +3 -3
- package/dist/bundle/{mistral-GDZXSXRS.js → mistral-XRYJGLLG.js} +1 -1
- package/dist/bundle/{openai-codex-responses-7XMPW4LG.js → openai-codex-responses-C6BRO2BN.js} +2 -2
- package/dist/bundle/{openai-completions-LNSVGDMF.js → openai-completions-DNXCK2JW.js} +4 -4
- package/dist/bundle/{openai-responses-MFZOBMIT.js → openai-responses-BEMWVIXE.js} +2 -2
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -3
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-config.d.ts +1 -2
- package/dist/core/agent-session-config.d.ts.map +1 -1
- package/dist/core/agent-session-config.js +0 -3
- package/dist/core/agent-session-config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +0 -8
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +0 -2
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -22
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +8 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +100 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/kernel/bootstrap.d.ts.map +1 -1
- package/dist/core/kernel/bootstrap.js +1 -1
- package/dist/core/kernel/bootstrap.js.map +1 -1
- package/dist/core/kernel/repl-manager.d.ts +32 -8
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +401 -102
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +11 -7
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js +2 -5
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/mcp/mcp-manager.d.ts +4 -2
- package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
- package/dist/core/mcp/mcp-manager.js +9 -7
- package/dist/core/mcp/mcp-manager.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/ollama-models.d.ts +3 -1
- package/dist/core/ollama-models.d.ts.map +1 -1
- package/dist/core/ollama-models.js +31 -10
- package/dist/core/ollama-models.js.map +1 -1
- package/dist/core/prime-inference-auth.d.ts +0 -9
- package/dist/core/prime-inference-auth.d.ts.map +1 -1
- package/dist/core/prime-inference-auth.js +0 -50
- package/dist/core/prime-inference-auth.js.map +1 -1
- package/dist/core/provider-display-names.d.ts.map +1 -1
- package/dist/core/provider-display-names.js +0 -1
- package/dist/core/provider-display-names.js.map +1 -1
- package/dist/core/session-manager.d.ts +0 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +0 -22
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -16
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -43
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -5
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/acp-mcp.d.ts +6 -0
- package/dist/core/tools/acp-mcp.d.ts.map +1 -0
- package/dist/core/tools/acp-mcp.js +86 -0
- package/dist/core/tools/acp-mcp.js.map +1 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ipython.d.ts.map +1 -1
- package/dist/core/tools/ipython.js +9 -4
- package/dist/core/tools/ipython.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -11
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/acp-mode.d.ts.map +1 -1
- package/dist/modes/acp/acp-mode.js +1 -1
- package/dist/modes/acp/acp-mode.js.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
- package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
- package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
- package/dist/modes/agents-view/agents-view-mode.js +0 -19
- package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts +1 -10
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +134 -274
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
- package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-protocol.js +6 -8
- package/dist/modes/daemon/daemon-protocol.js.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
- package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-supervisor.js +18 -52
- package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
- package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
- package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
- package/dist/modes/interactive/auth-flows.d.ts +0 -2
- package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
- package/dist/modes/interactive/auth-flows.js +1 -101
- package/dist/modes/interactive/auth-flows.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +13 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/mermaid.d.ts +12 -0
- package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
- package/dist/modes/interactive/components/mermaid.js +63 -0
- package/dist/modes/interactive/components/mermaid.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
- package/dist/modes/interactive/feature-hints.js +0 -4
- package/dist/modes/interactive/feature-hints.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -7
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +23 -279
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +1 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/settings.md +1 -31
- package/docs/usage.md +1 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
- package/dist/core/agent-traces.d.ts +0 -130
- package/dist/core/agent-traces.d.ts.map +0 -1
- package/dist/core/agent-traces.js +0 -746
- package/dist/core/agent-traces.js.map +0 -1
- package/dist/core/telemetry.d.ts +0 -92
- package/dist/core/telemetry.d.ts.map +0 -1
- package/dist/core/telemetry.js +0 -633
- package/dist/core/telemetry.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0] - 2026-09-03
|
|
4
|
+
|
|
5
|
+
- Added native callable tools for MCP servers supplied by ACP clients. ([#2002](https://github.com/PrimeIntellect-ai/prime-agent/pull/2002))
|
|
6
|
+
- Fixed `bash()` to capture all foreground command output before finalizing results by using an ordered per-command completion marker; output written after the marker (e.g. by `EXIT` traps or background jobs) is not in the awaited result but stays visible via `handle.output()`/`tail()`.
|
|
7
|
+
Agent messages now use core session admission to choose immediate or queued delivery.
|
|
8
|
+
- Fixed concurrent `execute_bash_and_wait` commands sharing one bash abort controller: each `executeBash` invocation now gets its own controller, so a finishing command no longer clears a still-running command's abort state and `abortBash` cancels every in-flight command.
|
|
9
|
+
- Namespaced kernel host handler results so handler fields cannot overwrite host reply protocol metadata.
|
|
10
|
+
- Render Mermaid code blocks in assistant messages as inline Unicode diagrams, with a "Mermaid diagrams" setting (off/final/streaming, default streaming).
|
|
11
|
+
- Fixed graceful Python kernel disposal so timed-out final snapshots are cancelled before teardown.
|
|
12
|
+
- Reduced kernel memory spikes during namespace snapshots: the payload now pickles straight into the staged file instead of building serialized copies in memory (peak snapshot overhead ~3.9x payload -> ~1x; ENG-5819).
|
|
13
|
+
- Made cross-worker agent lists current without broadcasting duplicate peer rosters.
|
|
14
|
+
- Fixed invalid kernel protocol frames hanging requests by rejecting the affected request and replacing the kernel from its latest state snapshot.
|
|
15
|
+
- Fixed remote agent messages being delivered twice when the daemon request timed out or the response was lost: the message is now sent exactly once per call, and post-send failures surface as errors instead of triggering a resend.
|
|
16
|
+
- Fixed kernel teardown so session cleanup and signal handling share one bounded graceful shutdown path.
|
|
17
|
+
|
|
18
|
+
## [0.8.8] - 2026-09-03
|
|
19
|
+
|
|
20
|
+
- Fixed selected Ollama models failing runtime authentication before requests were sent.
|
|
21
|
+
|
|
3
22
|
## [0.8.7] - 2026-09-03
|
|
4
23
|
|
|
5
24
|
- Added automatic Ollama model discovery and selection from the `/model` menu.
|
package/README.md
CHANGED
|
@@ -154,7 +154,6 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist
|
|
|
154
154
|
| `/new`, `/clear` | Start a new session |
|
|
155
155
|
| `/name <name>` | Set session display name |
|
|
156
156
|
| `/session` | Show session info (file, ID, messages) |
|
|
157
|
-
| `/traces [status\|on\|off\|preview\|upload-current\|upload-all\|login]` | Preview traces, run one-shot current/all uploads, and manage automatic sharing (`upload` aliases `upload-current`) |
|
|
158
157
|
| `/usage` | Show token, cost, and context usage |
|
|
159
158
|
| `/tree` | Jump to any point in the session and continue from there |
|
|
160
159
|
| `/fork` | Create a new session from a previous user message |
|
|
@@ -258,7 +257,7 @@ See [docs/settings.md](docs/settings.md) for all options.
|
|
|
258
257
|
|
|
259
258
|
### Update checks
|
|
260
259
|
|
|
261
|
-
ASM Agent stable builds fetch `https://pub-
|
|
260
|
+
ASM Agent stable builds fetch `https://pub-ca377e94504d4b9cb997babbbc998159.r2.dev/latest.json` to check whether a newer version exists. Beta builds fetch `beta.json` and remain on the beta channel. Override the base URL with `ASM_AGENT_DOWNLOAD_BASE_URL`. Disable version checks with `PI_SKIP_VERSION_CHECK=1`.
|
|
262
261
|
|
|
263
262
|
Use `--offline` or `PI_OFFLINE=1` to disable startup network operations, including update checks and package update checks.
|
|
264
263
|
|
|
@@ -676,14 +675,10 @@ asm-agent --thinking high "Solve this complex problem"
|
|
|
676
675
|
| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) |
|
|
677
676
|
| `PI_OFFLINE` | Disable startup network operations, including update checks and package update checks |
|
|
678
677
|
| `PI_SKIP_VERSION_CHECK` | Skip the ASM Agent version update check at startup. This prevents the release manifest request |
|
|
679
|
-
| `ASM_AGENT_TELEMETRY` | Override pseudonymous aggregate usage analytics with `1`/`true`/`yes` or `0`/`false`/`no` |
|
|
680
|
-
| `ASM_AGENT_TELEMETRY_ENDPOINT` | Override the aggregate analytics ingestion endpoint |
|
|
681
678
|
| `DO_NOT_TRACK` | Disable aggregate usage analytics when set to `1`/`true`/`yes` |
|
|
682
679
|
| `ASM_AGENT_DOWNLOAD_BASE_URL` | Override the ASM Agent release manifest and tarball base URL |
|
|
683
680
|
| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) |
|
|
684
|
-
| `PRIME_API_KEY` | Prime Inference API key
|
|
685
|
-
| `ASM_AGENT_TRACES_API_KEY` | Prime API key used only for opt-in trace sharing |
|
|
686
|
-
| `ASM_AGENT_TRACES_BASE_URL` | Override the ASM Agent trace upload API base URL |
|
|
681
|
+
| `PRIME_API_KEY` | Prime Inference API key |
|
|
687
682
|
| `ASM_AGENT_KERNEL_PYTHON` | Use an existing Python environment with `asm-agent-runtime` instead of auto-bootstrapping `~/.asm-agent/kernel-venv` |
|
|
688
683
|
| `VISUAL`, `EDITOR` | External editor for Ctrl+G |
|
|
689
684
|
|
|
Binary file
|
|
@@ -57,7 +57,7 @@ def _spawn_handle_from_payload(payload: Any) -> RLMSpawnHandle:
|
|
|
57
57
|
def _parse_host_reply(request_type: str, reply: dict[str, Any]) -> dict[str, Any]:
|
|
58
58
|
status = reply.get("status")
|
|
59
59
|
if status == "ok":
|
|
60
|
-
return
|
|
60
|
+
return reply["result"]
|
|
61
61
|
if status == "error":
|
|
62
62
|
raise RuntimeError(str(reply.get("error") or f"host request {request_type} failed"))
|
|
63
63
|
raise RuntimeError(f"host request {request_type} returned unexpected status: {status!r}")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
import atexit
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import secrets
|
|
9
10
|
import selectors
|
|
10
11
|
import shutil
|
|
11
12
|
import signal
|
|
@@ -36,6 +37,9 @@ _READ_CHUNK = 65536
|
|
|
36
37
|
# Fixed child-side fd for the status channel; POSIX shells (notably dash) only
|
|
37
38
|
# guarantee single-digit fds in redirection syntax.
|
|
38
39
|
_STATUS_FD = 9
|
|
40
|
+
_OUTPUT_FD = 8
|
|
41
|
+
_COMPLETION_PREFIX = b"\x1easm-agent-complete:"
|
|
42
|
+
_COMPLETION_SUFFIX = b"\x1f"
|
|
39
43
|
# Cancelled one-shot awaits: TERM grace before the group KILL, then the bounded
|
|
40
44
|
# wait for a confirmed group exit before CancelledError propagates.
|
|
41
45
|
_CANCEL_TERM_GRACE = 0.5
|
|
@@ -117,6 +121,10 @@ class BashHandle:
|
|
|
117
121
|
self._buffer = _BoundedBuffer()
|
|
118
122
|
self._done = threading.Event()
|
|
119
123
|
self._eof = threading.Event()
|
|
124
|
+
self._completion_terminal = threading.Event()
|
|
125
|
+
self._completion_output: str | None = None
|
|
126
|
+
self._completion_lock = threading.Lock()
|
|
127
|
+
self._completion_pending = b""
|
|
120
128
|
self._status: int | None = None
|
|
121
129
|
self._status_known = threading.Event()
|
|
122
130
|
self._reaped = False
|
|
@@ -134,6 +142,7 @@ class BashHandle:
|
|
|
134
142
|
# True only while the pump moves a chunk from the pipe into the buffer.
|
|
135
143
|
self._pump_transfer = False
|
|
136
144
|
self._job: int | None = None
|
|
145
|
+
self._completion_marker: bytes | None = None
|
|
137
146
|
status_write = -1
|
|
138
147
|
if _IS_POSIX:
|
|
139
148
|
# Full-duplex status channel: the child end rides in as stdin (fd 0)
|
|
@@ -151,9 +160,19 @@ class BashHandle:
|
|
|
151
160
|
os.close(self._status_read)
|
|
152
161
|
os.close(status_write)
|
|
153
162
|
raise
|
|
154
|
-
|
|
163
|
+
completion_token = secrets.token_hex(32)
|
|
164
|
+
# Halves stop passive echoes; a deliberate forgery freezes only this call while later bytes stay live.
|
|
165
|
+
token_midpoint = len(completion_token) // 2
|
|
166
|
+
self._completion_marker = (
|
|
167
|
+
_COMPLETION_PREFIX + completion_token.encode("ascii") + _COMPLETION_SUFFIX
|
|
168
|
+
)
|
|
169
|
+
script = _status_script(
|
|
170
|
+
_with_prefix(command),
|
|
171
|
+
completion_token[:token_midpoint],
|
|
172
|
+
completion_token[token_midpoint:],
|
|
173
|
+
)
|
|
155
174
|
else:
|
|
156
|
-
# Windows
|
|
175
|
+
# Windows lacks a foreground-status channel, so its exit drain stays best-effort.
|
|
157
176
|
script = _with_prefix(command)
|
|
158
177
|
self._job = _winjob.create_job()
|
|
159
178
|
if self._job is None:
|
|
@@ -304,8 +323,6 @@ class BashHandle:
|
|
|
304
323
|
stdout = self._proc.stdout
|
|
305
324
|
assert stdout is not None
|
|
306
325
|
if not _IS_POSIX:
|
|
307
|
-
# Windows: no FIONREAD fence; the drain keeps its quiescence
|
|
308
|
-
# heuristic (best-effort parity).
|
|
309
326
|
try:
|
|
310
327
|
while chunk := stdout.read1(_READ_CHUNK):
|
|
311
328
|
self._buffer.write(chunk)
|
|
@@ -314,9 +331,6 @@ class BashHandle:
|
|
|
314
331
|
stdout.close()
|
|
315
332
|
self._eof.set()
|
|
316
333
|
return
|
|
317
|
-
# POSIX: select-gate the read and flag the read->commit window, so the
|
|
318
|
-
# drain fence never sees "pipe empty + buffer quiescent" while a chunk
|
|
319
|
-
# is in flight between the pipe read and the buffer commit.
|
|
320
334
|
fd = stdout.fileno()
|
|
321
335
|
try:
|
|
322
336
|
with selectors.DefaultSelector() as sel:
|
|
@@ -328,17 +342,54 @@ class BashHandle:
|
|
|
328
342
|
chunk = os.read(fd, _READ_CHUNK)
|
|
329
343
|
if not chunk:
|
|
330
344
|
break
|
|
331
|
-
self.
|
|
345
|
+
self._consume_output(chunk)
|
|
332
346
|
finally:
|
|
333
347
|
self._pump_transfer = False
|
|
334
348
|
except (OSError, ValueError):
|
|
335
349
|
pass
|
|
350
|
+
self._abandon_completion()
|
|
336
351
|
try:
|
|
337
352
|
stdout.close()
|
|
338
353
|
except OSError:
|
|
339
354
|
pass
|
|
340
355
|
self._eof.set()
|
|
341
356
|
|
|
357
|
+
def _consume_output(self, chunk: bytes) -> None:
|
|
358
|
+
marker = self._completion_marker
|
|
359
|
+
assert marker is not None
|
|
360
|
+
with self._completion_lock:
|
|
361
|
+
if self._completion_terminal.is_set():
|
|
362
|
+
self._buffer.write(chunk)
|
|
363
|
+
return
|
|
364
|
+
data = self._completion_pending + chunk
|
|
365
|
+
marker_at = data.find(marker)
|
|
366
|
+
if marker_at >= 0:
|
|
367
|
+
self._buffer.write(data[:marker_at])
|
|
368
|
+
self._completion_pending = b""
|
|
369
|
+
self._completion_output = self._buffer.text()
|
|
370
|
+
self._completion_terminal.set()
|
|
371
|
+
self._buffer.write(data[marker_at + len(marker) :])
|
|
372
|
+
return
|
|
373
|
+
retained = 0
|
|
374
|
+
for size in range(min(len(data), len(marker) - 1), 0, -1):
|
|
375
|
+
if data.endswith(marker[:size]):
|
|
376
|
+
retained = size
|
|
377
|
+
break
|
|
378
|
+
self._buffer.write(data[:-retained] if retained else data)
|
|
379
|
+
self._completion_pending = data[-retained:] if retained else b""
|
|
380
|
+
|
|
381
|
+
def _abandon_completion(self) -> None:
|
|
382
|
+
with self._completion_lock:
|
|
383
|
+
if self._completion_terminal.is_set():
|
|
384
|
+
return
|
|
385
|
+
self._buffer.write(self._completion_pending)
|
|
386
|
+
self._completion_pending = b""
|
|
387
|
+
self._completion_terminal.set()
|
|
388
|
+
|
|
389
|
+
def _wait_for_completion(self) -> str | None:
|
|
390
|
+
self._completion_terminal.wait()
|
|
391
|
+
return self._completion_output
|
|
392
|
+
|
|
342
393
|
def _report(self) -> None:
|
|
343
394
|
# Finalize at foreground completion (status channel), not EOF, so
|
|
344
395
|
# `cmd &` does not hang the await; the shell then `wait`s for its
|
|
@@ -355,8 +406,10 @@ class BashHandle:
|
|
|
355
406
|
# (parsed status, EOF, garbage, exception) must set it.
|
|
356
407
|
self._status_known.set()
|
|
357
408
|
if status is not None:
|
|
358
|
-
self.
|
|
359
|
-
|
|
409
|
+
output = self._wait_for_completion()
|
|
410
|
+
if output is None:
|
|
411
|
+
self._drain_grace()
|
|
412
|
+
self._finalize(status, output)
|
|
360
413
|
|
|
361
414
|
def _watch(self) -> None:
|
|
362
415
|
# Observe shell death independently of the status socket: an early
|
|
@@ -377,6 +430,7 @@ class BashHandle:
|
|
|
377
430
|
with self._callback_lock:
|
|
378
431
|
delivered = self._status
|
|
379
432
|
if delivered is None and not self._done.is_set():
|
|
433
|
+
self._abandon_completion()
|
|
380
434
|
self._drain_grace()
|
|
381
435
|
self._finalize(exit_code)
|
|
382
436
|
with self._kill_lock:
|
|
@@ -439,10 +493,7 @@ class BashHandle:
|
|
|
439
493
|
os.close(self._wake_read)
|
|
440
494
|
|
|
441
495
|
def _drain_grace(self) -> None:
|
|
442
|
-
#
|
|
443
|
-
# EOF arrives immediately without background jobs, otherwise stop once the
|
|
444
|
-
# buffer is quiescent for one tick AND the pipe holds no unread bytes (a
|
|
445
|
-
# slow pump must not lose output the shell wrote before its status).
|
|
496
|
+
# Best-effort fallback when process exit/EOF arrives without a sentinel.
|
|
446
497
|
deadline = time.monotonic() + 0.5
|
|
447
498
|
size = self._buffer.size()
|
|
448
499
|
while time.monotonic() < deadline:
|
|
@@ -472,13 +523,13 @@ class BashHandle:
|
|
|
472
523
|
return False
|
|
473
524
|
return pending > 0
|
|
474
525
|
|
|
475
|
-
def _finalize(self, exit_code: int) -> None:
|
|
526
|
+
def _finalize(self, exit_code: int, output: str | None = None) -> None:
|
|
476
527
|
with self._callback_lock:
|
|
477
528
|
if self._done.is_set():
|
|
478
529
|
return
|
|
479
530
|
self._result = BashResult(
|
|
480
531
|
exit_code=exit_code,
|
|
481
|
-
output=self._buffer.text(),
|
|
532
|
+
output=self._buffer.text() if output is None else output,
|
|
482
533
|
duration=time.monotonic() - self._started,
|
|
483
534
|
)
|
|
484
535
|
self._done.set()
|
|
@@ -641,6 +692,9 @@ def bash(command: str) -> BashHandle:
|
|
|
641
692
|
POSIX; a kill-on-close job object on Windows entered while the child is
|
|
642
693
|
still suspended, so no descendant can escape it and kill()/crash cleanup
|
|
643
694
|
are unconditional -- bash() raises if containment cannot be established.
|
|
695
|
+
Output written after the completion fence (e.g. by an EXIT trap or a
|
|
696
|
+
background job) is not in BashResult.output but stays visible via
|
|
697
|
+
handle.output()/tail().
|
|
644
698
|
"""
|
|
645
699
|
if not isinstance(command, str) or not command:
|
|
646
700
|
raise TypeError("command must be a non-empty str")
|
|
@@ -674,26 +728,35 @@ def _with_prefix(command: str) -> str:
|
|
|
674
728
|
return f"{prefix}\n{command}" if prefix else command
|
|
675
729
|
|
|
676
730
|
|
|
677
|
-
def
|
|
678
|
-
#
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
731
|
+
def _fence_printf() -> str:
|
|
732
|
+
# `\command -p printf` defeats alias expansion but not a user-defined shell
|
|
733
|
+
# function named `command`, which would swallow both fence frames and leave
|
|
734
|
+
# the await hanging until the shell dies (wedged behind background jobs). A
|
|
735
|
+
# slash-qualified command name bypasses function and alias lookup for
|
|
736
|
+
# ordinary command names, so resolve printf on the system default utility PATH.
|
|
737
|
+
path = shutil.which("printf", path=os.confstr("CS_PATH") or os.defpath)
|
|
738
|
+
if path and "'" not in path:
|
|
739
|
+
return f"'{path}'"
|
|
740
|
+
return "\\command -p printf"
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _status_script(command: str, completion_a: str, completion_b: str) -> str:
|
|
744
|
+
# Closed control fds preserve background behavior; supported shells atomically write the frame.
|
|
745
|
+
emit = _fence_printf()
|
|
686
746
|
return (
|
|
687
|
-
f"exec {_STATUS_FD}>&0 0</dev/null\n"
|
|
747
|
+
f"exec {_STATUS_FD}>&0 {_OUTPUT_FD}>&1 0</dev/null\n"
|
|
688
748
|
f"read -r _asm_agent_gate <&{_STATUS_FD} || exit 127\n"
|
|
689
749
|
"{\n"
|
|
690
750
|
f"{command}\n"
|
|
691
|
-
f"}} {_STATUS_FD}>&-\n"
|
|
692
|
-
"
|
|
693
|
-
|
|
694
|
-
f"
|
|
751
|
+
f"}} {_OUTPUT_FD}>&- {_STATUS_FD}>&-\n"
|
|
752
|
+
"__asm_status=$?\n"
|
|
753
|
+
"\\set +x\n"
|
|
754
|
+
f"{emit} '\\036asm-agent-complete:%s%s\\037' "
|
|
755
|
+
f"'{completion_a}' '{completion_b}' >&{_OUTPUT_FD} || exit \"$__asm_status\"\n"
|
|
756
|
+
f"{emit} '%s\\n' \"$__asm_status\" >&{_STATUS_FD}\n"
|
|
757
|
+
f"exec {_OUTPUT_FD}>&- {_STATUS_FD}>&-\n"
|
|
695
758
|
"wait\n"
|
|
696
|
-
'exit "$
|
|
759
|
+
'exit "$__asm_status"\n'
|
|
697
760
|
)
|
|
698
761
|
|
|
699
762
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`python -m rlm.repl` starts a CPython REPL runtime that executes code cells in
|
|
4
4
|
one persistent `__main__` namespace on a single asyncio event loop. The wire
|
|
5
5
|
format is newline-delimited JSON: one object per line, UTF-8, no other framing.
|
|
6
|
-
The current protocol version is `
|
|
6
|
+
The current protocol version is `3`; the runtime announces it in the `ready`
|
|
7
7
|
event.
|
|
8
8
|
|
|
9
9
|
## Channels
|
|
@@ -29,7 +29,7 @@ event.
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| `execute` | `{"type":"execute","id":str,"code":str}` |
|
|
31
31
|
| `interrupt` | `{"type":"interrupt","id"?:str}` — no reply |
|
|
32
|
-
| `host_reply` | `{"type":"host_reply","id":str,"data":{...}}` — no reply |
|
|
32
|
+
| `host_reply` | `{"type":"host_reply","id":str,"data":{"status":"ok","result":{...}}}` or an error envelope — no reply |
|
|
33
33
|
| `snapshot` | `{"type":"snapshot","id":str,"path":str,"manifest_path":str,"max_bytes"?:int,"max_variable_bytes"?:int,"prune_oversized"?:bool}` |
|
|
34
34
|
| `restore` | `{"type":"restore","id":str,"path":str}` |
|
|
35
35
|
| `list_names` | `{"type":"list_names","id":str}` |
|
|
@@ -42,7 +42,7 @@ runtime keeps serving. Closing stdin is equivalent to `shutdown`.
|
|
|
42
42
|
|
|
43
43
|
## Events
|
|
44
44
|
|
|
45
|
-
- `{"event":"ready","protocol":
|
|
45
|
+
- `{"event":"ready","protocol":3,"python":"3.13.11"}` — sent once at startup;
|
|
46
46
|
the handshake. No banner precedes it.
|
|
47
47
|
- `{"event":"stdout"|"stderr","id":str|null,"text":str}` — captured output.
|
|
48
48
|
`id` is the cell whose Python execution context performed the write; asyncio
|
|
@@ -31,7 +31,7 @@ from typing import Any
|
|
|
31
31
|
|
|
32
32
|
from .bash import _kill_live_handles
|
|
33
33
|
|
|
34
|
-
PROTOCOL_VERSION =
|
|
34
|
+
PROTOCOL_VERSION = 3
|
|
35
35
|
|
|
36
36
|
DEFAULT_SNAPSHOT_MAX_BYTES = 256 * 1024 * 1024
|
|
37
37
|
DEFAULT_SNAPSHOT_MAX_VARIABLE_BYTES = 16 * 1024 * 1024
|
|
@@ -607,20 +607,19 @@ class _SnapshotSizeLimitExceeded(Exception):
|
|
|
607
607
|
pass
|
|
608
608
|
|
|
609
609
|
|
|
610
|
-
class
|
|
611
|
-
def __init__(self, limit: int) -> None:
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
self._buf = io.BytesIO()
|
|
610
|
+
class _CappedWriter:
|
|
611
|
+
def __init__(self, sink: Any, limit: int) -> None:
|
|
612
|
+
self._sink = sink
|
|
615
613
|
self._limit = limit
|
|
614
|
+
self.written = 0
|
|
616
615
|
|
|
617
|
-
def write(self, chunk:
|
|
618
|
-
|
|
616
|
+
def write(self, chunk: Any) -> int:
|
|
617
|
+
size = len(chunk)
|
|
618
|
+
if self.written + size > self._limit:
|
|
619
619
|
raise _SnapshotSizeLimitExceeded()
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return self._buf.getvalue()
|
|
620
|
+
self._sink.write(chunk)
|
|
621
|
+
self.written += size
|
|
622
|
+
return size
|
|
624
623
|
|
|
625
624
|
|
|
626
625
|
def _snapshot_state(
|
|
@@ -655,9 +654,9 @@ def _snapshot_state(
|
|
|
655
654
|
continue
|
|
656
655
|
remaining = max_bytes - total
|
|
657
656
|
limit = max_variable_bytes if prune_oversized else min(max_variable_bytes, remaining)
|
|
658
|
-
buffer =
|
|
657
|
+
buffer = io.BytesIO()
|
|
659
658
|
try:
|
|
660
|
-
dill.dump(value, buffer)
|
|
659
|
+
dill.dump(value, _CappedWriter(buffer, limit))
|
|
661
660
|
blob = buffer.getvalue()
|
|
662
661
|
except _SnapshotSizeLimitExceeded:
|
|
663
662
|
if not prune_oversized and remaining < max_variable_bytes:
|
|
@@ -706,39 +705,41 @@ def _snapshot_state(
|
|
|
706
705
|
previous = None
|
|
707
706
|
try:
|
|
708
707
|
try:
|
|
709
|
-
def serialize(candidate: dict[str, bytes]) -> bytes | None:
|
|
710
|
-
buffer = _SnapshotBuffer(max_bytes)
|
|
711
|
-
try:
|
|
712
|
-
dill.dump(candidate, buffer)
|
|
713
|
-
except _SnapshotSizeLimitExceeded:
|
|
714
|
-
return None
|
|
715
|
-
return buffer.getvalue()
|
|
716
|
-
|
|
717
|
-
serialized_payload = serialize(payload)
|
|
718
|
-
if serialized_payload is None:
|
|
719
|
-
items = list(payload.items())
|
|
720
|
-
serialized_payload = serialize({})
|
|
721
|
-
if serialized_payload is None:
|
|
722
|
-
return {"error": "write failed: snapshot exceeds aggregate snapshot size cap"}
|
|
723
|
-
# Keep the largest insertion-order prefix whose complete pickle fits.
|
|
724
|
-
# Prefix pickle size is monotonic because each prefix only adds a string key and bytes value.
|
|
725
|
-
low, high = 0, len(items) - 1
|
|
726
|
-
while low < high:
|
|
727
|
-
mid = (low + high + 1) // 2
|
|
728
|
-
candidate = serialize(dict(items[:mid]))
|
|
729
|
-
if candidate is None:
|
|
730
|
-
high = mid - 1
|
|
731
|
-
else:
|
|
732
|
-
low = mid
|
|
733
|
-
serialized_payload = candidate
|
|
734
|
-
for name, _ in items[low:]:
|
|
735
|
-
skipped.append({"name": name, "reason": "exceeds aggregate snapshot size cap"})
|
|
736
|
-
payload = dict(items[:low])
|
|
737
|
-
|
|
738
708
|
fh, tmp = stage_temp(path, "wb")
|
|
739
709
|
with fh:
|
|
740
|
-
|
|
741
|
-
|
|
710
|
+
def dump_to_temp(candidate: dict[str, bytes]) -> int | None:
|
|
711
|
+
writer = _CappedWriter(fh, max_bytes)
|
|
712
|
+
try:
|
|
713
|
+
dill.dump(candidate, writer)
|
|
714
|
+
except _SnapshotSizeLimitExceeded:
|
|
715
|
+
return None
|
|
716
|
+
return writer.written
|
|
717
|
+
|
|
718
|
+
def redump_to_temp(candidate: dict[str, bytes]) -> int | None:
|
|
719
|
+
fh.seek(0)
|
|
720
|
+
fh.truncate()
|
|
721
|
+
return dump_to_temp(candidate)
|
|
722
|
+
|
|
723
|
+
bytes_written = dump_to_temp(payload)
|
|
724
|
+
if bytes_written is None:
|
|
725
|
+
# Prefix pickle size is monotonic because each prefix only adds a string key and bytes value.
|
|
726
|
+
items = list(payload.items())
|
|
727
|
+
if redump_to_temp({}) is None:
|
|
728
|
+
return {"error": "write failed: snapshot exceeds aggregate snapshot size cap"}
|
|
729
|
+
low, high = 0, len(items) - 1
|
|
730
|
+
while low < high:
|
|
731
|
+
mid = (low + high + 1) // 2
|
|
732
|
+
if redump_to_temp(dict(items[:mid])) is None:
|
|
733
|
+
high = mid - 1
|
|
734
|
+
else:
|
|
735
|
+
low = mid
|
|
736
|
+
for name, _ in items[low:]:
|
|
737
|
+
skipped.append({"name": name, "reason": "exceeds aggregate snapshot size cap"})
|
|
738
|
+
payload = dict(items[:low])
|
|
739
|
+
# The search's last attempt may have overflowed the temp; rewrite the chosen prefix.
|
|
740
|
+
bytes_written = redump_to_temp(payload)
|
|
741
|
+
if bytes_written is None:
|
|
742
|
+
return {"error": "write failed: snapshot exceeds aggregate snapshot size cap"}
|
|
742
743
|
saved = sorted(payload.keys())
|
|
743
744
|
pruned = sorted(name for name in oversized if name in ns) if prune_oversized else []
|
|
744
745
|
manifest = {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|