6ea2943aa3
ci / obs-smoke (pull_request) Successful in 5s
lint-infisical-extractor / lint-infisical-extractor (pull_request) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
ci / lint (pull_request) Successful in 17s
ci / build (pull_request) Successful in 37s
ci / smoke-install (pull_request) Successful in 56s
ci / unit-tests (pull_request) Successful in 1m25s
ci / responsiveness-e2e (pull_request) Successful in 1m53s
The heartbeat delegation-result harvester (_check_delegations) surfaced EVERY completed/failed delegation row where source_id==self, deduped only by the IN-MEMORY _seen_delegation_ids set, then fired a self-message that woke the concierge to NARRATE the whole backlog. Because that set is process-scoped, every restart re-read the tenant's full backlog of historical completed PR-review rows (178 on the agents-team concierge) and re-narrated them — an endless re-narration loop across restarts — and the same path harvested codex-reviewer's autonomous org-pr-review-sweep results the concierge never asked to be woken for. Root fix (supersedes the watermark band-aid): the harvester now surfaces a delegation result ONLY when the concierge has an OPEN IN-FLIGHT AWAIT for it — a delegation it initiated this process and is genuinely awaiting. The open-await set is the union of the in-container in-flight registry (builtin_tools.delegation._delegations, read READ-ONLY so the delegate tool is untouched) and ids explicitly registered via the new register_awaited_delegation() seam. Both are process-scoped and EMPTY on a fresh boot, so the backlog is never re-read/re-narrated and a peer's autonomous sweep (no await) is never harvested; a genuinely user-awaited result is still delivered exactly once (seen-set dedupes the re-poll). Only the delegation-result harvest+self-wake scoping changes. The alive signal, status/degraded, loaded_mcp_tools duties and loop-guard #197 are untouched. _check_activity_delegations already persists its cursor to disk, so it does not re-narrate across restarts and is out of scope for the loop. Tests (wired as a loud merge-blocking CI gate): a peer autonomous/scheduled sweep result is NOT harvested; a user-awaited result is delivered exactly once and not re-delivered on re-poll; a fresh boot re-narrates ZERO of a 178-row historical backlog; a restart does not re-deliver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
229 lines
9.7 KiB
YAML
229 lines
9.7 KiB
YAML
name: ci
|
|
|
|
# Standalone-as-SSOT CI (post-migration 2026-05-20).
|
|
#
|
|
# This repo became the single source of truth for the molecule-runtime
|
|
# Python package as of the standalone-as-SSOT migration (issue #87
|
|
# follow-on, CTO-GO 2026-05-20). Edit Python under molecule_runtime/
|
|
# directly here; the monorepo workspace-server pins this wheel by
|
|
# version.
|
|
#
|
|
# Old mirror-guard CI removed — direct PRs are the canonical edit path.
|
|
|
|
on:
|
|
push:
|
|
branches: [main, staging]
|
|
pull_request:
|
|
# Dispatch lane for the LIVE Langfuse obs-smoke (needs the running local
|
|
# stack + Infisical creds, which only the on-box runner has). On a generic
|
|
# runner the smoke self-gates to SKIP, so it is also safe on push/PR.
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
unit-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Install test dependencies
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: |
|
|
# Install the editable package so CI exercises pyproject.toml's
|
|
# runtime dependency contract. This intentionally catches SDK/API
|
|
# drift in package-local adapter tests, not only tests under tests/.
|
|
# The `[test]` extra adds jsonschema for the workspace-comms
|
|
# schema-conformance gate (tests/test_workspace_comms_conformance.py).
|
|
pip install -e ".[test]" pytest pytest-asyncio
|
|
|
|
- name: Run unit tests
|
|
# Exclude the over-the-wire responsiveness e2e (its own job below) —
|
|
# it boots real uvicorn servers and a 50s busy window, too slow for
|
|
# the fast unit lane.
|
|
run: pytest -q --ignore=tests/integration
|
|
|
|
- name: Run build_routes real-subprocess regression gate
|
|
# Explicit, loud step for issue #88: the 4-branch build_routes contract
|
|
# must be exercised against the REAL a2a-sdk in child processes. A skip
|
|
# here means the real SDK is missing and the gate is inert.
|
|
run: pytest -v -rs tests/test_boot_routes_subprocess.py
|
|
|
|
- name: Run delegation-harvest active-await scoping regression gate
|
|
# Loud, merge-blocking gate for the concierge delegation-result
|
|
# RE-NARRATION loop (root fix, supersedes the watermark band-aid): the
|
|
# heartbeat harvester must surface a result ONLY for a delegation the
|
|
# concierge has an OPEN in-flight await for. Pins: a peer's autonomous/
|
|
# scheduled-sweep result is NOT harvested; a user-awaited result is
|
|
# delivered exactly once; a fresh boot re-narrates ZERO of a historical
|
|
# backlog; a restart does not re-deliver.
|
|
run: pytest -v -rs tests/test_heartbeat_harvest_active_await.py tests/test_heartbeat_notify_routing.py
|
|
|
|
responsiveness-e2e:
|
|
# Deterministic, merge-BLOCKING integration gate for the "busy agent
|
|
# becomes unreachable / blocks ~300s" regression class (2026-06-10).
|
|
#
|
|
# It drives a REAL RuntimeA2AExecutor mounted in REAL a2a-sdk JSON-RPC
|
|
# Starlette routes served by REAL uvicorn, over REAL HTTP, and asserts:
|
|
# (a) a concurrent same-context POST fast-acks (no head-of-line block)
|
|
# — locks in the non-blocking flip (#116);
|
|
# (b) the agent never goes 'stale' across a busy window that EXCEEDS
|
|
# the old 45s stale threshold — locks in the stale-window fix;
|
|
# (c) both turns complete + the agent recovers to idle;
|
|
# (d) a tool that hangs past the per-turn idle cap is bounded and the
|
|
# executor recovers instead of wedging — locks in A1 tool-timeout.
|
|
#
|
|
# No external creds / infra (localhost only + a scripted, LLM-free
|
|
# agent), so it is a deterministic merge gate — NOT a gated-nightly.
|
|
# The integration conftest LOUD-SKIPS (never silent-passes) if the real
|
|
# a2a-sdk wheel is somehow absent.
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Install test dependencies
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: |
|
|
# -e . pulls the REAL a2a-sdk[http-server] + starlette + uvicorn +
|
|
# httpx so the e2e exercises the production wire, not stubs.
|
|
pip install -e . pytest pytest-asyncio
|
|
|
|
- name: Run agent-responsiveness-under-load e2e
|
|
# CI-tuned timings: the busy window (50s) still comfortably EXCEEDS
|
|
# the asserted stale window (20s), so the >45s-stale-window contract
|
|
# is genuinely exercised, while keeping the job ~1 min. All timings
|
|
# are env-tunable so a slower runner can widen slack without code
|
|
# edits.
|
|
env:
|
|
RESP_E2E_A_BUSY_SECONDS: "50"
|
|
RESP_E2E_STALE_WINDOW_SECONDS: "20"
|
|
RESP_E2E_HEARTBEAT_INTERVAL: "4"
|
|
RESP_E2E_FAST_ACK_CEILING: "5"
|
|
RESP_E2E_IDLE_CAP: "3"
|
|
RESP_E2E_POLL_DEADLINE: "25"
|
|
run: pytest -q tests/integration
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Install ruff
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: pip install ruff
|
|
|
|
- name: Lint with ruff (syntax + obvious errors only)
|
|
# Initial lint scope is intentionally narrow — E9 (syntax), F (pyflakes
|
|
# errors only, not warnings). Broadening to full ruff rules is a
|
|
# follow-up task once the SSOT settles.
|
|
run: ruff check --select=E9,F63,F7,F82 molecule_runtime/
|
|
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Install build tooling
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: pip install build twine
|
|
|
|
- name: Build wheel + sdist
|
|
run: python -m build
|
|
|
|
- name: Verify package metadata
|
|
run: python -m twine check dist/*
|
|
|
|
- name: Upload wheel artifact
|
|
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3
|
|
with:
|
|
name: wheel
|
|
path: dist/*.whl
|
|
retention-days: 7
|
|
|
|
smoke-install:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Build local wheel for smoke install
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: |
|
|
pip install build
|
|
python -m build --wheel
|
|
|
|
- name: Install wheel in clean venv + smoke-import
|
|
env:
|
|
PIP_INDEX_URL: https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/
|
|
run: |
|
|
set -eu
|
|
python -m venv /tmp/smoke-venv
|
|
/tmp/smoke-venv/bin/pip install --quiet dist/*.whl
|
|
# Verify the package + the canonical multi-WS / runtime imports.
|
|
/tmp/smoke-venv/bin/python -c "
|
|
from molecule_runtime import executor_helpers
|
|
from molecule_runtime.mcp_workspace_resolver import resolve_workspaces
|
|
from molecule_runtime import mcp_cli
|
|
from molecule_runtime.executor_helpers import extract_attached_files
|
|
from molecule_runtime.llm_auth import normalise_llm_env
|
|
from molecule_runtime.credential_helper import install_credential_helper
|
|
from molecule_runtime.precommit_hook import install_pre_commit_hook
|
|
print('smoke-import: all canonical surfaces import cleanly')
|
|
"
|
|
# CLI entry-point — molecule-mcp must be installed + show help.
|
|
/tmp/smoke-venv/bin/molecule-mcp --help | head -5
|
|
|
|
# LIVE Langfuse trace smoke — the runtime end of the obs verification. The
|
|
# per-PR hermetic emission test (tests/test_langfuse_emission.py, unit-tests
|
|
# job) proves the OTLP bridge POSTs to the right endpoint with the right auth
|
|
# WITHOUT a live Langfuse; this job proves a real emitted trace actually lands
|
|
# in a reachable Langfuse, keyed off a run-id. It is GATED: with no Langfuse /
|
|
# no Infisical keys (any generic runner, or a stack with Langfuse off) the
|
|
# script SKIPs (exit 0) and never reds the lane — it only fails when Langfuse
|
|
# IS up+configured but the trace never shows up. Runs for real only on the
|
|
# on-box runner that can reach the local stack.
|
|
obs-smoke:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Live Langfuse trace smoke (gated when Langfuse off)
|
|
env:
|
|
# SSOT: the smoke derives LANGFUSE_HOST/keys from Infisical when these
|
|
# are present; empty on a runner without the identity → script SKIPs.
|
|
INFISICAL_API: ${{ secrets.INFISICAL_API }}
|
|
INFISICAL_TOKEN: ${{ secrets.INFISICAL_TOKEN }}
|
|
INFISICAL_WORKSPACE_ID: ${{ secrets.INFISICAL_WORKSPACE_ID }}
|
|
INFISICAL_OBS_ENV: prod
|
|
run: bash scripts/obs_smoke.sh
|