agentic-runner 3.29.0__tar.gz → 3.30.0__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 (81) hide show
  1. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/PKG-INFO +2 -2
  2. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/pyproject.toml +1 -1
  3. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/__init__.py +1 -1
  4. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/cli.py +57 -1
  5. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/service.py +23 -6
  6. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workstation.py +54 -5
  7. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/.gitignore +0 -0
  8. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/LICENSE +0 -0
  9. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/README.md +0 -0
  10. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/activities.py +0 -0
  11. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/attempts.py +0 -0
  12. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/auth_mode.py +0 -0
  13. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/build.py +0 -0
  14. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/callback.py +0 -0
  15. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/child_watcher.py +0 -0
  16. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/cli_floor.py +0 -0
  17. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/config.py +0 -0
  18. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/credentials.py +0 -0
  19. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/egress.py +0 -0
  20. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/harness_self_test.py +0 -0
  21. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/heartbeat_link.py +0 -0
  22. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/hooks.py +0 -0
  23. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/host_store.py +0 -0
  24. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/__init__.py +0 -0
  25. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  26. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  27. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  28. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  29. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  30. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  31. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  32. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  33. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  34. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  35. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  36. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/launch.py +0 -0
  37. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/lifecycle.py +0 -0
  38. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/llm_contracts.py +0 -0
  39. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/llm_proxy.py +0 -0
  40. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/mcp.py +0 -0
  41. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/message_store.py +0 -0
  42. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/oauth_connectors.py +0 -0
  43. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/organisation_name.py +0 -0
  44. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/pool_key.py +0 -0
  45. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/private_state.py +0 -0
  46. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/py.typed +0 -0
  47. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  48. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/registration.py +0 -0
  49. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/runner_limit.py +0 -0
  50. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/runtime/__init__.py +0 -0
  51. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  52. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/sealed_box.py +0 -0
  53. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/sign_in.py +0 -0
  54. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/skill.py +0 -0
  55. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
  56. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/__init__.py +0 -0
  57. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/__main__.py +0 -0
  58. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/control_plane.py +0 -0
  59. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/plugin.py +0 -0
  60. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  61. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/testing/workflow.py +0 -0
  62. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/tiny_http.py +0 -0
  63. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/triage_activities.py +0 -0
  64. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/usage_windows.py +0 -0
  65. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/user_sources.py +0 -0
  66. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/__init__.py +0 -0
  67. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  68. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
  69. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  70. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  71. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  72. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  73. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/command_policy.py +0 -0
  74. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  75. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  76. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/harness_logins.py +0 -0
  77. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  78. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  79. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  80. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/settings.py +0 -0
  81. {agentic_runner-3.29.0 → agentic_runner-3.30.0}/src/agentic_runner/workers/skills.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 3.29.0
3
+ Version: 3.30.0
4
4
  Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.12
8
- Requires-Dist: agentic-runner-contracts==3.29.0
8
+ Requires-Dist: agentic-runner-contracts==3.30.0
9
9
  Requires-Dist: cryptography<51,>=50
10
10
  Requires-Dist: httpx<1.0,>=0.27
11
11
  Requires-Dist: pydantic-settings<3.0,>=2.0
@@ -10,7 +10,7 @@ dependencies = [
10
10
  # Exactly this release's contracts: the two ship as one version, and an unpinned
11
11
  # requirement let `uv tool upgrade` keep an older contracts wheel the Runner cannot
12
12
  # import from (QTS-1473). `scripts/set-version.py` moves it with the version.
13
- "agentic-runner-contracts==3.29.0",
13
+ "agentic-runner-contracts==3.30.0",
14
14
  # Imported directly by `registration.py` (the Ed25519 identity that signs every
15
15
  # heartbeat); already present transitively through pyjwt[crypto], declared because
16
16
  # we import it.
@@ -7,6 +7,6 @@ stay on the platform; this package imports none of them, and a CI partition test
7
7
  (``tests/test_package_partition.py``) fails the build if that changes.
8
8
  """
9
9
 
10
- __version__ = "3.29.0"
10
+ __version__ = "3.30.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -11,7 +11,7 @@ namespace and queue this process was assigned. It is idempotent by state file --
11
11
  restart re-reads what is already there rather than registering a second Runner against
12
12
  the Organisation's cap.
13
13
 
14
- ``install | start | stop | status <name>``, ``credential set`` and ``list`` are the
14
+ ``install | start | stop | restart | status <name>``, ``credential set`` and ``list`` are the
15
15
  workstation Runner (PRD issue 47, ``workstation.py``): one login agent per Runner, managed
16
16
  by the user who hosts it, with no ``sudo`` anywhere. ``<name>`` is the Runner's, the one the
17
17
  console lists it under (QTS-1773); a machine with one Runner needs none (QTS-1774).
@@ -215,6 +215,17 @@ def main(argv: Sequence[str] | None = None) -> int:
215
215
  driven.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
216
216
  driven.add_argument("--root", default=None, type=Path)
217
217
 
218
+ restart = subcommands.add_parser(
219
+ "restart",
220
+ help="restart running workstation Runners on the agentic-runner installed now, as "
221
+ "after `uv tool upgrade`",
222
+ )
223
+ restart.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
224
+ which = restart.add_mutually_exclusive_group()
225
+ which.add_argument("--name", dest="name_flag", default=None, help=_INSTALLED_NAME_HELP)
226
+ which.add_argument("--all", action="store_true", help="every Runner running on this machine")
227
+ restart.add_argument("--root", default=None, type=Path)
228
+
218
229
  listed = subcommands.add_parser(
219
230
  "list", help="the workstation Runners installed on this machine, and which are running"
220
231
  )
@@ -368,6 +379,8 @@ def main(argv: Sequence[str] | None = None) -> int:
368
379
  return asyncio.run(service.run(attestation=_image_attestation(), install=_run_install()))
369
380
  if arguments.command in {"install", "start", "stop", "credential"}:
370
381
  return _workstation(arguments)
382
+ if arguments.command == "restart":
383
+ return _restart(arguments)
371
384
  if arguments.command == "list":
372
385
  for line in workstation.list_lines(arguments.root or workstation.default_root()):
373
386
  print(line)
@@ -462,6 +475,49 @@ def _workstation(arguments: argparse.Namespace) -> int:
462
475
  return 1
463
476
 
464
477
 
478
+ def _restart(arguments: argparse.Namespace) -> int:
479
+ """``restart <name> | --name <name> | --all``: exit 2 names the Runners installed here.
480
+
481
+ A stopped Runner is not started: it was stopped on purpose, and ``systemctl restart``
482
+ would start it where ``launchctl kickstart`` refuses to, so the two OSes would differ.
483
+ """
484
+
485
+ root = arguments.root or workstation.default_root()
486
+ name = arguments.name or arguments.name_flag
487
+ if arguments.all and name:
488
+ print("restart takes a name or --all, not both", file=sys.stderr)
489
+ return 2
490
+ try:
491
+ if arguments.all:
492
+ targets = [
493
+ workstation.RunnerPaths(root=root, name=installed)
494
+ for installed in workstation.installed_names(root)
495
+ ]
496
+ if not targets:
497
+ raise ValueError(f"no workstation Runner is installed under {root}")
498
+ elif name:
499
+ targets = [workstation.installed_runner(root, name)]
500
+ else:
501
+ targets = [workstation.only_runner(root, how="name one, or restart --all")]
502
+ except ValueError as error:
503
+ print(error, file=sys.stderr)
504
+ return 2
505
+ failed = False
506
+ for paths in targets:
507
+ if workstation.process_id(paths) is None:
508
+ print(f"{paths.name} is stopped; `agentic-runner start {paths.name}` starts it")
509
+ failed = failed or not arguments.all
510
+ continue
511
+ try:
512
+ pid, version = workstation.restart(paths)
513
+ except (FileNotFoundError, RuntimeError) as error:
514
+ print(f"{paths.name}: restart failed: {error}", file=sys.stderr)
515
+ failed = True
516
+ continue
517
+ print(f"restarted {paths.name}: pid {pid}, agentic-runner {version}")
518
+ return 1 if failed else 0
519
+
520
+
465
521
  def _image_attestation() -> HostAttestation:
466
522
  """What a container Runner says about itself, where issue 47 gave the workstation its
467
523
  own: the CLIs it found are what the registry serves and routing matches on."""
@@ -1301,14 +1301,17 @@ async def sign_in(which: str | None, *, store: KeyStore, method: str = OAUTH_TOK
1301
1301
  can = can_separate_uids()
1302
1302
  try:
1303
1303
  require_isolation_supported(mode, can_separate_uids=can)
1304
+ # `run` gives the settings this default; a sign-in must build the same ones, or a
1305
+ # workstation, whose environment sets only the control plane, is refused (QTS-2157).
1306
+ _default_callback_url(os.environ.get(CONTROL_PLANE_ENV, "").strip())
1304
1307
  settings = get_worker_settings()
1305
1308
  except RunnerRegistrationError as error:
1306
1309
  print(f"refusing to sign in ({error.reason}): {error}", file=sys.stderr)
1307
1310
  return 1
1308
- except ValidationError:
1311
+ except ValidationError as error:
1309
1312
  print(
1310
- "refusing to sign in: run this with the Runner's environment (`docker exec`, "
1311
- "`kubectl exec`, or on the workstation that runs it)",
1313
+ f"refusing to sign in: {_invalid_settings(error)}; run this with the Runner's "
1314
+ "environment (`docker exec`, `kubectl exec`, or on the workstation that runs it)",
1312
1315
  file=sys.stderr,
1313
1316
  )
1314
1317
  return 2
@@ -1336,6 +1339,22 @@ async def sign_in(which: str | None, *, store: KeyStore, method: str = OAUTH_TOK
1336
1339
  _SIGN_IN_CLIS: Final[Mapping[str, str]] = {"claude_code": "claude", "codex_cli": "codex"}
1337
1340
 
1338
1341
 
1342
+ def _default_callback_url(control_plane: str) -> None:
1343
+ # The control plane the activities call back is the one this process registered
1344
+ # with, unless the operator points them at an internal address explicitly.
1345
+ if control_plane:
1346
+ os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
1347
+
1348
+
1349
+ def _invalid_settings(error: ValidationError) -> str:
1350
+ """``INTERNAL_FASTAPI_BASE_URL (Field required)``, one per field, never the value."""
1351
+
1352
+ return ", ".join(
1353
+ f"{'.'.join(str(part) for part in detail['loc'])} ({detail['msg']})"
1354
+ for detail in error.errors(include_input=False, include_url=False)
1355
+ )
1356
+
1357
+
1339
1358
  async def run(
1340
1359
  *,
1341
1360
  http_client: httpx.AsyncClient | None = None,
@@ -1416,9 +1435,7 @@ async def _run(
1416
1435
  except (RunnerRegistrationError, UnsafeStateError) as error:
1417
1436
  print(f"refusing to start ({error.reason}): {error}", flush=True)
1418
1437
  return 1
1419
- # The control plane the activities call back is the one this process registered
1420
- # with, unless the operator points them at an internal address explicitly.
1421
- os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
1438
+ _default_callback_url(control_plane)
1422
1439
  settings = get_worker_settings()
1423
1440
  try:
1424
1441
  attestation = with_profile_acp_cli_kinds(with_acp_bridges(attestation, settings), settings)
@@ -1,7 +1,7 @@
1
1
  """The workstation Runner: one login agent per Runner (PRD issue 47, map ticket 23).
2
2
 
3
- ``agentic-runner install | start | stop | status <name>`` and ``list``. Everything a CI-runner
4
- precedent does on a developer machine and nothing a daemon would need:
3
+ ``agentic-runner install | start | stop | restart | status <name>`` and ``list``. Everything
4
+ a CI-runner precedent does on a developer machine and nothing a daemon would need:
5
5
 
6
6
  * **One process per Runner** (23 item 3), each with its own state directory, identity,
7
7
  namespace, Runner Token, hooks, credential store and Workspaces under ``<root>/<name>/``.
@@ -37,11 +37,12 @@ import shutil
37
37
  import subprocess
38
38
  import sys
39
39
  import tempfile
40
+ import time
40
41
  from collections.abc import Callable, Mapping, Sequence
41
42
  from dataclasses import dataclass
42
43
  from datetime import UTC, datetime
43
44
  from pathlib import Path
44
- from typing import Any, Final
45
+ from typing import Any, Final, Literal
45
46
  from xml.sax.saxutils import escape
46
47
 
47
48
  import httpx
@@ -91,11 +92,13 @@ __all__ = [
91
92
  "install",
92
93
  "install_channel",
93
94
  "installed_names",
95
+ "installed_runner",
94
96
  "list_lines",
95
97
  "locate_clis",
96
98
  "only_runner",
97
99
  "parse_acp_cli_kinds",
98
100
  "require_any_cli",
101
+ "restart",
99
102
  "run_environment",
100
103
  "run_process",
101
104
  "service_definition",
@@ -158,6 +161,16 @@ def installed_names(root: Path) -> list[str]:
158
161
  )
159
162
 
160
163
 
164
+ def installed_runner(root: Path, name: str) -> RunnerPaths:
165
+ """``<name>``, refused with the names installed here when it is not one of them."""
166
+
167
+ names = installed_names(root)
168
+ if name in names:
169
+ return RunnerPaths(root=root, name=name)
170
+ installed = ", ".join(names) if names else "none"
171
+ raise ValueError(f"no Runner named {name!r} is installed under {root}; installed: {installed}")
172
+
173
+
161
174
  def only_runner(root: Path, *, how: str = "name one") -> RunnerPaths:
162
175
  """The one Runner installed here, so a command needs no name (QTS-1774)."""
163
176
 
@@ -444,6 +457,7 @@ class ServiceDefinition:
444
457
  install: list[list[str]]
445
458
  start: list[list[str]]
446
459
  stop: list[list[str]]
460
+ restart: list[list[str]]
447
461
 
448
462
 
449
463
  def runner_command(name: str, root: Path) -> list[str]:
@@ -504,6 +518,9 @@ def _launch_agent(
504
518
  install=[],
505
519
  start=[["launchctl", "bootstrap", domain, str(plist)]],
506
520
  stop=[["launchctl", "bootout", f"{domain}/{name}"]],
521
+ # One call, not bootout then bootstrap: launchd refuses a bootstrap of a label it
522
+ # still holds, and it holds it until the old process has drained.
523
+ restart=[["launchctl", "kickstart", "-k", f"{domain}/{name}"]],
507
524
  )
508
525
 
509
526
 
@@ -546,6 +563,7 @@ def _systemd_user_unit(
546
563
  ],
547
564
  start=[[*systemctl, "enable", "--now", unit_name]],
548
565
  stop=[[*systemctl, "disable", "--now", unit_name]],
566
+ restart=[[*systemctl, "restart", unit_name]],
549
567
  )
550
568
 
551
569
 
@@ -599,6 +617,10 @@ def _logon_task(
599
617
  ["schtasks", "/End", "/TN", task_name],
600
618
  ["schtasks", "/Change", "/TN", task_name, "/DISABLE"],
601
619
  ],
620
+ restart=[
621
+ ["schtasks", "/End", "/TN", task_name],
622
+ ["schtasks", "/Run", "/TN", task_name],
623
+ ],
602
624
  )
603
625
 
604
626
 
@@ -837,9 +859,36 @@ def stop(paths: RunnerPaths, **driver: Any) -> None:
837
859
  _drive_service(paths, "stop", **driver)
838
860
 
839
861
 
862
+ def restart(
863
+ paths: RunnerPaths,
864
+ *,
865
+ timeout: float = 60.0,
866
+ sleep: Callable[[float], None] = time.sleep,
867
+ clock: Callable[[], float] = time.monotonic,
868
+ **driver: Any,
869
+ ) -> tuple[int, str]:
870
+ """Restart a running Runner and wait for its new process: ``(pid, version)``.
871
+
872
+ ``uv tool upgrade`` replaces the console script, not the process, so a login agent keeps
873
+ running the old version until this (QTS-2158). The process writes its version before
874
+ its pidfile, so a new pid means the version beside it is the new process's.
875
+ """
876
+
877
+ previous = process_id(paths)
878
+ _drive_service(paths, "restart", **driver)
879
+ deadline = clock() + timeout
880
+ while (pid := process_id(paths)) is None or pid == previous:
881
+ if clock() >= deadline:
882
+ raise RuntimeError(
883
+ f"no new process wrote {paths.pidfile} within {timeout:.0f}s; see {paths.log}"
884
+ )
885
+ sleep(0.5)
886
+ return pid, paths.started_version.read_text(encoding="utf-8").strip()
887
+
888
+
840
889
  def _drive_service(
841
890
  paths: RunnerPaths,
842
- verb: str,
891
+ verb: Literal["start", "stop", "restart"],
843
892
  *,
844
893
  platform: str = sys.platform,
845
894
  home: Path | None = None,
@@ -847,7 +896,7 @@ def _drive_service(
847
896
  ) -> None:
848
897
  settings = WorkstationSettings.load(paths)
849
898
  definition = service_definition(paths, settings, platform=platform, home=home)
850
- drive(definition.start if verb == "start" else definition.stop, run=run)
899
+ drive(getattr(definition, verb), run=run)
851
900
 
852
901
 
853
902
  def run_process(paths: RunnerPaths, *, login_agent: bool) -> int:
File without changes