outfitter-dispatch 0.2.0__tar.gz → 0.2.1__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.
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/PKG-INFO +1 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/skills/dispatch/SKILL.md +3 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/pyproject.toml +1 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/client.py +5 -2
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/transport.py +12 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/derive_cli.py +1 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/handlers.py +11 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/test_client.py +12 -1
- outfitter_dispatch-0.2.1/tests/client/test_transport.py +63 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/test_handlers.py +21 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/surfaces/test_derive_cli.py +20 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/uv.lock +1 -1
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/.gitignore +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/AGENTS.md +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/README.md +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/justfile +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/.codex-plugin/plugin.json +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/.mcp.json +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/README.md +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/assets/dispatch.svg +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/skills/README.md +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/skills/dm/SKILL.md +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/cli.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/errors.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/events.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/models.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/router.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/config.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/context.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/derive_mcp.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/errors.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/examples.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/execute.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/op.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/registry.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/models.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/new_config.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/ops.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/queue.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/reactor.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/scheduler.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/trigger_handlers.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/triggers.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/__main__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/control.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/host.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/lifecycle.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/supervisor.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/doctor.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/models.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/store.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/cli.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/mcp.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/version.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/conftest.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/test_events.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/test_models.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/client/test_router.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/conftest.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/contracts/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/contracts/test_contracts.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/test_examples.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/test_new_config.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/test_trigger_handlers.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/core/test_triggers.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/daemon/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/daemon/test_control.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/daemon/test_lifecycle.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/daemon/test_supervisor.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/fakes.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/_drive.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/conftest.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/test_app_server.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/test_daemon_e2e.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/test_lifecycle_e2e.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/registry/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/registry/test_store.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/registry/test_triggers_store.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/surfaces/__init__.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/surfaces/test_derive_mcp.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/surfaces/test_mcp_routing.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/surfaces/test_parity.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/test_doctor.py +0 -0
- {outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: outfitter-dispatch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Local control plane for orchestrating Codex agent lanes over the Codex App Server.
|
|
5
5
|
Project-URL: Homepage, https://github.com/outfitter-dev/dispatch
|
|
6
6
|
Project-URL: Repository, https://github.com/outfitter-dev/dispatch
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/skills/dispatch/SKILL.md
RENAMED
|
@@ -89,7 +89,9 @@ processes and there is no cross-process write interlock.
|
|
|
89
89
|
|
|
90
90
|
Attach is bounded: if the app-server stalls, the underlying `thread/resume`
|
|
91
91
|
times out (~15s) and `attach` fails with a clear `app_server` error, registering
|
|
92
|
-
no lane. There is no half-attached state to clean up.
|
|
92
|
+
no lane. There is no half-attached state to clean up. Large persisted histories
|
|
93
|
+
are supported; attach should not fail only because a resumed thread has more than
|
|
94
|
+
64 KiB of turns.
|
|
93
95
|
|
|
94
96
|
## Discover Sessions
|
|
95
97
|
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/client.py
RENAMED
|
@@ -15,7 +15,7 @@ from typing import Self
|
|
|
15
15
|
|
|
16
16
|
from pydantic import BaseModel
|
|
17
17
|
|
|
18
|
-
from .errors import
|
|
18
|
+
from .errors import ClientError, TransportError
|
|
19
19
|
from .events import LaneEvent
|
|
20
20
|
from .models import (
|
|
21
21
|
ApprovalPolicy,
|
|
@@ -101,9 +101,12 @@ class AppServerClient:
|
|
|
101
101
|
if message is None:
|
|
102
102
|
break
|
|
103
103
|
self._router.handle(message)
|
|
104
|
-
except
|
|
104
|
+
except ClientError as exc:
|
|
105
105
|
self._router.fail_all(exc)
|
|
106
106
|
return
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
self._router.fail_all(TransportError(f"app-server read loop failed: {exc}"))
|
|
109
|
+
return
|
|
107
110
|
self._router.fail_all(TransportError("app-server stream closed (stdout EOF)"))
|
|
108
111
|
finally:
|
|
109
112
|
self._closed_event.set() # wake supervisors waiting on wait_closed()
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/transport.py
RENAMED
|
@@ -18,6 +18,8 @@ from typing import Protocol
|
|
|
18
18
|
|
|
19
19
|
from .errors import ProtocolError, TransportError
|
|
20
20
|
|
|
21
|
+
DEFAULT_STDIO_READ_LIMIT = 64 * 1024 * 1024
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
class Transport(Protocol):
|
|
23
25
|
"""Bidirectional JSON message channel to one app-server connection."""
|
|
@@ -38,9 +40,11 @@ class StdioTransport:
|
|
|
38
40
|
self,
|
|
39
41
|
argv: Sequence[str] = ("codex", "app-server", "--listen", "stdio://"),
|
|
40
42
|
env: Mapping[str, str] | None = None,
|
|
43
|
+
read_limit: int = DEFAULT_STDIO_READ_LIMIT,
|
|
41
44
|
) -> None:
|
|
42
45
|
self._argv = tuple(argv)
|
|
43
46
|
self._env = dict(env) if env is not None else None
|
|
47
|
+
self._read_limit = read_limit
|
|
44
48
|
self._proc: asyncio.subprocess.Process | None = None
|
|
45
49
|
self._stderr_tail: deque[str] = deque(maxlen=50)
|
|
46
50
|
self._stderr_task: asyncio.Task[None] | None = None
|
|
@@ -53,6 +57,7 @@ class StdioTransport:
|
|
|
53
57
|
stdout=asyncio.subprocess.PIPE,
|
|
54
58
|
stderr=asyncio.subprocess.PIPE,
|
|
55
59
|
env=self._env,
|
|
60
|
+
limit=self._read_limit,
|
|
56
61
|
)
|
|
57
62
|
except OSError as exc: # binary missing / not executable
|
|
58
63
|
raise TransportError(f"failed to spawn {self._argv[0]}: {exc}") from exc
|
|
@@ -82,7 +87,13 @@ class StdioTransport:
|
|
|
82
87
|
proc = self._require_proc()
|
|
83
88
|
assert proc.stdout is not None
|
|
84
89
|
while True:
|
|
85
|
-
|
|
90
|
+
try:
|
|
91
|
+
raw = await proc.stdout.readline()
|
|
92
|
+
except ValueError as exc:
|
|
93
|
+
raise ProtocolError(
|
|
94
|
+
"app-server JSONL line exceeded dispatch's stdio reader limit "
|
|
95
|
+
f"({self._read_limit} bytes)"
|
|
96
|
+
) from exc
|
|
86
97
|
if raw == b"": # EOF: process closed stdout (crashed or exited)
|
|
87
98
|
return None
|
|
88
99
|
text = raw.decode(errors="replace").strip()
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/derive_cli.py
RENAMED
|
@@ -125,7 +125,7 @@ def _op_command(
|
|
|
125
125
|
def command(**kwargs: object) -> None:
|
|
126
126
|
json_requested = bool(kwargs.pop("json", False))
|
|
127
127
|
if op.intent == "destroy":
|
|
128
|
-
typer.confirm(f"Run destroy op {op.id!r}?", abort=True)
|
|
128
|
+
typer.confirm(f"Run destroy op {op.id!r}?", abort=True, err=json_requested)
|
|
129
129
|
result = invoke(op.id, dict(kwargs))
|
|
130
130
|
render(op, result)
|
|
131
131
|
_ignore_json(json_requested)
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/handlers.py
RENAMED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
|
|
12
|
+
from outfitter.dispatch.client.errors import AppServerError as ClientAppServerError
|
|
12
13
|
from outfitter.dispatch.client.errors import ClientError
|
|
13
14
|
from outfitter.dispatch.client.models import SandboxPolicy, ThreadGoal, ThreadInfo, ThreadSandbox
|
|
14
15
|
from outfitter.dispatch.contracts.context import Ctx
|
|
@@ -596,12 +597,21 @@ async def discover(inp: DiscoverInput, ctx: Ctx) -> Discovery:
|
|
|
596
597
|
async def archive(inp: LaneInput, ctx: Ctx) -> LaneRef:
|
|
597
598
|
lane = await _resolve(ctx, inp.lane)
|
|
598
599
|
_require_writable(lane) # archiving mutates the shared thread store (ADR-0005)
|
|
599
|
-
|
|
600
|
+
try:
|
|
601
|
+
await ctx.client.thread_archive(lane.id)
|
|
602
|
+
except ClientAppServerError as exc:
|
|
603
|
+
if not _is_no_rollout_archive_error(exc):
|
|
604
|
+
raise
|
|
605
|
+
ctx.log.info("lane.archive_local_no_rollout", lane=lane.id)
|
|
600
606
|
await ctx.registry.update_lane_status(lane.id, "archived")
|
|
601
607
|
await ctx.registry.log_action("archive", lane=lane.id)
|
|
602
608
|
return _ref(await ctx.registry.get_lane(lane.id))
|
|
603
609
|
|
|
604
610
|
|
|
611
|
+
def _is_no_rollout_archive_error(exc: ClientAppServerError) -> bool:
|
|
612
|
+
return exc.code == -32600 and "no rollout found" in exc.message.lower()
|
|
613
|
+
|
|
614
|
+
|
|
605
615
|
async def status(inp: StatusInput, ctx: Ctx) -> StatusOutput:
|
|
606
616
|
lanes = await ctx.registry.list_lanes()
|
|
607
617
|
triggers = await ctx.registry.list_triggers()
|
|
@@ -7,7 +7,7 @@ import asyncio
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from outfitter.dispatch.client.client import AppServerClient
|
|
10
|
-
from outfitter.dispatch.client.errors import AppServerError, TransportError
|
|
10
|
+
from outfitter.dispatch.client.errors import AppServerError, ProtocolError, TransportError
|
|
11
11
|
from outfitter.dispatch.client.events import TurnCompleted
|
|
12
12
|
|
|
13
13
|
from .conftest import FakeTransport, Responder
|
|
@@ -189,6 +189,17 @@ async def test_eof_fails_pending_request_with_transport_error(
|
|
|
189
189
|
await task
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
async def test_reader_error_fails_pending_request() -> None:
|
|
193
|
+
class BrokenTransport(FakeTransport):
|
|
194
|
+
async def receive(self) -> dict[str, object] | None:
|
|
195
|
+
raise ProtocolError("line too large")
|
|
196
|
+
|
|
197
|
+
c = AppServerClient(BrokenTransport())
|
|
198
|
+
await c.start()
|
|
199
|
+
with pytest.raises(ProtocolError, match="line too large"):
|
|
200
|
+
await c.thread_read("L1")
|
|
201
|
+
|
|
202
|
+
|
|
192
203
|
async def test_events_stream_yields_projected_lane_event(
|
|
193
204
|
client: tuple[AppServerClient, FakeTransport],
|
|
194
205
|
) -> None:
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unit tests for the stdio App Server transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from outfitter.dispatch.client.transport import StdioTransport
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_stdio_transport_sets_reader_limit(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
17
|
+
captured: dict[str, Any] = {}
|
|
18
|
+
|
|
19
|
+
class DummyProcess:
|
|
20
|
+
returncode = None
|
|
21
|
+
stdin = None
|
|
22
|
+
stdout = None
|
|
23
|
+
stderr = None
|
|
24
|
+
|
|
25
|
+
async def fake_create_subprocess_exec(*argv: str, **kwargs: Any) -> DummyProcess:
|
|
26
|
+
captured["argv"] = argv
|
|
27
|
+
captured["kwargs"] = kwargs
|
|
28
|
+
return DummyProcess()
|
|
29
|
+
|
|
30
|
+
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
|
|
31
|
+
|
|
32
|
+
transport = StdioTransport(argv=("codex", "app-server"), read_limit=123_456)
|
|
33
|
+
await transport.start()
|
|
34
|
+
|
|
35
|
+
assert captured["argv"] == ("codex", "app-server")
|
|
36
|
+
assert captured["kwargs"]["limit"] == 123_456
|
|
37
|
+
assert captured["kwargs"]["stdin"] is asyncio.subprocess.PIPE
|
|
38
|
+
assert captured["kwargs"]["stdout"] is asyncio.subprocess.PIPE
|
|
39
|
+
assert captured["kwargs"]["stderr"] is asyncio.subprocess.PIPE
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_stdio_transport_accepts_sequence_argv() -> None:
|
|
43
|
+
argv: Sequence[str] = ("codex", "app-server", "--listen", "stdio://")
|
|
44
|
+
transport = StdioTransport(argv=argv)
|
|
45
|
+
assert transport.returncode is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def test_stdio_transport_reads_large_jsonl_line() -> None:
|
|
49
|
+
payload = {"id": 1, "result": {"text": "x" * 70_000}}
|
|
50
|
+
script = (
|
|
51
|
+
"import json, sys; "
|
|
52
|
+
f"sys.stdout.write({json.dumps(json.dumps(payload))} + '\\n'); "
|
|
53
|
+
"sys.stdout.flush()"
|
|
54
|
+
)
|
|
55
|
+
transport = StdioTransport(
|
|
56
|
+
argv=(sys.executable, "-c", script),
|
|
57
|
+
read_limit=128 * 1024,
|
|
58
|
+
)
|
|
59
|
+
await transport.start()
|
|
60
|
+
try:
|
|
61
|
+
assert await transport.receive() == payload
|
|
62
|
+
finally:
|
|
63
|
+
await transport.close()
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
import pytest
|
|
10
10
|
import pytest_asyncio
|
|
11
11
|
|
|
12
|
+
from outfitter.dispatch.client.errors import AppServerError as ClientAppServerError
|
|
12
13
|
from outfitter.dispatch.client.errors import TransportError
|
|
13
14
|
from outfitter.dispatch.client.models import ThreadGoal, ThreadInfo, ThreadStatus
|
|
14
15
|
from outfitter.dispatch.contracts.errors import AppServerError, AuthorityError, ValidationError
|
|
@@ -543,6 +544,26 @@ async def test_roster_then_archive_flips_status(store: Registry) -> None:
|
|
|
543
544
|
assert len(everything.lanes) == 1
|
|
544
545
|
|
|
545
546
|
|
|
547
|
+
class _NoRolloutArchiveClient(FakeLaneClient):
|
|
548
|
+
async def thread_archive(self, thread_id: str) -> None:
|
|
549
|
+
self._record("thread_archive", thread_id=thread_id)
|
|
550
|
+
raise ClientAppServerError(-32600, f"no rollout found for thread id {thread_id}")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
async def test_archive_no_rollout_lane_marks_local_lane_archived(store: Registry) -> None:
|
|
554
|
+
client = _NoRolloutArchiveClient()
|
|
555
|
+
ctx = make_ctx(store, client)
|
|
556
|
+
await handlers.new_lane(NewInput(name="smoke", ephemeral=True, send=False), ctx)
|
|
557
|
+
|
|
558
|
+
archived = await handlers.archive(LaneInput(lane="lane-1"), ctx)
|
|
559
|
+
|
|
560
|
+
assert archived.status == "archived"
|
|
561
|
+
assert (await handlers.roster(RosterInput(), ctx)).lanes == []
|
|
562
|
+
everything = await handlers.roster(RosterInput(include_archived=True), ctx)
|
|
563
|
+
assert [lane.id for lane in everything.lanes] == ["lane-1"]
|
|
564
|
+
assert any(name == "thread_archive" for name, _ in client.calls)
|
|
565
|
+
|
|
566
|
+
|
|
546
567
|
async def test_status_and_log_reflect_activity(store: Registry) -> None:
|
|
547
568
|
ctx = make_ctx(store)
|
|
548
569
|
await handlers.open_lane(OpenInput(name="one"), ctx)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
|
|
5
7
|
import typer
|
|
6
8
|
from typer.testing import CliRunner
|
|
7
9
|
|
|
@@ -149,6 +151,24 @@ def test_lane_archive_prompts_for_confirmation() -> None:
|
|
|
149
151
|
assert confirmed.exit_code == 0
|
|
150
152
|
|
|
151
153
|
|
|
154
|
+
def test_json_destroy_prompt_does_not_pollute_stdout() -> None:
|
|
155
|
+
def invoke(op_id: str, params: dict[str, object]) -> dict[str, object]:
|
|
156
|
+
return {"id": "L1", "handle": "@x", "source": "own", "status": "archived"}
|
|
157
|
+
|
|
158
|
+
app = derive_cli(REGISTRY, invoke)
|
|
159
|
+
result = runner.invoke(app, ["lane", "archive", "L1", "--json"], input="y\n")
|
|
160
|
+
|
|
161
|
+
assert result.exit_code == 0
|
|
162
|
+
assert "Run destroy op" not in result.stdout
|
|
163
|
+
payload = result.stdout[result.stdout.index("{") :]
|
|
164
|
+
assert json.loads(payload) == {
|
|
165
|
+
"id": "L1",
|
|
166
|
+
"handle": "@x",
|
|
167
|
+
"source": "own",
|
|
168
|
+
"status": "archived",
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
152
172
|
def test_schema_command_prints_derived_schema_without_daemon() -> None:
|
|
153
173
|
app = derive_cli(REGISTRY, lambda _op, _params: {})
|
|
154
174
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/plugins/dispatch/.codex-plugin/plugin.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/__init__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/errors.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/events.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/models.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/client/router.py
RENAMED
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/__init__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/context.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/derive_mcp.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/errors.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/examples.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/execute.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/op.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/contracts/registry.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/new_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/reactor.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/scheduler.py
RENAMED
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/core/triggers.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/__init__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/__main__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/control.py
RENAMED
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/lifecycle.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/daemon/supervisor.py
RENAMED
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/__init__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/models.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/registry/store.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/__init__.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/cli.py
RENAMED
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/src/outfitter/dispatch/surfaces/mcp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{outfitter_dispatch-0.2.0 → outfitter_dispatch-0.2.1}/tests/integration/test_lifecycle_e2e.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|