fix(sdk): harden runtime lifecycle and JSON-RPC
Keep DeepSeekHarness.run() reusable, but make ownership of its lazy runtime process explicit. Document the context-manager/close contract and update every construction example to use a context manager so repeated runs remain valid without encouraging leaked subprocesses. Contain notification predicate failures at the subscription boundary. Remove only the subscriber whose callback raised, deliver that exception through its queue, and continue dispatching to healthy subscribers so arbitrary callback code cannot terminate the shared reader thread or strand later requests. Enforce one in-flight prompt per server session with an atomic activePrompt guard. Route overlap through the existing -32603 handler-error response and clear the guard in finally, preserving parallel prompts across sessions and sequential reuse without changing JSON-RPC request or notification shapes. Use StringDecoder for line framing so a UTF-8 code point split across Buffer chunks is not corrupted. Add a queued-write flush barrier, and make memoized shutdown await it before disposal and exit while retaining exactly-once cleanup when shutdown calls race or flushing fails. Cover callback isolation, same-session exclusion, cross-session concurrency, split multibyte input, delayed writes, racing shutdown, and flush failure with deterministic tests.
This commit is contained in:
@@ -43,7 +43,12 @@ class TurnResult:
|
||||
|
||||
|
||||
class DeepSeekHarness:
|
||||
"""Synchronous high-level SDK for running DeepSeek Harness agent turns."""
|
||||
"""Reusable synchronous SDK for running DeepSeek Harness agent turns.
|
||||
|
||||
The runtime subprocess starts lazily and remains owned by this instance
|
||||
across calls to :meth:`run`. Use the instance as a context manager, or call
|
||||
:meth:`close` explicitly when finished, so the subprocess is always reaped.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DeepSeekHarnessConfig | None = None, **kwargs: object) -> None:
|
||||
if config is not None and kwargs:
|
||||
|
||||
@@ -350,10 +350,19 @@ class HarnessClient:
|
||||
params = message.get("params")
|
||||
notification = Notification(method=method, payload=params if isinstance(params, dict) else {})
|
||||
with self._lock:
|
||||
subscribers = list(self._notification_subscribers.values())
|
||||
subscribers = list(self._notification_subscribers.items())
|
||||
delivered = False
|
||||
for subscriber, predicate in subscribers:
|
||||
if predicate is None or predicate(notification):
|
||||
for subscription_id, (subscriber, predicate) in subscribers:
|
||||
try:
|
||||
matches = predicate is None or predicate(notification)
|
||||
except BaseException as exc:
|
||||
with self._lock:
|
||||
current = self._notification_subscribers.get(subscription_id)
|
||||
if current is not None and current[0] is subscriber:
|
||||
self._notification_subscribers.pop(subscription_id, None)
|
||||
subscriber.put(exc)
|
||||
continue
|
||||
if matches:
|
||||
subscriber.put(notification)
|
||||
delivered = True
|
||||
if not delivered:
|
||||
|
||||
Reference in New Issue
Block a user