Skip to content

Implement governed FAAS job lifecycle contract for Hermes#10

Draft
erikhinla wants to merge 17 commits into
codex/faas-hermes-contract-reconciliationfrom
codex/faas-job-lifecycle-contract
Draft

Implement governed FAAS job lifecycle contract for Hermes#10
erikhinla wants to merge 17 commits into
codex/faas-hermes-contract-reconciliationfrom
codex/faas-job-lifecycle-contract

Conversation

@erikhinla

Copy link
Copy Markdown
Owner

Purpose

This draft PR is stacked on #9 and implements the minimum Postgres-backed FAAS contract required before a Hermes worker adapter can execute a real proving task.

What this changes

  • Reconciles intake/routing vocabulary to canonical worker lanes: openclaw, hermes, agent_zero and risk tiers low, medium, high.
  • Introduces artifact_production as the bounded Hermes work type.
  • Makes task intake replay-safe by treating task_id as the idempotency key and refusing duplicate queue entries.
  • Persists the bounded worker brief (title, goal, inputs, output_required) with each job.
  • Adds protected worker lifecycle endpoints:
    • GET /v1/jobs/{task_id}
    • POST /v1/jobs/{task_id}/claim
    • POST /v1/jobs/{task_id}/complete
    • POST /v1/jobs/{task_id}/fail
    • POST /v1/jobs/{task_id}/escalate
    • POST /v1/jobs/{task_id}/reflections
  • Adds lease fields and append-only reflection sequencing for idempotency and replay safety.
  • Secures the legacy /v1/hermes/* endpoints with the existing API token dependency and makes reflection sequence aware.
  • Adds services/bizbrain_lite/migrations/002_faas_worker_claim_contract.sql for an existing Hetzner staging database.

Authority and deployment boundary

  • FAAS remains the governing execution and proof layer.
  • Hermes is a governed worker, not the execution engine or runtime governor.
  • This PR does not add or deploy a Hermes container.
  • This PR does not touch Hostinger production.
  • Apply and exercise this contract on Hetzner staging only after review.

Review gates before staging

  1. Review migration behavior against any existing staging job_records duplicates; the migration intentionally stops if idempotency cannot be enforced cleanly.
  2. Review the new intake routing rules and API-token boundary.
  3. Run the service/API tests and a migration against Hetzner staging.
  4. Only then build the Hermes adapter/container and run the real TBTX artifact proving task.

Verification note

I inspected the branch diff and isolated the implementation from #9. I could not run local Python/tests in the current Codex session because the local command runner fails to launch a shell; runtime verification remains required on staging before this PR is promoted.

@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flow-state Ready Ready Preview, Comment May 26, 2026 7:48pm

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a governed worker execution model (FAAS) by updating schemas, database models, and API endpoints to support task claiming, lease management, and sequential reflections. A new worker-facing lifecycle API (worker_jobs.py) is added, and existing intake and skill routers are refactored to align with the new contract. The review feedback highlights several critical improvement opportunities in the new worker_jobs.py endpoints, specifically recommending a 409 Conflict when a different worker attempts to claim an active lease, and adding idempotency checks to the fail_job and escalate_job endpoints. Additionally, a potential race condition was flagged in the legacy write_reflection endpoint when concurrently generating sequence numbers.

Comment on lines +95 to +96
if job.status == JobStatus.ACTIVE.value and job.lease_expires_at and job.lease_expires_at > now:
return {"outcome": "already_running", "job": job_payload(job)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When a job is already ACTIVE with an unexpired lease, any worker attempting to claim it receives a 200 OK response with {"outcome": "already_running"}.

To prevent other workers from mistakenly assuming they successfully claimed the job (or having to parse the 200 OK response body to detect a failed claim), the API should raise a 409 Conflict if a different worker tries to claim an active lease, while still returning already_running with 200 OK for idempotent retries by the same worker.

Suggested change
if job.status == JobStatus.ACTIVE.value and job.lease_expires_at and job.lease_expires_at > now:
return {"outcome": "already_running", "job": job_payload(job)}
if job.status == JobStatus.ACTIVE.value and job.lease_expires_at and job.lease_expires_at > now:
if job.claimed_by == request.worker_id:
return {"outcome": "already_running", "job": job_payload(job)}
raise HTTPException(status_code=409, detail="Task is currently leased by another worker")

Comment on lines +140 to +141
if job.status != JobStatus.ACTIVE.value or job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may fail this task")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The fail_job endpoint currently lacks an idempotency check. If a worker retries a failed request (e.g., due to a network timeout), the second request will find the job in FAILED status (which is not ACTIVE) and raise a 409 Conflict error. Adding an idempotency check similar to complete_job ensures that retries are handled gracefully.

Suggested change
if job.status != JobStatus.ACTIVE.value or job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may fail this task")
if job.status == JobStatus.FAILED.value and job.claimed_by == request.worker_id:
return {"outcome": "already_recorded", "job": job_payload(job)}
if job.status != JobStatus.ACTIVE.value or job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may fail this task")

Comment on lines +157 to +158
if job.status == JobStatus.ACTIVE.value and job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may escalate this task")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The escalate_job endpoint has two issues:

  1. It only enforces the worker claim check if the job is currently ACTIVE. If the job is in another state (e.g., COMPLETED or QUEUED), anyone can escalate it, which bypasses state transition guards.
  2. It lacks an idempotency check for retried escalation requests by the same worker.

Restricting escalation to ACTIVE jobs and adding an idempotency check for already ESCALATED jobs resolves both issues.

Suggested change
if job.status == JobStatus.ACTIVE.value and job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may escalate this task")
if job.status == JobStatus.ESCALATED.value and job.claimed_by == request.worker_id:
return {"outcome": "already_recorded", "job": job_payload(job)}
if job.status != JobStatus.ACTIVE.value or job.claimed_by != request.worker_id:
raise HTTPException(status_code=409, detail="Only the worker holding the active claim may escalate this task")

Comment on lines +80 to +85
if reflection.sequence_number is None:
next_result = await db.execute(
select(func.coalesce(func.max(ReflectionRecord.sequence_number), 0) + 1)
.where(ReflectionRecord.task_id == reflection.task_id)
)
sequence_number = next_result.scalar_one()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the legacy compatibility write_reflection endpoint, if sequence_number is not provided, it is computed concurrently using max(sequence_number) + 1. Under concurrent requests for the same task_id, this will lead to a race condition where both requests compute the same sequence number, causing one to fail with a database unique constraint violation (uq_reflection_task_sequence).

While this is a legacy endpoint, it is recommended to either lock the job row (similar to the new /jobs/{task_id}/reflections endpoint) or handle the unique constraint violation gracefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant