superdoc-sdk 1.6.0.dev23__tar.gz → 1.6.0.dev29__tar.gz

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 (38) hide show
  1. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/PKG-INFO +6 -6
  2. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/pyproject.toml +6 -6
  3. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/__init__.py +2 -0
  4. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/client.py +5 -0
  5. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/runtime.py +7 -1
  6. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/transport.py +179 -35
  7. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/PKG-INFO +6 -6
  8. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/requires.txt +5 -5
  9. superdoc_sdk-1.6.0.dev29/tests/test_transport.py +943 -0
  10. superdoc_sdk-1.6.0.dev23/tests/test_transport.py +0 -495
  11. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/README.md +0 -0
  12. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/setup.cfg +0 -0
  13. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/embedded_cli.py +0 -0
  14. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/errors.py +0 -0
  15. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/__init__.py +0 -0
  16. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/client.py +0 -0
  17. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/generated/contract.py +0 -0
  18. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/protocol.py +0 -0
  19. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skill_api.py +0 -0
  20. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skills/__init__.py +0 -0
  21. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/skills/editing-docx.md +0 -0
  22. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/test_parity_helper.py +0 -0
  23. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/catalog.json +0 -0
  24. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/intent_dispatch_generated.py +0 -0
  25. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/system-prompt-mcp.md +0 -0
  26. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/system-prompt.md +0 -0
  27. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools-policy.json +0 -0
  28. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.anthropic.json +0 -0
  29. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.generic.json +0 -0
  30. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.openai.json +0 -0
  31. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools/tools.vercel.json +0 -0
  32. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc/tools_api.py +0 -0
  33. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/SOURCES.txt +0 -0
  34. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/dependency_links.txt +0 -0
  35. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/superdoc_sdk.egg-info/top_level.txt +0 -0
  36. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_client_method_naming.py +0 -0
  37. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_helpers_reexports.py +0 -0
  38. {superdoc_sdk-1.6.0.dev23 → superdoc_sdk-1.6.0.dev29}/tests/test_protocol.py +0 -0
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superdoc-sdk
3
- Version: 1.6.0.dev23
3
+ Version: 1.6.0.dev29
4
4
  Summary: SuperDoc SDK (CLI-backed)
5
5
  Author: SuperDoc
6
6
  License-Expression: AGPL-3.0
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
- Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev23; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
10
- Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev23; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
11
- Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev23; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
12
- Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev23; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
13
- Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev23; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
9
+ Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
10
+ Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
11
+ Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
12
+ Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
13
+ Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
14
14
 
15
15
  # superdoc-sdk
16
16
 
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "superdoc-sdk"
7
- version = "1.6.0.dev23"
7
+ version = "1.6.0.dev29"
8
8
  description = "SuperDoc SDK (CLI-backed)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "AGPL-3.0"
12
12
  authors = [{ name = "SuperDoc" }]
13
13
  dependencies = [
14
- "superdoc-sdk-cli-darwin-arm64==1.6.0.dev23; platform_system == 'Darwin' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
15
- "superdoc-sdk-cli-darwin-x64==1.6.0.dev23; platform_system == 'Darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
16
- "superdoc-sdk-cli-linux-x64==1.6.0.dev23; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
17
- "superdoc-sdk-cli-linux-arm64==1.6.0.dev23; platform_system == 'Linux' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
18
- "superdoc-sdk-cli-windows-x64==1.6.0.dev23; platform_system == 'Windows' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
14
+ "superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == 'Darwin' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
15
+ "superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == 'Darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
16
+ "superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == 'Linux' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
17
+ "superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == 'Linux' and (platform_machine == 'arm64' or platform_machine == 'aarch64' or platform_machine == 'ARM64')",
18
+ "superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == 'Windows' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'amd64')",
19
19
  ]
20
20
 
21
21
  [tool.setuptools]
@@ -10,6 +10,7 @@ from .tools_api import (
10
10
  get_tool_catalog,
11
11
  list_tools,
12
12
  )
13
+ from .transport import DEFAULT_STDOUT_BUFFER_LIMIT_BYTES
13
14
 
14
15
  __all__ = [
15
16
  "SuperDocClient",
@@ -17,6 +18,7 @@ __all__ = [
17
18
  "SuperDocDocument",
18
19
  "AsyncSuperDocDocument",
19
20
  "SuperDocError",
21
+ "DEFAULT_STDOUT_BUFFER_LIMIT_BYTES",
20
22
  "get_skill",
21
23
  "install_skill",
22
24
  "list_skills",
@@ -22,6 +22,7 @@ from .generated.client import (
22
22
  DocOpenResult as GeneratedDocOpenResult,
23
23
  )
24
24
  from .runtime import SuperDocAsyncRuntime, SuperDocSyncRuntime
25
+ from .transport import DEFAULT_STDOUT_BUFFER_LIMIT_BYTES
25
26
 
26
27
  UserIdentity = Dict[str, str]
27
28
 
@@ -340,6 +341,9 @@ class AsyncSuperDocClient:
340
341
  request_timeout_ms: int | None = None,
341
342
  watchdog_timeout_ms: int = 30_000,
342
343
  max_queue_depth: int = 100,
344
+ # Raise if a single host response can exceed this size (e.g. reading
345
+ # very large documents); otherwise the default is safe.
346
+ stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
343
347
  default_change_mode: Literal['direct', 'tracked'] | None = None,
344
348
  user: UserIdentity | None = None,
345
349
  ) -> None:
@@ -350,6 +354,7 @@ class AsyncSuperDocClient:
350
354
  request_timeout_ms=request_timeout_ms,
351
355
  watchdog_timeout_ms=watchdog_timeout_ms,
352
356
  max_queue_depth=max_queue_depth,
357
+ stdout_buffer_limit_bytes=stdout_buffer_limit_bytes,
353
358
  default_change_mode=default_change_mode,
354
359
  user=user,
355
360
  )
@@ -14,7 +14,11 @@ from typing import Any, Dict, Optional
14
14
  from .embedded_cli import resolve_embedded_cli_path
15
15
  from .generated.contract import OPERATION_INDEX
16
16
  from .protocol import normalize_default_change_mode
17
- from .transport import AsyncHostTransport, SyncHostTransport
17
+ from .transport import (
18
+ DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
19
+ AsyncHostTransport,
20
+ SyncHostTransport,
21
+ )
18
22
 
19
23
 
20
24
  class SuperDocSyncRuntime:
@@ -79,6 +83,7 @@ class SuperDocAsyncRuntime:
79
83
  request_timeout_ms: Optional[int] = None,
80
84
  watchdog_timeout_ms: int = 30_000,
81
85
  max_queue_depth: int = 100,
86
+ stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
82
87
  default_change_mode: Optional[str] = None,
83
88
  user: Optional[Dict[str, str]] = None,
84
89
  ) -> None:
@@ -93,6 +98,7 @@ class SuperDocAsyncRuntime:
93
98
  request_timeout_ms=request_timeout_ms,
94
99
  watchdog_timeout_ms=watchdog_timeout_ms,
95
100
  max_queue_depth=max_queue_depth,
101
+ stdout_buffer_limit_bytes=stdout_buffer_limit_bytes,
96
102
  default_change_mode=self._default_change_mode,
97
103
  user=user,
98
104
  )
@@ -43,6 +43,12 @@ from .protocol import (
43
43
 
44
44
  logger = logging.getLogger('superdoc.transport')
45
45
 
46
+ # Default stdout StreamReader buffer for the async transport. Host responses
47
+ # are single newline-delimited JSON lines, so this caps the largest individual
48
+ # response a caller can receive. Raise it if your workload routinely produces
49
+ # responses above this size (e.g. whole-document reads on very large docs).
50
+ DEFAULT_STDOUT_BUFFER_LIMIT_BYTES = 64 * 1024 * 1024
51
+
46
52
  # Opt-in debug logging via SUPERDOC_DEBUG=1 or SUPERDOC_LOG_LEVEL=debug.
47
53
  # Only configures the named logger — never mutates root logging config.
48
54
  _log_level = os.environ.get('SUPERDOC_LOG_LEVEL', '').lower()
@@ -399,6 +405,7 @@ class AsyncHostTransport:
399
405
  request_timeout_ms: Optional[int] = None,
400
406
  watchdog_timeout_ms: int = 30_000,
401
407
  max_queue_depth: int = 100,
408
+ stdout_buffer_limit_bytes: int = DEFAULT_STDOUT_BUFFER_LIMIT_BYTES,
402
409
  default_change_mode: Optional[ChangeMode] = None,
403
410
  user: Optional[Dict[str, str]] = None,
404
411
  ) -> None:
@@ -409,11 +416,13 @@ class AsyncHostTransport:
409
416
  self._request_timeout_ms = request_timeout_ms
410
417
  self._watchdog_timeout_ms = watchdog_timeout_ms
411
418
  self._max_queue_depth = max_queue_depth
419
+ self._stdout_buffer_limit_bytes = stdout_buffer_limit_bytes
412
420
  self._default_change_mode = default_change_mode
413
421
  self._user = user
414
422
 
415
423
  self._process: Optional[asyncio.subprocess.Process] = None
416
424
  self._reader_task: Optional[asyncio.Task] = None
425
+ self._cleanup_task: Optional[asyncio.Task] = None
417
426
  self._pending: Dict[int, asyncio.Future] = {}
418
427
  self._state = _State.DISCONNECTED
419
428
  self._next_request_id = 1
@@ -428,7 +437,22 @@ class AsyncHostTransport:
428
437
 
429
438
  async def dispose(self) -> None:
430
439
  """Gracefully shut down the host process."""
431
- if self._state == _State.DISCONNECTED or self._state == _State.DISPOSING:
440
+ if self._state == _State.DISCONNECTED:
441
+ return
442
+ if self._state == _State.DISPOSING:
443
+ # A reader-triggered cleanup is in flight (or an earlier teardown
444
+ # left state in DISPOSING briefly). Wait for it so the caller
445
+ # observes "host fully torn down" by the time dispose() returns.
446
+ # shield() so a cancelled dispose() doesn't interrupt _cleanup
447
+ # mid-flight and leak the host process.
448
+ existing = self._cleanup_task
449
+ if existing and not existing.done():
450
+ try:
451
+ await asyncio.shield(existing)
452
+ except asyncio.CancelledError:
453
+ raise
454
+ except Exception:
455
+ pass
432
456
  return
433
457
 
434
458
  self._stopping = True
@@ -507,6 +531,20 @@ class AsyncHostTransport:
507
531
 
508
532
  async def _ensure_connected(self) -> None:
509
533
  """Lazy connect: spawn and handshake if not already connected."""
534
+ # Drain any in-flight teardown before spawning a new host. Without
535
+ # this, a concurrent reader-triggered cleanup would still be running
536
+ # when _start_host reassigns self._process / self._reader_task; the
537
+ # cleanup task would then cancel the fresh reader and kill the fresh
538
+ # process. shield() so we don't cancel the cleanup if our caller is.
539
+ cleanup = self._cleanup_task
540
+ if cleanup and not cleanup.done():
541
+ try:
542
+ await asyncio.shield(cleanup)
543
+ except asyncio.CancelledError:
544
+ raise
545
+ except Exception:
546
+ pass
547
+
510
548
  if self._state == _State.CONNECTED and self._process and self._process.returncode is None:
511
549
  return
512
550
 
@@ -531,12 +569,15 @@ class AsyncHostTransport:
531
569
  args = [*prefix_args, 'host', '--stdio']
532
570
 
533
571
  try:
572
+ # ``limit`` raises asyncio's StreamReader buffer above its 64 KiB
573
+ # default; host responses are single JSON lines and can exceed it.
534
574
  self._process = await asyncio.create_subprocess_exec(
535
575
  command, *args,
536
576
  stdin=asyncio.subprocess.PIPE,
537
577
  stdout=asyncio.subprocess.PIPE,
538
578
  stderr=asyncio.subprocess.DEVNULL,
539
579
  env={**os.environ, **self._env},
580
+ limit=self._stdout_buffer_limit_bytes,
540
581
  )
541
582
  logger.debug('Host spawned (pid=%s, bin=%s).', self._process.pid, self._cli_bin)
542
583
  except Exception as exc:
@@ -582,7 +623,29 @@ class AsyncHostTransport:
582
623
 
583
624
  try:
584
625
  while True:
585
- raw = await process.stdout.readline()
626
+ try:
627
+ raw = await process.stdout.readline()
628
+ except ValueError as exc:
629
+ # asyncio.StreamReader.readline() re-raises LimitOverrunError
630
+ # from readuntil() as ValueError when a single line exceeds
631
+ # `limit` (see CPython asyncio/streams.py). The host is still
632
+ # alive — schedule cleanup so a later dispose() doesn't
633
+ # short-circuit on DISCONNECTED state. Scoped to readline()
634
+ # only so unrelated ValueErrors from dispatch aren't
635
+ # reclassified as a buffer-limit error. _schedule_cleanup
636
+ # is a no-op when _stopping is set (graceful dispose path).
637
+ logger.debug('Reader loop buffer overflow: %s', exc)
638
+ self._schedule_cleanup(SuperDocError(
639
+ 'Host response exceeded stdout buffer limit. '
640
+ 'Raise stdout_buffer_limit_bytes to accommodate larger responses.',
641
+ code=HOST_PROTOCOL_ERROR,
642
+ details={
643
+ 'message': str(exc),
644
+ 'stdout_buffer_limit_bytes': self._stdout_buffer_limit_bytes,
645
+ },
646
+ ))
647
+ return
648
+
586
649
  if not raw:
587
650
  # EOF — process died.
588
651
  break
@@ -614,16 +677,16 @@ class AsyncHostTransport:
614
677
  except Exception as exc:
615
678
  logger.debug('Reader loop error: %s', exc)
616
679
 
617
- # Reader exited (EOF or error) — reject all pending futures.
618
- if not self._stopping:
619
- exit_code = process.returncode
620
- error = SuperDocError(
621
- 'Host process disconnected.',
622
- code=HOST_DISCONNECTED,
623
- details={'exit_code': exit_code, 'signal': None},
624
- )
625
- self._reject_all_pending(error)
626
- self._state = _State.DISCONNECTED
680
+ # Reader exited (EOF or unexpected error) — tear down the process so
681
+ # no orphaned host is left running, then reject pending futures.
682
+ # _schedule_cleanup is a no-op when _stopping is set (graceful
683
+ # dispose path) so we don't race the dispose teardown.
684
+ exit_code = process.returncode
685
+ self._schedule_cleanup(SuperDocError(
686
+ 'Host process disconnected.',
687
+ code=HOST_DISCONNECTED,
688
+ details={'exit_code': exit_code, 'signal': None},
689
+ ))
627
690
 
628
691
  async def _send_request(self, method: str, params: Any, watchdog_ms: int) -> Any:
629
692
  """Send a JSON-RPC request and await the matching response future."""
@@ -687,34 +750,88 @@ class AsyncHostTransport:
687
750
  future.set_exception(error)
688
751
 
689
752
  async def _kill_and_reset(self) -> None:
690
- """Kill the host process and reset to DISCONNECTED."""
691
- await self._cleanup(
692
- SuperDocError('Host process disconnected.', code=HOST_DISCONNECTED),
693
- )
694
-
695
- async def _cleanup(self, error: Optional[SuperDocError]) -> None:
696
- """Cancel reader, kill process, reject pending, reset state."""
697
- if self._reader_task and not self._reader_task.done():
698
- self._reader_task.cancel()
753
+ """Kill the host process and reset to DISCONNECTED.
754
+
755
+ Coordinates with `_schedule_cleanup` so callers (e.g. `_send_request`
756
+ on watchdog timeout or stdin write failure) don't run a parallel
757
+ `_cleanup` that races a reader-triggered cleanup on
758
+ `_reject_all_pending` and `process.kill`. If a cleanup is already in
759
+ flight, await it; otherwise own a fresh task in the same slot so a
760
+ later concurrent caller sees us instead of starting its own.
761
+
762
+ shield() the await so caller cancellation (e.g. an `invoke()` task
763
+ that times out and is then cancelled by the user) does NOT propagate
764
+ into `_cleanup` — interrupting cleanup mid-flight would leak the
765
+ subprocess and wedge state in DISPOSING.
766
+ """
767
+ existing = self._cleanup_task
768
+ if existing and not existing.done():
699
769
  try:
700
- await self._reader_task
701
- except (asyncio.CancelledError, Exception):
770
+ await asyncio.shield(existing)
771
+ except asyncio.CancelledError:
772
+ raise
773
+ except Exception:
702
774
  pass
703
- self._reader_task = None
775
+ return
776
+ self._state = _State.DISPOSING
777
+ task = asyncio.create_task(self._cleanup(
778
+ SuperDocError('Host process disconnected.', code=HOST_DISCONNECTED),
779
+ ))
780
+ self._cleanup_task = task
781
+ try:
782
+ await asyncio.shield(task)
783
+ except asyncio.CancelledError:
784
+ raise
785
+ except Exception:
786
+ pass
704
787
 
788
+ def _schedule_cleanup(self, error: SuperDocError) -> None:
789
+ """Fire-and-forget teardown from inside the reader task.
790
+
791
+ Why a separate task: `_cleanup` cancels and awaits `self._reader_task`.
792
+ Awaiting it from inside the reader itself would deadlock — so we punt
793
+ to a fresh task, and by the time it runs the reader has already
794
+ returned (so cancel+await is a no-op).
795
+
796
+ Synchronously flips state to DISPOSING so concurrent `invoke()` callers
797
+ observe the failed transport immediately rather than passing the
798
+ CONNECTED fast path and blocking on a future the dead reader can never
799
+ resolve until `watchdog_timeout_ms`.
800
+
801
+ Skips when `_stopping` is set: a graceful `dispose()` is already
802
+ tearing down, and a parallel cleanup task would race on
803
+ `_reject_all_pending` and `process.kill`.
804
+
805
+ Idempotent: if a cleanup is already in flight, subsequent errors are
806
+ dropped — the first one wins. Callers may observe completion via
807
+ `self._cleanup_task`.
808
+ """
809
+ if self._stopping:
810
+ return
811
+ if self._cleanup_task and not self._cleanup_task.done():
812
+ return
813
+ self._state = _State.DISPOSING
814
+ self._cleanup_task = asyncio.create_task(self._cleanup(error))
815
+
816
+ async def _cleanup(self, error: Optional[SuperDocError]) -> None:
817
+ """Cancel reader, kill process, reject pending, reset state.
818
+
819
+ Capture handles and flip user-visible state SYNCHRONOUSLY at the top
820
+ before any awaits. That way, even if cancellation arrives during
821
+ `process.wait()`, observers see a consistent "torn down" transport
822
+ (state DISCONNECTED, _process None, pending futures rejected) rather
823
+ than a half-disposed one. The async work below is best-effort
824
+ process reaping.
825
+ """
826
+ # Snapshot and clear before any await so concurrent callers see a
827
+ # fully torn-down transport from this point on.
828
+ reader_task = self._reader_task
705
829
  process = self._process
706
- if process:
707
- try:
708
- process.kill()
709
- except Exception:
710
- pass
711
- try:
712
- await asyncio.wait_for(process.wait(), timeout=2)
713
- except (asyncio.TimeoutError, Exception):
714
- pass
830
+ self._reader_task = None
715
831
  self._process = None
832
+ self._state = _State.DISCONNECTED
716
833
 
717
- if error:
834
+ if error is not None:
718
835
  self._reject_all_pending(error)
719
836
  else:
720
837
  # Dispose path — reject remaining with generic disconnect.
@@ -722,4 +839,31 @@ class AsyncHostTransport:
722
839
  SuperDocError('Host process was disposed.', code=HOST_DISCONNECTED),
723
840
  )
724
841
 
725
- self._state = _State.DISCONNECTED
842
+ try:
843
+ if reader_task and not reader_task.done():
844
+ reader_task.cancel()
845
+ try:
846
+ await reader_task
847
+ except (asyncio.CancelledError, Exception):
848
+ pass
849
+
850
+ if process:
851
+ try:
852
+ process.kill()
853
+ except Exception:
854
+ pass
855
+ try:
856
+ await asyncio.wait_for(process.wait(), timeout=2)
857
+ except (asyncio.TimeoutError, asyncio.CancelledError, Exception):
858
+ pass
859
+ finally:
860
+ # Release the task handle if we are the in-flight cleanup task,
861
+ # so introspection doesn't surface a stale done handle and the
862
+ # next teardown gets a fresh slot. Skip when called inline (e.g.
863
+ # from dispose) — that current task is not our cleanup task.
864
+ try:
865
+ current = asyncio.current_task()
866
+ except RuntimeError:
867
+ current = None
868
+ if current is not None and self._cleanup_task is current:
869
+ self._cleanup_task = None
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superdoc-sdk
3
- Version: 1.6.0.dev23
3
+ Version: 1.6.0.dev29
4
4
  Summary: SuperDoc SDK (CLI-backed)
5
5
  Author: SuperDoc
6
6
  License-Expression: AGPL-3.0
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
- Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev23; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
10
- Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev23; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
11
- Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev23; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
12
- Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev23; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
13
- Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev23; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
9
+ Requires-Dist: superdoc-sdk-cli-darwin-arm64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
10
+ Requires-Dist: superdoc-sdk-cli-darwin-x64==1.6.0.dev29; platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
11
+ Requires-Dist: superdoc-sdk-cli-linux-x64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
12
+ Requires-Dist: superdoc-sdk-cli-linux-arm64==1.6.0.dev29; platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")
13
+ Requires-Dist: superdoc-sdk-cli-windows-x64==1.6.0.dev29; platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")
14
14
 
15
15
  # superdoc-sdk
16
16
 
@@ -1,15 +1,15 @@
1
1
 
2
2
  [:platform_system == "Darwin" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")]
3
- superdoc-sdk-cli-darwin-arm64==1.6.0.dev23
3
+ superdoc-sdk-cli-darwin-arm64==1.6.0.dev29
4
4
 
5
5
  [:platform_system == "Darwin" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
6
- superdoc-sdk-cli-darwin-x64==1.6.0.dev23
6
+ superdoc-sdk-cli-darwin-x64==1.6.0.dev29
7
7
 
8
8
  [:platform_system == "Linux" and (platform_machine == "arm64" or platform_machine == "aarch64" or platform_machine == "ARM64")]
9
- superdoc-sdk-cli-linux-arm64==1.6.0.dev23
9
+ superdoc-sdk-cli-linux-arm64==1.6.0.dev29
10
10
 
11
11
  [:platform_system == "Linux" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
12
- superdoc-sdk-cli-linux-x64==1.6.0.dev23
12
+ superdoc-sdk-cli-linux-x64==1.6.0.dev29
13
13
 
14
14
  [:platform_system == "Windows" and (platform_machine == "x86_64" or platform_machine == "AMD64" or platform_machine == "amd64")]
15
- superdoc-sdk-cli-windows-x64==1.6.0.dev23
15
+ superdoc-sdk-cli-windows-x64==1.6.0.dev29