afk: wire the T3 adapter to the REAL orchestration contract + fix priority
The T3 dispatch adapter was written against a guessed wire shape that the test
fake accepted but the live t3-afk server 400s — so the previously-green suite did
NOT mean the loop was actually wired to T3. Reverse-engineered the real contract
from the v0.0.27 binary, verified it live against t3-afk (including multi-turn),
and rewrote the adapter to match:
- dispatch sends BARE commands keyed by `type` (not a `command` string), with
client-minted threadId/commandId/messageId + createdAt; the server replies
{sequence}, so dispatch returns the id it generated (never one parsed back).
- a thread lives in a project (workspaceRoot = the repo checkout the agent runs
in), so dispatch ensures the repo's project (snapshot -> project.create iff
absent) before thread.create + thread.turn.start.
- add send_turn() for follow-up turns on an existing thread — multi-turn context
retention is verified live (turn 2 recalled turn 1).
- watcher reads thread liveness from latestTurn.state (completed->idle,
running/in_progress/pending->running, errored->error), not a non-existent
top-level `status` field.
Guard against recurrence: the test fake now REJECTS any command lacking a `type`
discriminator (the original bug fails loudly), plus an opt-in live smoke test
(tests/test_afk_t3_live.py) so "green" can mean "wired to T3".
Also align dispatch_policy to lower-priority-value-first (P0 before P1), matching
tracker conventions and Issue.priority's own docstring — it had deliberately
diverged to higher-first. Loop still ships DISABLED (kill switch on, empty
allowlist). 416 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2ef0db9a96
commit
e34640cc47
8 changed files with 555 additions and 272 deletions
|
|
@ -7,10 +7,11 @@ and the fix-forward bookkeeping), one ``tick``:
|
|||
|
||||
1. **assemble a ``RunState``** from the live edges + the run's bookkeeping:
|
||||
* ``thread_status`` — from ``t3_client.snapshot()``, by finding this run's
|
||||
thread and mapping T3's ``running``/``idle``/``error`` to a
|
||||
``ThreadStatus`` (missing thread, or any unrecognised status, folds to
|
||||
``None`` → "no status yet" → the state machine WAITs; we never escalate
|
||||
or close on a status we don't understand);
|
||||
thread and mapping its ``latestTurn.state`` (``completed`` → idle,
|
||||
``running``/``in_progress``/``pending`` → running, ``errored`` → error)
|
||||
to a ``ThreadStatus`` (missing thread, no turn yet, or any unrecognised
|
||||
state folds to ``None`` → "no status yet" → the state machine WAITs; we
|
||||
never escalate or close on a status we don't understand);
|
||||
* ``ci_status`` — ``ci_watcher.status(repo, commit)`` *only* when a commit
|
||||
is pushed (no commit ⇒ nothing to check ⇒ ``None``);
|
||||
* ``pushed`` / ``fix_forward_attempts`` / ``elapsed_seconds`` — straight
|
||||
|
|
@ -50,13 +51,22 @@ from .notifier import KIND_DONE, KIND_FROZEN, KIND_NEEDS_HUMAN
|
|||
from .poller import T3Port as _DispatchPort # dispatch(repo, issue, prompt) -> id
|
||||
from .types import Action, CIStatus, Config, Issue, Phase, RunState, ThreadStatus
|
||||
|
||||
# T3 snapshot status string -> ThreadStatus. Anything not in here (a status T3
|
||||
# adds later, or a malformed entry) maps to None — "no usable status yet" — so
|
||||
# the state machine waits rather than acting on something it can't interpret.
|
||||
# T3 ``latestTurn.state`` -> ThreadStatus. The real snapshot reports a thread's
|
||||
# liveness as the state of its latest turn (verified against t3-afk v0.0.27):
|
||||
# ``completed`` == the turn finished cleanly (agent is idle, awaiting input);
|
||||
# any not-yet-finished state (``running``/``in_progress``/``pending``/``queued``/
|
||||
# ``pendingInit``) == still working; ``errored`` == the turn failed. Anything not
|
||||
# in here (a state T3 adds later, or a malformed/absent entry) maps to None —
|
||||
# "no usable status yet" — so the state machine waits rather than acting on
|
||||
# something it can't interpret.
|
||||
_THREAD_STATUS_BY_STRING: dict[str, ThreadStatus] = {
|
||||
"completed": ThreadStatus.IDLE,
|
||||
"running": ThreadStatus.RUNNING,
|
||||
"idle": ThreadStatus.IDLE,
|
||||
"error": ThreadStatus.ERROR,
|
||||
"in_progress": ThreadStatus.RUNNING,
|
||||
"pending": ThreadStatus.RUNNING,
|
||||
"queued": ThreadStatus.RUNNING,
|
||||
"pendingInit": ThreadStatus.RUNNING,
|
||||
"errored": ThreadStatus.ERROR,
|
||||
}
|
||||
|
||||
# Action -> the terminal doorbell kind to ring. Only the terminal actions appear;
|
||||
|
|
@ -201,10 +211,13 @@ class Watcher:
|
|||
|
||||
def _thread_status(self, thread_id: str) -> ThreadStatus | None:
|
||||
"""This thread's liveness from the fleet snapshot, or ``None`` when the
|
||||
thread is absent or its status string is one we don't recognise."""
|
||||
thread is absent, has no turn yet, or its ``latestTurn.state`` is one we
|
||||
don't recognise. Liveness is the state of the thread's latest turn (the
|
||||
real snapshot shape), not a top-level ``status`` field."""
|
||||
for thread in self._t3.snapshot().get("threads", []):
|
||||
if thread.get("id") == thread_id:
|
||||
return _THREAD_STATUS_BY_STRING.get(thread.get("status"))
|
||||
latest_turn = thread.get("latestTurn") or {}
|
||||
return _THREAD_STATUS_BY_STRING.get(latest_turn.get("state"))
|
||||
return None
|
||||
|
||||
# ----------------------------------------------------------------- #
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue