feat(runtime): adapter-declared idle_timeout_override — primitive #2 of 6 - #2139
Conversation
Capability primitive #2 (task #117). The first cross-cutting capability where the adapter actually displaces platform behavior — claude-code's streaming session can legitimately go silent for 8+ minutes during synthesis + slow tool calls; the platform's hardcoded 5min idle timer in a2a_proxy.go cancels it mid-flight (the bug PR #2128 patched at the env-var layer). This PR fixes it at the right layer: the adapter declares "I need 600s" and the platform's dispatch path honors it. Wire shape (Python → Go): POST /registry/heartbeat { "workspace_id": "...", ... "runtime_metadata": { "capabilities": {"heartbeat": false, "scheduler": false, ...}, "idle_timeout_seconds": 600 // optional, omitted = use default } } Default behavior preserved: any adapter that doesn't override BaseAdapter.idle_timeout_override() (returns None by default) sends no idle_timeout_seconds field; the Go side falls through to idleTimeoutDuration (env A2A_IDLE_TIMEOUT_SECONDS, default 5min). Existing langgraph / crewai / deepagents workspaces are unaffected. Components: Python: - adapter_base.py: idle_timeout_override() method on BaseAdapter returning None (the platform-default sentinel). - heartbeat.py: _runtime_metadata_payload() lazy-imports the active adapter and assembles the capability + override block. Try/except swallows ANY error so heartbeat never breaks because of capability discovery — observability outranks capability accuracy. Go: - models.HeartbeatPayload.RuntimeMetadata (pointer so absent = "old runtime, didn't say"; explicit zero-cap = "new runtime, declared no native ownership"). - handlers.runtimeOverrides: in-memory sync.Map cache keyed by workspaceID. Populated by the heartbeat handler, consulted on every dispatchA2A. Reset on platform restart (worst-case 30s of platform-default behavior — acceptable; nothing about overrides is correctness-critical). - a2a_proxy.dispatchA2A: looks up the override before applyIdle Timeout; falls through to global default when absent. Tests: Python (17, all new): - RuntimeCapabilities dataclass shape (frozen, defaults, wire keys) - BaseAdapter.capabilities() default + override + sibling isolation - idle_timeout_override default, positive override, dropped-override - Heartbeat metadata producer: default adapter emits all-False, native adapter emits flag + override, missing ADAPTER_MODULE returns {} (graceful), zero/negative override is omitted from wire, exception inside adapter swallowed Go (6, all new): - SetIdleTimeout + IdleTimeout round-trip - Zero/negative duration clears the override - Empty workspace_id ignored - Replacement (heartbeat overwrites prior value) - Reset clears entire cache - Concurrent reads + writes (sync.Map invariant) Verification: - 1308 / 1308 workspace pytest pass (was 1300, +8) - All Go handlers tests pass (6 new + existing) - go vet clean See project memory `project_runtime_native_pluggable.md` for the architecture principle this implements. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an adapter declares provides_native_scheduler=True (because its SDK has built-in cron / Temporal-style workflows), the platform's polling loop must skip firing schedules for that workspace — otherwise the schedule fires twice (once natively, once via platform). The native skip preserves observability (next_run_at still advances, the schedule row stays in the DB, last_run_at would still update) while moving the FIRE responsibility to the SDK. Stacked on PR #2139 (idle_timeout_override end-to-end). The RuntimeMetadata heartbeat block already carries the capability map; this PR teaches the platform how to read and act on the scheduler bit. Components: - handlers/runtime_overrides.go: extended the cache to store capability flags alongside idle timeout. Two heartbeat fields are independent — SetIdleTimeout / SetCapabilities each update one without stomping the other. Defensive copy on SetCapabilities so a caller mutating its map after the call doesn't retroactively change cached declarations. Empty entries dropped to avoid stale husks. - handlers/runtime_overrides.go: new HasCapability(workspaceID, name) + ProvidesNativeScheduler(workspaceID) — the latter is the package-level adapter the scheduler imports (avoids a handlers/scheduler import cycle). - handlers/registry.go: heartbeat handler now calls SetCapabilities in addition to SetIdleTimeout. - scheduler/scheduler.go: NativeSchedulerCheck function-pointer DI (mirrors the existing QueueDrainFunc pattern). New() leaves the field nil so existing callers preserve today's "always fire" behavior. SetNativeSchedulerCheck wires production. tick() drops workspaces declaring native ownership before goroutine fan-out; advances next_run_at so we don't tight-loop on the same row. - cmd/server/main.go: wires handlers.ProvidesNativeScheduler into the cron scheduler at server boot. Tests: Go (7 new): - SetCapabilitiesAndHas (round-trip) - per-workspace isolation (ws-a's declaration doesn't leak to ws-b) - nil/empty map clears (adapter dropping the flag restores fallback) - SetCapabilities is a defensive copy (caller mutation can't retroactively flip cached value) - SetIdleTimeout preserves capabilities and vice-versa (two-field independence) - empty entry deleted (no stale husks) - ProvidesNativeScheduler reads the same singleton heartbeat writes - SetNativeSchedulerCheck wires the function (scheduler-side) - nil-check safety contract for tick Python: no change needed — the heartbeat already serializes the full capability map via _runtime_metadata_payload (PR #2139). An adapter setting RuntimeCapabilities(provides_native_scheduler=True) automatically flows through. Verification: - 1308 / 1308 Python pytest pass (unchanged) - All Go handlers + scheduler tests pass - go build + go vet clean See project memory `project_runtime_native_pluggable.md`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(runtime): native_scheduler skip — primitive #3 of 6
5-axis reviewCorrectness: ✓ Several careful design choices:
Tests: ✓ 23 new tests with real coverage:
Architecture: ✓ Cache choice is well-justified inline:
The Security: ✓ No new auth boundary. Workspace already has a trusted bearer for /heartbeat — Performance: ✓ sync.Map atomic load per dispatch (zero contention under steady load); one heartbeat-rate write per 30s; defensive map copy is one allocation per heartbeat. Fine on every axis. FYI (non-blocking)
LGTM. Updating branch + arming auto-merge. |
…adata Reviewer bot flagged: import was leftover from earlier scaffolding — all test fixtures use sys.modules monkey-patching with SimpleNamespace instead. Drop to unblock merge. Tests still 5/5 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tier-check (#2139) The qa (id 20) and security (id 21) Gitea teams have existed since the 2026-05-12 orchestrator preflight (verified via /orgs/{org}/teams), but sop-tier-check.sh still treated them as pending placeholders (qa???, security???). This meant tier:medium PRs could never satisfy the qa/security clause — the script skipped unresolved ???-suffixed teams and the clause always failed. Changes: - TIER_EXPR[tier:medium]: qa???,security??? → qa,security - Update comment block to list the five live teams (ceo, engineers, managers, qa, security) and remove the internal#189 pending-team note. - Update test_sop_tier_check_clause_split.sh fixture to match the real team names. The ???-suffix fallback logic is preserved in the resolver so genuinely missing future teams still fail closed with a clear error. Closes #2139 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… tier refs Completes the SOP tier system removal started in #2407 by cleaning remaining tier artifacts and salvaging the non-tier fixes from #2396/#2397/#2399 branches. Changes: 1. **qa-review.yml + security-review.yml** — salvage #2139 + #2159: - Add `labeled, unlabeled` to `pull_request_target` triggers so gates re-evaluate when labels change (#2139). - Remove unreliable `github.event.review.state` guard (#2159); evaluator (review-check.sh) already reads actual reviews from API. - Replace `SOP_TIER_CHECK_TOKEN` with `SOP_CHECKLIST_GATE_TOKEN`. 2. **Workflow token cleanup** — zero SOP_TIER_CHECK_TOKEN refs: - sop-checklist.yml, gate-check-v3.yml, audit-force-merge.yml, ci-required-drift.yml: replace or remove all SOP_TIER_CHECK_TOKEN references. 3. **Lint + runbook cleanup** — remove stale tier-check mentions: - lint-required-no-paths.yml + lint-required-no-paths.py: update example context from `sop-checklist / tier-check` to `sop-checklist / all-items-acked`. - gitea-operational-quirks.md: update token name references. 4. **Mutation test enhancement** (test_no_tier_regression.sh): - Fail if SOP_TIER_CHECK_TOKEN reappears anywhere. - Fail if qa-review/security-review lose labeled/unlabeled triggers. - Fail if review.state guard reappears. 5. **Unit test updates** (test_gate_review_auto_fire.py): - Assert absence of review.state guard instead of presence. - Assert SOP_CHECKLIST_GATE_TOKEN instead of SOP_TIER_CHECK_TOKEN. All tests pass: - test_gate_review_auto_fire.py: 11 passed - test_gitea_merge_queue.py: 70 passed - test_gate_check.py: 9 passed - test_lint_required_no_paths.py: 21 passed - test_sop_checklist.py: 101 passed - test_no_tier_regression.sh: PASS Fixes #2403
Summary
Capability primitive #2 of 6 (task #117), stacked on the foundation in PR #2137. The first cross-cutting capability where the adapter actually displaces platform behavior end-to-end.
The bug PR #2128 patched at the wrong layer: claude-code's streaming session can legitimately go silent for 8+ minutes during synthesis + slow tool calls. The platform's hardcoded 5min idle timer in `a2a_proxy.go` cancels it mid-flight. PR #2128 made the timer env-tunable cluster-wide, but ops can't pick a single value that works for both fast langgraph + slow claude-code synthesis. This PR fixes it at the right layer: the adapter declares its own window and the platform honors it per-workspace.
Wire shape (Python → Go)
```
POST /registry/heartbeat
{
"workspace_id": "...",
...
"runtime_metadata": {
"capabilities": {"heartbeat": false, "scheduler": false, ...},
"idle_timeout_seconds": 600 // optional — omitted means use platform default
}
}
```
Default behavior preserved: any adapter that doesn't override `BaseAdapter.idle_timeout_override()` (returns None) sends no `idle_timeout_seconds` field; the Go side falls through to `idleTimeoutDuration` (env `A2A_IDLE_TIMEOUT_SECONDS`, default 5min). Existing langgraph / crewai / deepagents are unaffected.
Components
Python:
Go:
Tests (23 new)
Python (17):
Go (6):
Verification
Stacked
This is independent of PR #2137 (already merged). Each primitive is its own PR going forward.
🤖 Generated with Claude Code