@ponythewhite/base-context 1.0.14 → 1.0.16

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 (90) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/base-context-runtime/pyproject.toml +1 -1
  3. package/dist/base-context-runtime/src/rlm/bash.py +19 -0
  4. package/dist/base-context-runtime/src/rlm/job_watch.py +156 -0
  5. package/dist/base-context-runtime/src/rlm/repl.md +28 -6
  6. package/dist/base-context-runtime/src/rlm/repl.py +6 -1
  7. package/dist/base-context-runtime/test/test_bash.py +4 -4
  8. package/dist/base-context-runtime/test/test_job_watch.py +67 -0
  9. package/dist/base-context-runtime/uv.lock +1 -1
  10. package/dist/build-info.json +1 -1
  11. package/dist/bundle/{bundled-modules-UODO7ZR7.js → bundled-modules-2DMTK7VK.js} +2 -2
  12. package/dist/bundle/{chunk-SX7CAXWU.js → chunk-MSH6QRP5.js} +1730 -867
  13. package/dist/bundle/{chunk-NLRWERWA.js → chunk-T2OK7SRV.js} +53 -21
  14. package/dist/bundle/{cli-main-6GXXIMQQ.js → cli-main-WL2L5IEQ.js} +2 -2
  15. package/dist/bundle/cli.js +1 -1
  16. package/dist/bundle/{main-HF64B4HM.js → main-XPIEIEQJ.js} +2 -2
  17. package/dist/core/agent-session.d.ts +2 -0
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +191 -33
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction/compaction.d.ts +1 -1
  22. package/dist/core/compaction/compaction.d.ts.map +1 -1
  23. package/dist/core/compaction/compaction.js +11 -9
  24. package/dist/core/compaction/compaction.js.map +1 -1
  25. package/dist/core/history-index-worker.d.ts.map +1 -1
  26. package/dist/core/history-index-worker.js +22 -1
  27. package/dist/core/history-index-worker.js.map +1 -1
  28. package/dist/core/history-index.d.ts +2 -0
  29. package/dist/core/history-index.d.ts.map +1 -1
  30. package/dist/core/history-index.js.map +1 -1
  31. package/dist/core/inference-coordinator.d.ts.map +1 -1
  32. package/dist/core/inference-coordinator.js +13 -2
  33. package/dist/core/inference-coordinator.js.map +1 -1
  34. package/dist/core/job-watch.d.ts +120 -0
  35. package/dist/core/job-watch.d.ts.map +1 -0
  36. package/dist/core/job-watch.js +648 -0
  37. package/dist/core/job-watch.js.map +1 -0
  38. package/dist/core/kernel/repl-manager.d.ts +5 -1
  39. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  40. package/dist/core/kernel/repl-manager.js +57 -0
  41. package/dist/core/kernel/repl-manager.js.map +1 -1
  42. package/dist/core/kernel/shared.d.ts +12 -0
  43. package/dist/core/kernel/shared.d.ts.map +1 -1
  44. package/dist/core/kernel/shared.js.map +1 -1
  45. package/dist/core/prompts/rlm.d.ts.map +1 -1
  46. package/dist/core/prompts/rlm.js +49 -39
  47. package/dist/core/prompts/rlm.js.map +1 -1
  48. package/dist/core/session-manager.d.ts +10 -0
  49. package/dist/core/session-manager.d.ts.map +1 -1
  50. package/dist/core/session-manager.js +66 -1
  51. package/dist/core/session-manager.js.map +1 -1
  52. package/dist/core/tools/prime-context.d.ts.map +1 -1
  53. package/dist/core/tools/prime-context.js +1 -1
  54. package/dist/core/tools/prime-context.js.map +1 -1
  55. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  56. package/dist/modes/daemon/daemon-mode.js +17 -17
  57. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  58. package/dist/modes/daemon/daemon-worker-client.d.ts +1 -0
  59. package/dist/modes/daemon/daemon-worker-client.d.ts.map +1 -1
  60. package/dist/modes/daemon/daemon-worker-client.js +8 -0
  61. package/dist/modes/daemon/daemon-worker-client.js.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  63. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  64. package/dist/modes/interactive/interactive-mode.js +31 -5
  65. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  66. package/dist/skills/agent-message/SKILL.md +8 -2
  67. package/dist/skills/bounded-inspect/SKILL.md +78 -0
  68. package/dist/skills/bounded-inspect/pyproject.toml +16 -0
  69. package/dist/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
  70. package/dist/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
  71. package/dist/skills/job-watch/SKILL.md +43 -0
  72. package/dist/skills/job-watch/pyproject.toml +13 -0
  73. package/dist/skills/job-watch/src/job_watch/__init__.py +61 -0
  74. package/dist/skills/rlm-heartbeat/SKILL.md +10 -6
  75. package/dist/utils/file-lines.d.ts.map +1 -1
  76. package/dist/utils/file-lines.js +6 -2
  77. package/dist/utils/file-lines.js.map +1 -1
  78. package/docs/rlm-runtime.md +4 -1
  79. package/docs/skills.md +18 -26
  80. package/docs/upstream-1.0.15.md +83 -0
  81. package/package.json +4 -4
  82. package/skills/agent-message/SKILL.md +8 -2
  83. package/skills/bounded-inspect/SKILL.md +78 -0
  84. package/skills/bounded-inspect/pyproject.toml +16 -0
  85. package/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
  86. package/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
  87. package/skills/job-watch/SKILL.md +43 -0
  88. package/skills/job-watch/pyproject.toml +13 -0
  89. package/skills/job-watch/src/job_watch/__init__.py +61 -0
  90. package/skills/rlm-heartbeat/SKILL.md +10 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.16] - 2026-09-28
4
+
5
+ - Fixed background child-usage updates incorrectly stopping an active goal during request preparation.
6
+ - Kept failed goals visible in the bottom-right status tray instead of hiding them.
7
+ - Fixed temporary memory retention when reading a final multi-chunk line without a trailing newline.
8
+ - Added `job-watch` for native job monitoring, prepared reports, and scoped waiting in goal and autonomous modes.
9
+ - Added `bounded-inspect` for selected text and JSON evidence under shared source and output budgets.
10
+ - Improved skill recovery, child-result capsule guidance, and reuse of small project helpers.
11
+
12
+ ## [1.0.15] - 2026-09-28
13
+
14
+ - Fixed compaction of long-lived sessions by selecting the active canonical context before source hydration. This covers manual, agent-requested, threshold, and overflow-recovery compaction without increasing source limits.
15
+ - Fixed Codex HTTP retries losing native context acknowledgement after retryable failures. Only an unchanged prepared request can reuse its acknowledged epoch.
16
+ - Changed `/goal stop` and `/goal clear` to acknowledge the goal that was cleared instead of reporting only the resulting idle state.
17
+ - Fixed daemon worker connection retries retaining failed socket and channel references.
18
+ - Handled Python kernel stderr stream errors through the existing kernel error path.
19
+ - Fixed the goal status display being overwritten by delayed state reads and streamed catch-up snapshots.
20
+
3
21
  ## [1.0.14] - 2026-09-23
4
22
 
5
23
  - Fixed compaction recovery after failed or interrupted partial tool calls, including resuming affected saved sessions without exposing failed replies to the provider.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "base-context-runtime"
3
- version = "1.0.14"
3
+ version = "1.0.16"
4
4
  description = "Kernel-side runtime shim for Base Context recursion"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE", "NOTICE"]
@@ -594,6 +594,25 @@ class BashHandle:
594
594
  return
595
595
  callback()
596
596
 
597
+ def add_done_callback(self, callback: Callable[[BashResult], None]) -> Callable[[], None]:
598
+ """Observe completion without taking await ownership; return an unsubscribe function.
599
+
600
+ The callback may run on a worker thread or immediately for a finished handle.
601
+ Removing it never signals the process. A callback already running may finish.
602
+ """
603
+ def invoke() -> None:
604
+ assert self._result is not None
605
+ callback(self._result)
606
+
607
+ self._add_done_callback(invoke)
608
+
609
+ def remove() -> None:
610
+ with self._callback_lock:
611
+ if invoke in self._callbacks:
612
+ self._callbacks.remove(invoke)
613
+
614
+ return remove
615
+
597
616
  async def _wait(self) -> BashResult:
598
617
  # Asyncio-native wakeup: no executor thread is parked for the command's
599
618
  # duration, so many concurrent awaits cannot exhaust the default pool.
@@ -0,0 +1,156 @@
1
+ """Contained job-watch probes and completion callbacks. No notebook evaluation."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import math
7
+ import uuid
8
+ from decimal import Decimal
9
+ import weakref
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Callable
12
+
13
+ from .bash import BashHandle, BashResult, bash
14
+
15
+ _identities: weakref.WeakKeyDictionary[BashHandle, str] = weakref.WeakKeyDictionary()
16
+ _handles: weakref.WeakValueDictionary[str, BashHandle] = weakref.WeakValueDictionary()
17
+ _results: weakref.WeakKeyDictionary[BashHandle, BashResult] = weakref.WeakKeyDictionary()
18
+ _callbacks: dict[tuple[str, str], Callable[[], None]] = {}
19
+ _probes: dict[str, asyncio.Task[None]] = {}
20
+
21
+
22
+ def identity(handle: BashHandle) -> str:
23
+ if not isinstance(handle, BashHandle):
24
+ raise TypeError("job must be a live BashHandle")
25
+ resource = _identities.setdefault(handle, uuid.uuid4().hex)
26
+ _handles[resource] = handle
27
+ return resource
28
+
29
+
30
+ def _observed(job_id: str, result: BashResult | None) -> dict[str, Any]:
31
+ return {
32
+ "observed_at": datetime.now(timezone.utc).isoformat(),
33
+ "job_id": job_id,
34
+ "state": "running" if result is None else "succeeded" if result.exit_code == 0 else "failed",
35
+ "progress": {} if result is None else {"exit_code": result.exit_code, "duration_seconds": result.duration},
36
+ "attention": [], "evidence": [],
37
+ }
38
+
39
+
40
+ def bind(handle: BashHandle, watch_id: str, generation: str, job_id: str) -> None:
41
+ from . import host_request
42
+ loop = asyncio.get_running_loop()
43
+ key = (watch_id, generation)
44
+ if key in _callbacks:
45
+ return
46
+
47
+ async def deliver(observation: dict[str, Any]) -> None:
48
+ try:
49
+ await host_request("job_watch.observation", {
50
+ "id": watch_id, "generation": generation,
51
+ "result": {"source": "handle", "observation": observation},
52
+ })
53
+ except RuntimeError:
54
+ pass # Host teardown owns recovery; never restart a monitored process.
55
+
56
+ def completed(result: BashResult) -> None:
57
+ _results[handle] = result
58
+ observation = _observed(job_id, result)
59
+ try:
60
+ loop.call_soon_threadsafe(lambda: asyncio.create_task(deliver(observation)))
61
+ except RuntimeError:
62
+ pass
63
+
64
+ _callbacks[key] = handle.add_done_callback(completed)
65
+
66
+
67
+ def unbind(watch_id: str) -> None:
68
+ for key in list(_callbacks):
69
+ if key[0] == watch_id:
70
+ _callbacks.pop(key)()
71
+
72
+
73
+ _SAFE_INTEGER = 2**53 - 1
74
+ _NUMERIC_ERROR = "probe numeric precision is unavailable; encode exact identifiers and precision-sensitive values as strings"
75
+
76
+
77
+ def _exact_integer(token: str) -> int:
78
+ try:
79
+ value = int(token)
80
+ except ValueError:
81
+ raise ValueError(_NUMERIC_ERROR) from None
82
+ if abs(value) > _SAFE_INTEGER or token == "-0":
83
+ raise ValueError(_NUMERIC_ERROR)
84
+ return value
85
+
86
+
87
+ def _exact_decimal(token: str) -> float:
88
+ value = float(token)
89
+ if (not math.isfinite(value) or abs(value) > _SAFE_INTEGER
90
+ or (value == 0 and token.startswith("-")) or Decimal(str(value)) != Decimal(token)):
91
+ raise ValueError(_NUMERIC_ERROR)
92
+ return value
93
+
94
+
95
+ async def _probe(request: dict[str, Any]) -> None:
96
+ from . import host_request
97
+ request_id = request["id"]
98
+ try:
99
+ command = request.get("command")
100
+ handle = _handles.get(request["resourceId"])
101
+ if request.get("completionSource") == "handle" and handle is None:
102
+ result = {"source": "handle", "error": "handle unavailable after kernel restart"}
103
+ elif command is None:
104
+ result = ({"source": "handle", "observation": _observed(request["jobId"], _results.get(handle))}
105
+ if handle is not None else {"source": "handle", "error": "handle unavailable after kernel restart"})
106
+ else:
107
+ # The first await owns this new probe only. Cancellation never reaches the monitored job.
108
+ finished = await asyncio.wait_for(bash(command), timeout=request["timeoutMs"] / 1000)
109
+ output = finished.output
110
+ result = {"source": "probe", "output": output, "capture_complete": False}
111
+ if finished.exit_code != 0:
112
+ result["error"] = f"probe exited {finished.exit_code}"
113
+ elif len(output.encode("utf-8")) > 16384:
114
+ result["error"] = "probe JSON exceeds 16 KiB; use a project summary producer"
115
+ else:
116
+ try:
117
+ result["observation"] = json.loads(output, parse_int=_exact_integer,
118
+ parse_float=_exact_decimal, parse_constant=_exact_decimal)
119
+ except (ValueError, TypeError) as error:
120
+ result["error"] = (str(error) if isinstance(error, ValueError) and not isinstance(error, json.JSONDecodeError)
121
+ else "probe JSON unavailable or incomplete")
122
+ await host_request("job_watch.probe_result", {"request_id": request_id, "result": result})
123
+ except asyncio.CancelledError:
124
+ raise
125
+ except Exception as error:
126
+ try:
127
+ await host_request("job_watch.probe_result", {
128
+ "request_id": request_id, "result": {"source": "probe", "error": f"{type(error).__name__}: {error}"[:256]},
129
+ })
130
+ except RuntimeError:
131
+ pass
132
+ finally:
133
+ _probes.pop(request_id, None)
134
+
135
+
136
+ def dispatch(request: dict[str, Any]) -> None:
137
+ """Called only by the REPL protocol dispatcher, not by notebook eval."""
138
+ request_id = request.get("id")
139
+ if not isinstance(request_id, str):
140
+ raise ValueError("job_watch_probe requires an id")
141
+ if request.get("cancel"):
142
+ task = _probes.get(request_id)
143
+ if task:
144
+ task.cancel()
145
+ return
146
+ if request_id in _probes:
147
+ return
148
+ if not isinstance(request.get("jobId"), str) or not isinstance(request.get("resourceId"), str):
149
+ raise ValueError("job_watch_probe requires resource and job identity")
150
+ command = request.get("command")
151
+ if command is not None and (not isinstance(command, str) or not command or len(command.encode()) > 8192):
152
+ raise ValueError("invalid probe command")
153
+ timeout = request.get("timeoutMs")
154
+ if not isinstance(timeout, (int, float)) or not 1 <= timeout <= 300000:
155
+ raise ValueError("invalid probe timeout")
156
+ _probes[request_id] = asyncio.create_task(_probe(request))
@@ -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 `3`; the runtime announces it in the `ready`
6
+ The current protocol version is `4`; the runtime announces it in the `ready`
7
7
  event.
8
8
 
9
9
  ## Channels
@@ -30,20 +30,23 @@ event.
30
30
  | `execute` | `{"type":"execute","id":str,"code":str}` |
31
31
  | `interrupt` | `{"type":"interrupt","id"?:str}` — no reply |
32
32
  | `host_reply` | `{"type":"host_reply","id":str,"data":{"status":"ok","result":{...}}}` or an error envelope — no reply |
33
+ | `job_watch_probe` | Optional capability: `{"type":"job_watch_probe","id":str,"watchId":str,"generation":str,"resourceId":str,"jobId":str,"completionSource":"handle"|"probe","command":str|null,"timeoutMs":number}`; cancel with `{"type":"job_watch_probe","id":str,"cancel":true}` |
33
34
  | `snapshot` | `{"type":"snapshot","id":str,"path":str,"manifest_path":str,"max_bytes"?:int,"max_variable_bytes"?:int,"prune_oversized"?:bool}` |
34
35
  | `restore` | `{"type":"restore","id":str,"path":str}` |
35
36
  | `list_names` | `{"type":"list_names","id":str}` |
36
37
  | `shutdown` | `{"type":"shutdown","id"?:str}` |
37
38
 
38
- Requests other than `interrupt` and `host_reply` run strictly in order, one at
39
- a time. A malformed line
39
+ Requests other than `interrupt`, `host_reply`, and the optional `job_watch_probe`
40
+ run strictly in order, one at a time. A malformed line
40
41
  produces `{"event":"error","id":null,"ename":"ProtocolError",...}` and the
41
42
  runtime keeps serving. Closing stdin is equivalent to `shutdown`.
42
43
 
43
44
  ## Events
44
45
 
45
- - `{"event":"ready","protocol":3,"python":"3.13.11"}` — sent once at startup;
46
- the handshake. No banner precedes it.
46
+ - `{"event":"ready","protocol":4,"python":"3.13.11"}` — sent once at startup;
47
+ the handshake. No banner precedes it. The optional `capabilities` array may
48
+ contain `"job_watch_probe_v1"`. Protocol version remains 4; old hosts ignore
49
+ optional metadata and new hosts refuse probes locally when the capability is absent.
47
50
  - `{"event":"stdout"|"stderr","id":str|null,"text":str}` — captured output.
48
51
  `id` is the cell whose Python execution context performed the write; asyncio
49
52
  tasks inherit the spawning cell's id (even after that cell finished). `null`
@@ -62,7 +65,7 @@ runtime keeps serving. Closing stdin is equivalent to `shutdown`.
62
65
  carrying the same id.
63
66
  - `{"event":"error","id":str|null,"ename":str,"evalue":str,"traceback":[str,...]}`
64
67
  - `{"event":"done","id":str,"status":"ok"|"error"}` — exactly one per id'd
65
- request, always after all of that request's other events. A snapshot `done`
68
+ queued request, always after all of that request's other events. A snapshot `done`
66
69
  adds `saved`, `skipped`, `pruned`, `bytes`; a restore `done` adds `restored`,
67
70
  `failed`; a `list_names` `done` adds `names`; a failed snapshot/restore adds
68
71
  `reason`. Restoring a missing file reports `status:"ok"` with empty
@@ -138,6 +141,25 @@ in-flight execute. Replies for unknown ids, or for a request whose awaiting
138
141
  cell was cancelled, are dropped. `rlm.repl.is_active()` reports whether the
139
142
  process is serving the protocol (importing the module does not count).
140
143
 
144
+ ## Optional job-watch bridge
145
+
146
+ `job_watch_probe_v1` runs contained read-only probes outside the ordinary cell
147
+ FIFO, not notebook closures. Results use the existing host bridge with
148
+ `type:"job_watch.probe_result"`, the original `request_id`, and `result`; this
149
+ request does not produce an ordinary cell `done`. Local handle completion uses
150
+ `job_watch.observation` with watch identity/generation. The host owns comparison,
151
+ scheduling, prepared follow-ups, and scoped goal/autonomous parking.
152
+
153
+ Probe commands must name the intended working directory, interpreter, and paths;
154
+ no notebook environment snapshot is persisted.
155
+
156
+ `completionSource` distinguishes a local handle from an authoritative external
157
+ probe. A lost handle becomes unknown; neither a PID nor a successful detached
158
+ launcher is proof of remote completion. Cancelling a probe or unregistering a
159
+ watch does not kill the monitored job. The passive Bash callback bridge does
160
+ not change interrupted-await ownership. This adds no daemon wire version or
161
+ model tool and requires no extra startup service.
162
+
141
163
  ## Snapshot / restore
142
164
 
143
165
  `snapshot` serializes the user namespace with `dill` (recurse mode), one name
@@ -986,6 +986,10 @@ def _handle_request_line(raw: bytes, queue: asyncio.Queue[dict[str, Any]]) -> No
986
986
  return
987
987
  _request_interrupt(req.get("id"))
988
988
  return
989
+ if rtype == "job_watch_probe":
990
+ from .job_watch import dispatch
991
+ _loop.call_soon_threadsafe(dispatch, req)
992
+ return
989
993
  if rtype == "host_reply":
990
994
  # Bypass the FIFO queue: the awaiting cell IS the in-flight
991
995
  # execute, so a queued reply would deadlock behind it.
@@ -1152,7 +1156,8 @@ def main() -> None:
1152
1156
  signal.signal(signal.SIGINT, _sigint_handler)
1153
1157
  threading.Thread(target=_read_requests, args=(stdin_fd, queue), daemon=True).start()
1154
1158
 
1155
- _send({"event": "ready", "protocol": PROTOCOL_VERSION, "python": platform.python_version()})
1159
+ _send({"event": "ready", "protocol": PROTOCOL_VERSION, "python": platform.python_version(),
1160
+ "capabilities": ["job_watch_probe_v1"]})
1156
1161
 
1157
1162
  _serve_task = _loop.create_task(_serve(queue, user_module.__dict__))
1158
1163
  # A KeyboardInterrupt escaping a cell or background task stops
@@ -1237,8 +1237,8 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
1237
1237
  with mock.patch.dict(
1238
1238
  os.environ,
1239
1239
  {
1240
- "PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL": journal,
1241
- "PRIME_AGENT_KERNEL_OWNER_PID": "notanint",
1240
+ "BASE_CONTEXT_INTERNAL_ORPHAN_PROCESS_JOURNAL": journal,
1241
+ "BASE_CONTEXT_KERNEL_OWNER_PID": "notanint",
1242
1242
  },
1243
1243
  ):
1244
1244
  with self.assertRaises(RuntimeError):
@@ -1250,8 +1250,8 @@ class BashTest(unittest.IsolatedAsyncioTestCase):
1250
1250
  with mock.patch.dict(
1251
1251
  os.environ,
1252
1252
  {
1253
- "PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL": journal,
1254
- "PRIME_AGENT_KERNEL_OWNER_PID": str(os.getpid()),
1253
+ "BASE_CONTEXT_INTERNAL_ORPHAN_PROCESS_JOURNAL": journal,
1254
+ "BASE_CONTEXT_KERNEL_OWNER_PID": str(os.getpid()),
1255
1255
  },
1256
1256
  ):
1257
1257
  with mock.patch.object(bash_module, "_process_start_id", return_value=None):
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ import unittest
3
+ from rlm.bash import bash
4
+ from rlm.job_watch import identity, _probe, _handles
5
+ from unittest.mock import AsyncMock, patch
6
+ import gc
7
+ import threading
8
+
9
+
10
+ class JobWatchCallbackTest(unittest.IsolatedAsyncioTestCase):
11
+ async def test_callback_and_identity_preserve_first_await_ownership(self):
12
+ handle = bash("python3 -c 'import time; time.sleep(10)'")
13
+ resource = identity(handle)
14
+ remove = handle.add_done_callback(lambda result: None)
15
+ with patch("rlm.host_request", new=AsyncMock()):
16
+ await _probe({"id": "probe", "resourceId": resource, "jobId": "job", "completionSource": "handle", "command": None})
17
+ self.assertFalse(handle._released)
18
+ async def wait():
19
+ return await handle
20
+ task = asyncio.create_task(wait())
21
+ await asyncio.sleep(0)
22
+ task.cancel()
23
+ with self.assertRaises(asyncio.CancelledError):
24
+ await task
25
+ self.assertIsNotNone(handle.poll())
26
+ self.assertNotEqual(handle.poll().exit_code, 0)
27
+ remove()
28
+
29
+ async def test_background_await_stays_nonowning_and_unregister_never_kills(self):
30
+ handle = bash("python3 -c 'import time; time.sleep(10)'")
31
+ _ = handle.pid
32
+ remove = handle.add_done_callback(lambda result: None)
33
+ async def wait():
34
+ return await handle
35
+ task = asyncio.create_task(wait())
36
+ await asyncio.sleep(0)
37
+ task.cancel()
38
+ with self.assertRaises(asyncio.CancelledError):
39
+ await task
40
+ remove()
41
+ self.assertIsNone(handle.poll())
42
+ handle.kill(grace=0)
43
+ await handle
44
+
45
+ async def test_completed_callback_is_immediate_and_idempotently_removable(self):
46
+ workers = []
47
+ real_thread = threading.Thread
48
+ def worker_thread(*args, **kwargs):
49
+ thread = real_thread(*args, **kwargs)
50
+ workers.append(thread)
51
+ return thread
52
+ with patch("rlm.bash.threading.Thread", side_effect=worker_thread):
53
+ handle = bash("printf done")
54
+ resource = identity(handle)
55
+ result = await handle
56
+ seen = []
57
+ remove = handle.add_done_callback(seen.append)
58
+ remove(); remove()
59
+ self.assertEqual(seen, [result])
60
+ # Foreground result delivery precedes containment-worker retirement.
61
+ # Their bound methods legitimately retain the handle until they exit.
62
+ for worker in workers:
63
+ await asyncio.to_thread(worker.join, 1)
64
+ self.assertFalse(worker.is_alive())
65
+ del remove, handle
66
+ gc.collect()
67
+ self.assertNotIn(resource, _handles)
@@ -367,7 +367,7 @@ wheels = [
367
367
 
368
368
  [[package]]
369
369
  name = "base-context-runtime"
370
- version = "1.0.14"
370
+ version = "1.0.16"
371
371
  source = { editable = "." }
372
372
  dependencies = [
373
373
  { name = "mcp" },
@@ -1,5 +1,5 @@
1
1
  {
2
- "sourceCommit": "1c9651df8abd812cdba9e323517067363acc621c",
2
+ "sourceCommit": "2ec583d3c870d042a46a5ecc1e6ffa83b8edd95e",
3
3
  "sourceDirty": false,
4
4
  "buildHost": {
5
5
  "platform": "linux",
@@ -51,7 +51,7 @@ import {
51
51
  oauth_exports,
52
52
  runPrintMode,
53
53
  runRpcMode
54
- } from "./chunk-NLRWERWA.js";
54
+ } from "./chunk-T2OK7SRV.js";
55
55
  import {
56
56
  AgentSession,
57
57
  CURRENT_SESSION_VERSION,
@@ -143,7 +143,7 @@ import {
143
143
  withFileMutationQueue,
144
144
  wrapRegisteredTool,
145
145
  wrapRegisteredTools
146
- } from "./chunk-SX7CAXWU.js";
146
+ } from "./chunk-MSH6QRP5.js";
147
147
  import "./chunk-CFNASUYO.js";
148
148
  import "./chunk-V6LOBH6X.js";
149
149
  import {