python-delphi-lsp 2.3.3__py3-none-any.whl → 3.1.0__py3-none-any.whl

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.
delphi_lsp/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.3.3"
1
+ __version__ = "3.1.0"
delphi_lsp/agent_cache.py CHANGED
@@ -40,9 +40,13 @@ _STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
40
40
  _STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
41
41
  _START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
42
42
  _CACHE_REVISION_CHECK_INTERVAL_SECONDS = 3600.0
43
+ _WATCHER_READY_TIMEOUT_SECONDS = 1.0
44
+ _WATCHER_READY_POLL_MILLISECONDS = 50
43
45
  _WATCHED_SUFFIXES = frozenset(
44
46
  {".pas", ".pp", ".inc", ".dpr", ".dpk", ".dproj", ".cfg"}
45
47
  )
48
+ _PROCESS_START_LOCKS_GUARD = threading.Lock()
49
+ _PROCESS_START_LOCKS: dict[str, tuple[threading.Lock, int]] = {}
46
50
 
47
51
 
48
52
  def current_process_rss_bytes() -> int:
@@ -578,6 +582,30 @@ def _start_lock(root: str | Path, timeout: float):
578
582
  pass
579
583
 
580
584
 
585
+ @contextlib.contextmanager
586
+ def _process_start_lock(root: str | Path):
587
+ canonical = str(Path(root).resolve())
588
+ with _PROCESS_START_LOCKS_GUARD:
589
+ lock, users = _PROCESS_START_LOCKS.get(
590
+ canonical,
591
+ (threading.Lock(), 0),
592
+ )
593
+ _PROCESS_START_LOCKS[canonical] = (lock, users + 1)
594
+ try:
595
+ with lock:
596
+ yield
597
+ finally:
598
+ with _PROCESS_START_LOCKS_GUARD:
599
+ current_lock, current_users = _PROCESS_START_LOCKS[canonical]
600
+ if current_users == 1:
601
+ del _PROCESS_START_LOCKS[canonical]
602
+ else:
603
+ _PROCESS_START_LOCKS[canonical] = (
604
+ current_lock,
605
+ current_users - 1,
606
+ )
607
+
608
+
581
609
  def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> CacheClientResponse:
582
610
  try:
583
611
  with socket.create_connection(("127.0.0.1", metadata.port), timeout=2) as connection:
@@ -655,31 +683,55 @@ def _daemon_process_options() -> dict[str, object]:
655
683
 
656
684
 
657
685
  class _CacheService:
658
- def __init__(self, metadata: CacheMetadata) -> None:
686
+ def __init__(
687
+ self,
688
+ metadata: CacheMetadata,
689
+ *,
690
+ defer_context: bool = False,
691
+ ) -> None:
659
692
  self.metadata = metadata
660
693
  self._rss_baseline_bytes = current_process_rss_bytes()
661
- self.context = AgentContext.open(
662
- metadata.root,
663
- metadata.project_file or None,
664
- workers=metadata.workers,
665
- worker_memory_budget_bytes=metadata.max_memory_bytes,
666
- revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
667
- navigation_cache_dir=navigation_cache_path(
668
- metadata.root,
669
- create=True,
670
- ),
671
- )
694
+ self._context: AgentContext | None = None
695
+ self.context_ready = threading.Event()
696
+ self.watcher_ready = threading.Event()
672
697
  self.budget = CacheBudget(metadata.max_memory_bytes)
673
698
  self.stats = CacheStats()
674
699
  self.lock = threading.Lock()
675
700
  self.started = time.monotonic()
676
701
  self.last_activity = self.started
677
702
  self.cache_state = "warming"
678
- self.last_revision = self.context.workspace.workspace_revision
703
+ self.last_revision = ""
679
704
  self.last_budget = BudgetResult(0, 0.0, 0.0, False, False, False)
680
705
  self.prewarm_seconds = 0.0
706
+ self.startup_error = ""
681
707
  self.shutdown = threading.Event()
682
708
  self.prewarm_thread: threading.Thread | None = None
709
+ if not defer_context:
710
+ self._initialize_context()
711
+
712
+ @property
713
+ def context(self) -> AgentContext:
714
+ if self._context is None:
715
+ raise RuntimeError("Cache workspace is still being discovered.")
716
+ return self._context
717
+
718
+ def _initialize_context(self) -> AgentContext:
719
+ if self._context is None:
720
+ context = AgentContext.open(
721
+ self.metadata.root,
722
+ self.metadata.project_file or None,
723
+ workers=self.metadata.workers,
724
+ worker_memory_budget_bytes=self.metadata.max_memory_bytes,
725
+ revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
726
+ navigation_cache_dir=navigation_cache_path(
727
+ self.metadata.root,
728
+ create=True,
729
+ ),
730
+ )
731
+ self._context = context
732
+ self.last_revision = context.workspace.workspace_revision
733
+ self.context_ready.set()
734
+ return self._context
683
735
 
684
736
  def _measure_retained_bytes(self) -> int:
685
737
  process_rss = current_process_rss_bytes()
@@ -692,8 +744,9 @@ class _CacheService:
692
744
 
693
745
  def prewarm(self) -> None:
694
746
  started = time.monotonic()
747
+ context = self._initialize_context()
695
748
  try:
696
- self.last_revision = self.context.prewarm_navigation()
749
+ self.last_revision = context.prewarm_navigation()
697
750
  self.cache_state = "warm"
698
751
  except AgentProtocolError as error:
699
752
  if error.code != "project_required":
@@ -708,15 +761,24 @@ class _CacheService:
708
761
  self.stats.evictions += 1
709
762
  self.cache_state = "compact"
710
763
  self.prewarm_seconds = time.monotonic() - started
711
- self.stats.parallel_fallbacks = self.context.parallel_stats.fallbacks
764
+ self.stats.parallel_fallbacks = context.parallel_stats.fallbacks
712
765
 
713
766
  def start_prewarm(self) -> None:
714
767
  def run() -> None:
715
768
  try:
716
769
  with self.lock:
717
770
  self.prewarm()
718
- except Exception:
771
+ except Exception as error:
772
+ detail = _truncate_and_sanitize_startup_diagnostics(
773
+ f"{type(error).__name__}: {error}".encode(
774
+ "utf-8",
775
+ "replace",
776
+ )
777
+ )
778
+ self.startup_error = detail or type(error).__name__
719
779
  self.cache_state = "failed"
780
+ finally:
781
+ self.context_ready.set()
720
782
 
721
783
  self.prewarm_thread = threading.Thread(
722
784
  target=run,
@@ -740,7 +802,16 @@ class _CacheService:
740
802
  "Cache is still warming. Retry after cache status reports ready.",
741
803
  )
742
804
  if self.cache_state == "failed":
743
- raise CacheClientError("cache_failed", "Cache warm-up failed.")
805
+ detail = (
806
+ f" {self.startup_error}"
807
+ if self.startup_error
808
+ else ""
809
+ )
810
+ raise CacheClientError(
811
+ "cache_failed",
812
+ f"Cache workspace discovery or warm-up failed.{detail}",
813
+ )
814
+ self.watcher_ready.wait(timeout=_WATCHER_READY_TIMEOUT_SECONDS)
744
815
  with self.lock:
745
816
  self.last_activity = time.monotonic()
746
817
  before = self.last_revision
@@ -784,6 +855,12 @@ class _CacheService:
784
855
  def status(self) -> dict[str, object]:
785
856
  now = time.monotonic()
786
857
  idle = max(0.0, now - self.last_activity)
858
+ context = self._context
859
+ parallel_stats = (
860
+ context.parallel_stats
861
+ if context is not None
862
+ else None
863
+ )
787
864
  return {
788
865
  "pid": self.metadata.pid, "root": self.metadata.root, "project_file": self.metadata.project_file,
789
866
  "version": self.metadata.version, "uptime": now - self.started, "idle_seconds": idle,
@@ -795,15 +872,18 @@ class _CacheService:
795
872
  "cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
796
873
  "rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
797
874
  "workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
798
- "workers_effective": self.context.parallel_stats.effective_workers,
799
- "parallel_files_completed": self.context.parallel_stats.files_completed,
875
+ "workers_effective": parallel_stats.effective_workers if parallel_stats else 0,
876
+ "parallel_files_completed": parallel_stats.files_completed if parallel_stats else 0,
800
877
  "prewarm_seconds": self.prewarm_seconds,
801
- "parallel_seconds": self.context.parallel_stats.elapsed_seconds,
878
+ "parallel_seconds": parallel_stats.elapsed_seconds if parallel_stats else 0.0,
802
879
  "parallel_fallbacks": self.stats.parallel_fallbacks,
803
- "navigation_disk_hits": self.context.navigation_disk_hits,
804
- "navigation_disk_misses": self.context.navigation_disk_misses,
880
+ "navigation_disk_hits": context.navigation_disk_hits if context else 0,
881
+ "navigation_disk_misses": context.navigation_disk_misses if context else 0,
882
+ "cpg_cache_entries": context.cpg_cache_entries if context else 0,
883
+ "cpg_cache_bytes": context.cpg_cache_bytes if context else 0,
805
884
  "idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
806
885
  "workspace_revision": self.last_revision,
886
+ "startup_error": self.startup_error,
807
887
  }
808
888
 
809
889
 
@@ -816,7 +896,9 @@ def watch_workspace_changes(
816
896
  *,
817
897
  stop_event: threading.Event,
818
898
  on_change: Callable[[], None],
899
+ on_ready: Callable[[], None] | None = None,
819
900
  ) -> None:
901
+ ready = False
820
902
  try:
821
903
  for changes in watch(
822
904
  root,
@@ -824,20 +906,33 @@ def watch_workspace_changes(
824
906
  stop_event=stop_event,
825
907
  debounce=50,
826
908
  step=20,
909
+ rust_timeout=_WATCHER_READY_POLL_MILLISECONDS,
910
+ yield_on_timeout=True,
827
911
  recursive=True,
828
912
  raise_interrupt=False,
829
913
  ):
914
+ if not ready:
915
+ ready = True
916
+ if on_ready is not None:
917
+ on_ready()
830
918
  if changes:
831
919
  on_change()
832
920
  except (OSError, RuntimeError):
833
921
  on_change()
922
+ finally:
923
+ if not ready and on_ready is not None:
924
+ on_ready()
834
925
 
835
926
 
836
927
  def _watch_workspace(service: _CacheService) -> None:
928
+ service.context_ready.wait()
929
+ if service.shutdown.is_set() or service._context is None:
930
+ return
837
931
  watch_workspace_changes(
838
932
  service.metadata.root,
839
933
  stop_event=service.shutdown,
840
- on_change=service.context.invalidate_revision_cache,
934
+ on_change=service._context.invalidate_revision_cache,
935
+ on_ready=service.watcher_ready.set,
841
936
  )
842
937
 
843
938
 
@@ -888,7 +983,7 @@ def run_cache_daemon(
888
983
  )
889
984
  watcher: threading.Thread | None = None
890
985
  try:
891
- service = _CacheService(metadata)
986
+ service = _CacheService(metadata, defer_context=True)
892
987
  _write_metadata(metadata)
893
988
  watcher = threading.Thread(
894
989
  target=_watch_workspace,
@@ -900,10 +995,7 @@ def run_cache_daemon(
900
995
  service.start_prewarm()
901
996
  while (
902
997
  not service.shutdown.is_set()
903
- and (
904
- service.cache_state == "warming"
905
- or time.monotonic() - service.last_activity < idle_timeout
906
- )
998
+ and time.monotonic() - service.last_activity < idle_timeout
907
999
  ):
908
1000
  try:
909
1001
  connection, _ = listener.accept()
@@ -915,10 +1007,10 @@ def run_cache_daemon(
915
1007
  finally:
916
1008
  if "service" in locals():
917
1009
  service.shutdown.set()
918
- if watcher is not None:
919
- watcher.join(timeout=2.0)
920
1010
  listener.close()
921
1011
  _remove_metadata_if_owned(metadata)
1012
+ if watcher is not None:
1013
+ watcher.join(timeout=2.0)
922
1014
 
923
1015
 
924
1016
  def _start_cache_unlocked(
@@ -972,6 +1064,8 @@ def _start_cache_unlocked(
972
1064
  process = subprocess.Popen(command, **options)
973
1065
  deadline = time.monotonic() + startup_timeout
974
1066
  startup_ready = False
1067
+ timed_out = False
1068
+ return_code: int | None = None
975
1069
  try:
976
1070
  while time.monotonic() < deadline:
977
1071
  metadata = _read_metadata(canonical)
@@ -985,10 +1079,12 @@ def _start_cache_unlocked(
985
1079
  if process.poll() is not None:
986
1080
  break
987
1081
  time.sleep(0.05)
988
- if process.poll() is None:
1082
+ return_code = process.poll()
1083
+ timed_out = return_code is None
1084
+ if timed_out:
989
1085
  process.kill()
990
1086
  with contextlib.suppress(Exception):
991
- process.wait()
1087
+ return_code = process.wait()
992
1088
  metadata = _read_metadata(canonical)
993
1089
  if metadata and metadata.pid == process.pid:
994
1090
  _remove_metadata_if_owned(metadata)
@@ -996,9 +1092,28 @@ def _start_cache_unlocked(
996
1092
  raw = _read_startup_tail(diagnostics)
997
1093
  message = _truncate_and_sanitize_startup_diagnostics(raw)
998
1094
  base = "Cache daemon did not become ready."
1095
+ if timed_out:
1096
+ reason = (
1097
+ f" Startup timed out after {startup_timeout:.1f}s before "
1098
+ "readiness metadata was published."
1099
+ )
1100
+ else:
1101
+ reason = (
1102
+ f" Child process exited with code {return_code} before "
1103
+ "readiness metadata was published."
1104
+ )
999
1105
  if message:
1000
- raise CacheClientError("startup_failed", f"{base} {message}")
1001
- raise CacheClientError("startup_failed", base)
1106
+ raise CacheClientError(
1107
+ "startup_failed",
1108
+ f"{base}{reason} Child stderr: {message}",
1109
+ )
1110
+ raise CacheClientError(
1111
+ "startup_failed",
1112
+ (
1113
+ f"{base}{reason} No child diagnostics were emitted. "
1114
+ f"Python executable: {sys.executable}. Workspace: {canonical}."
1115
+ ),
1116
+ )
1002
1117
  finally:
1003
1118
  if not startup_ready:
1004
1119
  if process.poll() is None:
@@ -1022,15 +1137,16 @@ def start_cache(
1022
1137
  raise ValueError("idle_timeout must be greater than zero.")
1023
1138
  if not math.isfinite(startup_timeout) or startup_timeout <= 0:
1024
1139
  raise ValueError("startup_timeout must be greater than zero.")
1025
- with _start_lock(root, startup_timeout):
1026
- return _start_cache_unlocked(
1027
- root,
1028
- project_file=project_file,
1029
- max_memory_bytes=max_memory_bytes,
1030
- workers=workers,
1031
- idle_timeout=idle_timeout,
1032
- startup_timeout=startup_timeout,
1033
- )
1140
+ with _process_start_lock(root):
1141
+ with _start_lock(root, startup_timeout):
1142
+ return _start_cache_unlocked(
1143
+ root,
1144
+ project_file=project_file,
1145
+ max_memory_bytes=max_memory_bytes,
1146
+ workers=workers,
1147
+ idle_timeout=idle_timeout,
1148
+ startup_timeout=startup_timeout,
1149
+ )
1034
1150
 
1035
1151
 
1036
1152
  def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
delphi_lsp/agent_cli.py CHANGED
@@ -24,8 +24,17 @@ from .agent_cache import (
24
24
  watch_workspace_changes,
25
25
  )
26
26
  from .agent_layers import build_codebase_index, layer_payload, render_layer
27
- from .agent_protocol import AgentProtocolError, SUPPORTED_ACTIONS, SUPPORTED_DETAILS, SUPPORTED_RELATIONS
27
+ from .agent_protocol import (
28
+ SCHEMA_VERSION,
29
+ AgentProtocolError,
30
+ SUPPORTED_ACTIONS,
31
+ SUPPORTED_DETAILS,
32
+ SUPPORTED_DIRECTIONS,
33
+ SUPPORTED_GRAPHS,
34
+ SUPPORTED_RELATIONS,
35
+ )
28
36
  from .agent_templates import install_opencode_support, install_skill
37
+ from .agent_wiki import WikiExportError, export_okf_wiki
29
38
  from .parallel_outline import ParallelOutlineError, parse_worker_setting
30
39
 
31
40
 
@@ -84,6 +93,19 @@ def build_parser() -> argparse.ArgumentParser:
84
93
  index.add_argument("--workers", type=parse_worker_setting, default=0)
85
94
  index.set_defaults(func=_index)
86
95
 
96
+ wiki = subcommands.add_parser("wiki", help="Export a portable Markdown knowledge wiki.")
97
+ wiki_commands = wiki.add_subparsers(dest="wiki_command", required=True)
98
+ wiki_export = wiki_commands.add_parser(
99
+ "export",
100
+ help="Export all indexed codebase knowledge as an Open Knowledge Format bundle.",
101
+ )
102
+ wiki_export.add_argument("--root", type=Path, default=Path("."))
103
+ wiki_export.add_argument("--project-file", type=Path)
104
+ wiki_export.add_argument("--out", type=Path, default=Path(".delphi-lsp") / "wiki")
105
+ wiki_export.add_argument("--workers", type=parse_worker_setting, default=0)
106
+ wiki_export.add_argument("--force", action="store_true")
107
+ wiki_export.set_defaults(func=_wiki_export)
108
+
87
109
  skill = subcommands.add_parser("skill", help="Install agent skill templates.")
88
110
  skill_commands = skill.add_subparsers(dest="skill_command", required=True)
89
111
  skill_install = skill_commands.add_parser("install", help="Install .agents skill.")
@@ -108,13 +130,13 @@ def build_parser() -> argparse.ArgumentParser:
108
130
  )
109
131
  opencode_install.set_defaults(func=_opencode_install)
110
132
 
111
- worker = subcommands.add_parser("worker", help="Serve Protocol v2 NDJSON requests.")
133
+ worker = subcommands.add_parser("worker", help="Serve Protocol v3 NDJSON requests.")
112
134
  worker.add_argument("--root", type=Path, required=True)
113
135
  worker.add_argument("--project-file", type=Path)
114
136
  worker.add_argument("--workers", type=parse_worker_setting, default=0)
115
137
  worker.set_defaults(func=_worker)
116
138
 
117
- cache = subcommands.add_parser("cache", help="Manage the shared Protocol v2 cache daemon.")
139
+ cache = subcommands.add_parser("cache", help="Manage the shared Protocol v3 cache daemon.")
118
140
  cache_commands = cache.add_subparsers(dest="cache_command", required=True)
119
141
  cache_start = cache_commands.add_parser("start", help="Start the cache daemon if needed.")
120
142
  _add_cache_start_arguments(cache_start)
@@ -141,6 +163,9 @@ def build_parser() -> argparse.ArgumentParser:
141
163
  query.add_argument("--project-id", default="")
142
164
  query.add_argument("--detail", choices=SUPPORTED_DETAILS, default="summary")
143
165
  query.add_argument("--relation", choices=SUPPORTED_RELATIONS)
166
+ query.add_argument("--graph", choices=SUPPORTED_GRAPHS, default="full")
167
+ query.add_argument("--direction", choices=SUPPORTED_DIRECTIONS, default="out")
168
+ query.add_argument("--depth", type=_cpg_depth, default=4)
144
169
  query.add_argument("--cursor", default="")
145
170
  query.add_argument("--max-items", type=_max_items, default=12)
146
171
  query.add_argument("--max-chars", type=_max_chars, default=12000)
@@ -179,6 +204,13 @@ def _positive_integer(value: str) -> int:
179
204
  return parsed
180
205
 
181
206
 
207
+ def _cpg_depth(value: str) -> int:
208
+ parsed = int(value)
209
+ if not 1 <= parsed <= 16:
210
+ raise argparse.ArgumentTypeError("depth must be between 1 and 16")
211
+ return parsed
212
+
213
+
182
214
  def _max_items(value: str) -> int:
183
215
  parsed = int(value)
184
216
  if not 1 <= parsed <= 50:
@@ -270,7 +302,26 @@ def _index(args: argparse.Namespace) -> None:
270
302
  print(args.out)
271
303
 
272
304
 
305
+ def _wiki_export(args: argparse.Namespace) -> None:
306
+ args.root = _workspace_root(args.root)
307
+ output = args.out
308
+ if not output.is_absolute():
309
+ output = args.root / output
310
+ try:
311
+ result = export_okf_wiki(
312
+ args.root,
313
+ output,
314
+ project_file=args.project_file,
315
+ workers=args.workers,
316
+ force=args.force,
317
+ )
318
+ except WikiExportError as error:
319
+ raise _CliError("wiki_export_failed", str(error)) from None
320
+ _write_json(result.to_mapping())
321
+
322
+
273
323
  def _skill_install(args: argparse.Namespace) -> None:
324
+ _require_install_directory(args.target)
274
325
  try:
275
326
  skill_path = install_skill(args.target, force=args.force)
276
327
  except FileExistsError as error:
@@ -279,6 +330,7 @@ def _skill_install(args: argparse.Namespace) -> None:
279
330
 
280
331
 
281
332
  def _opencode_install(args: argparse.Namespace) -> None:
333
+ _require_install_directory(args.target)
282
334
  try:
283
335
  skill_path, plugin_path, agent_path = install_opencode_support(
284
336
  args.target,
@@ -293,6 +345,12 @@ def _opencode_install(args: argparse.Namespace) -> None:
293
345
  print(agent_path)
294
346
 
295
347
 
348
+ def _require_install_directory(target: str | Path) -> None:
349
+ path = Path(target).expanduser()
350
+ if path.exists() and not path.is_dir():
351
+ raise _CliError("io_error", f"Install target is not a directory: {path}")
352
+
353
+
296
354
  def _worker(args: argparse.Namespace) -> None:
297
355
  args.root = _workspace_root(args.root)
298
356
  context = _open_worker_context(args.root, args.project_file, args.workers)
@@ -430,13 +488,23 @@ def _query(args: argparse.Namespace) -> int:
430
488
  if args.value:
431
489
  if args.action in {"find", "metrics"}:
432
490
  request["query"] = args.value
433
- elif args.action in {"focus", "inspect", "trace"}:
491
+ elif args.action in {"focus", "inspect", "trace", "cpg"}:
434
492
  request["target_id"] = args.value
435
493
  else:
436
494
  sys.stderr.write(f"cache_error:invalid_request: {args.action} does not accept a value.\n")
437
495
  sys.stderr.flush()
438
496
  return 1
439
- for argument, field in (("project_id", "project_id"), ("detail", "detail"), ("relation", "relation"), ("cursor", "cursor"), ("max_items", "max_items"), ("max_chars", "max_chars")):
497
+ for argument, field in (
498
+ ("project_id", "project_id"),
499
+ ("detail", "detail"),
500
+ ("relation", "relation"),
501
+ ("graph", "graph"),
502
+ ("direction", "direction"),
503
+ ("depth", "depth"),
504
+ ("cursor", "cursor"),
505
+ ("max_items", "max_items"),
506
+ ("max_chars", "max_chars"),
507
+ ):
440
508
  value = getattr(args, argument)
441
509
  if value is not None:
442
510
  request[field] = value
@@ -521,7 +589,7 @@ def _serve_worker(context: AgentContext, input_stream: BinaryIO, output_stream:
521
589
  message = _worker_error("internal_error", _INTERNAL_ERROR_MESSAGE)
522
590
  _write_worker_message(output_stream, message)
523
591
  def _worker_error(code: str, message: str) -> dict[str, object]:
524
- return {"schema": 2, "error": {"code": code, "message": message}}
592
+ return {"schema": SCHEMA_VERSION, "error": {"code": code, "message": message}}
525
593
 
526
594
 
527
595
  def _write_worker_message(output_stream: BinaryIO, message: object) -> None: