Summary
Enable and verify the async leaf-execution path (POST /runs {async:true} → Redis Streams work-queue → KEDA-scaled leaf-worker Jobs) end-to-end on OCP, so the harness scales dynamically and asynchronously on backlog instead of relying on the manual pre-scale workaround.
This is a finish + verify, not a build — the machinery is ~95% present.
Why
Long agentic leaves (8–16 min), bursty, idle-heavy. The synchronous /runs path holds one HTTP connection per leaf for its whole duration → head-of-line blocking, ~45% transport loss, and it defeats Knative autoscaling (the harness's own 503 {saturated} sheds excess faster than the KPA reacts, so pods stay at ~1). We've been papering over this with a manual pre-scale hack in the experiment drivers.
The async path fixes all of it: KEDA (not Knative) scales workers by watching the Redis stream backlog; the Knative harness becomes a thin 202 acceptor → no long-held connections, no 503/pre-scale problem, transport loss gone.
What already exists (main @ #153)
- Redis Streams work-queue — stream
leaf-queue, group leaf-workers; XADD/XAUTOCLAIM/XREADGROUP/XACK, heartbeat, dead-letter reap, idle-consumer GC (packages/work-queue/src/queue.ts).
- Async route —
POST /runs {async:true} → enqueue → 202 accepted+sessionId; GET /runs/status?sessionId= reads from Redis (packages/knative-server/src/server.ts).
- Worker — claim-one → run → write result → ack, with retry classification (
leaf-job.ts, harness/src/leaf-job-runner.ts, classify-outcome.ts).
- KEDA ScaledJob —
redis-streams scaler on leaf-queue/leaf-workers, dual triggers lagCount (new work + scale-to-zero) + pendingEntriesCount (reclaimer), pollingInterval 5s, maxReplicaCount 10, run-once Jobs (deploy/knative/leaf-scaledjob.yaml).
--with-keda installs KEDA (RH Custom Metrics Autoscaler) + KedaController + waits for the CRD (deploy/knative/setup-ocp.sh).
- Pool leasing — least-loaded + Redis lease + saturation error (
harness/src/select-sandbox.ts).
- E2E smoke —
deploy/knative/leaf-async-smoke.sh.
Remaining work (the actual scope)
Acceptance criteria
Relationship to other work
Assisted-By: Claude Code
Summary
Enable and verify the async leaf-execution path (
POST /runs {async:true}→ Redis Streams work-queue → KEDA-scaled leaf-worker Jobs) end-to-end on OCP, so the harness scales dynamically and asynchronously on backlog instead of relying on the manual pre-scale workaround.This is a finish + verify, not a build — the machinery is ~95% present.
Why
Long agentic leaves (8–16 min), bursty, idle-heavy. The synchronous
/runspath holds one HTTP connection per leaf for its whole duration → head-of-line blocking, ~45% transport loss, and it defeats Knative autoscaling (the harness's own503 {saturated}sheds excess faster than the KPA reacts, so pods stay at ~1). We've been papering over this with a manual pre-scale hack in the experiment drivers.The async path fixes all of it: KEDA (not Knative) scales workers by watching the Redis stream backlog; the Knative harness becomes a thin
202acceptor → no long-held connections, no 503/pre-scale problem, transport loss gone.What already exists (main @ #153)
leaf-queue, groupleaf-workers; XADD/XAUTOCLAIM/XREADGROUP/XACK, heartbeat, dead-letter reap, idle-consumer GC (packages/work-queue/src/queue.ts).POST /runs {async:true}→ enqueue →202 accepted+sessionId;GET /runs/status?sessionId=reads from Redis (packages/knative-server/src/server.ts).leaf-job.ts,harness/src/leaf-job-runner.ts,classify-outcome.ts).redis-streamsscaler onleaf-queue/leaf-workers, dual triggerslagCount(new work + scale-to-zero) +pendingEntriesCount(reclaimer),pollingInterval 5s,maxReplicaCount 10, run-once Jobs (deploy/knative/leaf-scaledjob.yaml).--with-kedainstalls KEDA (RH Custom Metrics Autoscaler) + KedaController + waits for the CRD (deploy/knative/setup-ocp.sh).harness/src/select-sandbox.ts).deploy/knative/leaf-async-smoke.sh.Remaining work (the actual scope)
leaf-scaledjob.yamlpinsKAGENTI_SANDBOX_NAME=sandbox-0and sets noKAGENTI_SANDBOX_POOL_SELECTOR→ all 10 workers would collide on one sandbox. Set the pool selector (+ cap/ttl) so workers lease from the pool.dev.local/serverless-harness:local; wire it into theoverlays/ocpimage transform (else it applies rawdev.local→ ImagePullBackOff, same gotcha as the sandbox-relay).--with-kedaactually appliesleaf-scaledjob.yaml(+ theserverless-harnessSA/RBAC the Job needs) after installing KEDA./runs(hold-connection) toPOST {async:true}→ poll/runs/status(lib.shalready haspoll_leaf_result). Also eliminates the ~45% transport loss and retires the pre-scale hack.maxReplicaCountvs pool × lease-cap. Excess workers correctly hitSandboxPoolSaturatedError→ requeue (clean back-pressure); align numbers intentionally (pool × cap ≈ target concurrency).setup-ocp --with-keda→leaf-async-smoke.sh→ burst-enqueue → watch KEDA spawn Jobs → drain → scale-to-zero; confirm paused/gated leaves + the reclaimer.Acceptance criteria
leaf-queuelag (up tomaxReplicaCount) and scales to 0 when drained.sandbox-0collision); over-provisioned workers back-pressure via requeue, not failure.dev.local).Relationship to other work
Assisted-By: Claude Code