Agent Resource Quotas and Task Lifecycle Governance
Follows the complete user flow from configuration, creation, queueing, and execution to terminal-state diagnosis, describing current OpenProgram capabilities, resource limits in reference implementations, and the unified governance contract still required.
00 Scope and final decisions
This design governs agent/Job eligibility, per-session queue and execution capacity, parent-child budgets, actual usage, termination, and recovery. Existing agent.max_spawn_depth, agent.max_spawn_fanout, agent.max_messages continue governing call topology and cycles, without reinterpretation as resource budgets.
| Decision | Conclusion | Reason |
|---|---|---|
max_live_global | Do not add | The existing process-level ThreadPoolExecutor already uses OPENPROGRAM_JOB_WORKERS to control global execution capacity, default 4. Another equivalent setting would create conflicting global limits. The UI displays effective worker capacity read-only. |
max_live_per_session | Add | Prevent one session from consuming every worker; count only tasks actually executing whose stop has not yet been confirmed. |
max_queued_per_session | Add | Limit admitted but not running tasks to prevent an unbounded persistent queue. Queued tasks consume no live slot. |
max_jobs_per_session | Add | Limit cumulative successful admissions over the session lifecycle; terminal tasks do not return this quota. |
| token / cost / runtime / idle | Include all, with different enforcement conditions | Tokens and time are enforceable with safe upper bounds and cancellable boundaries; costs are enforceable in advance only when prices are known. Unknown cost must not be treated as zero. |
| Factual ledger | Reuse usage ledger | Actual provider usage remains recorded only in usage_events. Admissions and unsettled reservations are execution state, not a second actual-usage ledger. |
Outside this designBackground-session UI, MCP server, purchases or top-ups, organization billing, and multi-machine cluster scheduling. These systems may later reuse shared DTOs, reason codes, budget scopes, and reservation protocols; their product interactions are not assumed here.
Collaboration counts and resource limits measure different operations
The Agent collaboration design §5 specifies collaboration operations, message counts, and spawn counts; this page specifies durable execution admission, capacity, and resource budgets. max_jobs_per_session is not another name for spawn depth or per-turn fan-out. A current Job's id is also its canonical execution_id; job_output(job_id=...) retains its parameter name and accepts that same identity.
| Limit | Scope and counted operation | Behavior at the limit |
|---|---|---|
agent.max_spawn_depth | Generation count in the execution context; only agent(...) creating a new branch increments the child's generation. Dispatch onto an existing branch does not. | Refuse this new branch; existing branches may still receive work within the other limits. |
agent.max_messages | A path-local message count: spawn, agent(to=...), and send_message set the target count to sender plus one. Automatic follow-up reuses the completed Job's count without another increment; parallel calls in the same sender turn do not share a cumulative counter. | Refuse new collaboration deliveries; do not cancel existing Jobs or stop other tools in the current turn. |
agent.max_spawn_fanout | New branches with creates_agent=true, counted by caller (session_id, turn_id); deliveries onto existing branches do not count. | Refuse more branches in that turn; child completion does not return this per-turn count. |
max_jobs_per_session | Cumulative successful admissions for the execution target session (Job parent_session_id), including new Jobs on new or existing branches. Cross-session caller provenance is recorded separately. | Refuse new admission with quota.jobs_exhausted, retryable=false; admitted Job termination does not return the cumulative count. |
max_queued_per_session | Target-session admissions in preparing or queued. | Refuse new admission with quota.queue_full, retryable=true; rejection creates no new Job. |
max_live_per_session / scheduler_capacity | Session / global claims in live or stopping; work not yet confirmed stopped still occupies capacity. | Keep admitted Jobs queued until a live claim is available; capacity waiting is not execution failure. |
For the three collaboration limits, 0 disables that limit check; message and generation values may still propagate through execution contexts. Resource limits use a different schema: null means unset/inherit, every non-null value must be positive, and 0 is invalid. Queued/cumulative/token/cost/runtime/idle limits remaining unset after inheritance are disabled; the effective live default is still scheduler capacity.
agent(to=...) creates no branch but creates a new execution, so it checks the message limit and requests target-session admission. send_message also creates no branch; its current idle-target path calls run_agent_turn_async to create a creates_agent=false Job, consuming target-session cumulative admission. Ordinary busy-target inbox receipt precedes creation of that execution: queued delivery does not mean Job admission succeeded, and a later refusal must be reported to the sender. Messaging is not exempt from resource limits.
job_output still has can_use=delegation_budget_left and is hidden when the message budget is exhausted; this interface behavior remains to be corrected. The registered agent tools are agent, list_jobs, and job_output; there is no standalone job_stop tool. Cancellation reuses canonical execution control and the internal JobRunner.cancel_execution method; an internal method name is not a callable model tool.01 Current implementation: admission, resource accounting, and execution control integrated, with explicit gaps
The current worktree integrates Job, ResourceGovernor, durable admission, owner lease/reconcile, execution-time boundaries, and resource views into major execution paths. Source confirms these capabilities. Product tests were not rerun in this round; historical results remain in Git. Below, source-confirmed implementations, source-confirmed gaps, and unverified scope are distinguished.
| Existing mechanism | Confirmed behavior | Governance gap | Evidence |
|---|---|---|---|
| Global execution pool and admission | OPENPROGRAM_JOB_WORKERS remains the sole global capacity; runner and synchronous agent entry points call ResourceGovernor.admit_job(), and SQLite protects counts with BEGIN IMMEDIATE. | JobRunner starts a durable dispatcher, reconciler, and budget loop; the whole-product gate was not rerun in this round. | openprogram/agent/job/runner/ · admit_job_entity / spawn_job / _run_one openprogram/agent/resource_governance/ · admit_job / try_start |
| Persistent task and resource state | Job already has budget_scope_id, effective_limits, reason_code; the usage DB has admissions, budget scopes, and reservations. | Job, canonical execution store, ResourceSaga, and startup recovery source exist; the crash/concurrency matrix was not reverified in this round. | openprogram/agent/job/types.py openprogram/usage/ledger.py openprogram/agent/resource_governance/ |
| Session limits | max_live_per_session, max_queued_per_session, max_jobs_per_session are in the config schema and checked atomically by the governor. | Current source includes JobResourceView and Web/CLI/TUI/model surfaces; entry-point consistency and fairness regressions were not rerun. | openprogram/config_schema.py openprogram/agent/resource_governance/ · resolve_resource_limits / _capacity |
| Cancellation and recovery | Cancellation continues cascading through parent-child relationships; running tasks use cancel/kill/watchdog, and the governor has lease, stopping, and release primitives. | Current source includes RuntimeControlService, terminal recovery, lease reconciliation, and stopping/release paths; cross-process concurrency evidence was not reverified. | openprogram/agent/job/runner/ · cancel_execution / _run_one openprogram/agent/resource_governance/ · renew_lease / request_stop / release_task |
| token / cost | BudgetedRequest connects the shared provider stream entry points to reserve_provider_request before credentials/network access; output caps are clamped and terminal usage settles the reservation. The usage ledger retains unknown-cost provenance. | The production path uses reserve_provider_request rather than the standalone reserve_tokens/reserve_cost methods. Accounting checks and unknown-price refusal under a cost ceiling exist in source; provider coverage and failure-injection gates were not rerun in this review. | openprogram/providers/stream.py · stream_simple / stream openprogram/providers/budget.py · BudgetedRequest openprogram/agent/resource_governance/ · reserve_provider_request |
| runtime / idle | Configuration and budget scopes can store max_runtime_seconds, idle_timeout_seconds; existing provider/tool timeouts remain independent. | The current runner includes a budget loop, time limits, bounded_operation_timeout, and a non-preemptible guard; per-operation regressions were not reverified. | openprogram/config_schema.py openprogram/providers/utils/deadline.py openprogram/agent/job/runner/ |
| User state | Web and runtime control provide execution observation/cancellation and state broadcasts; model tools expose list_jobs/job_output; JobResourceView can generate the basic structure. | Current source includes JobResourceView, job_status resource projection, and Web/CLI/TUI/model read surfaces; cross-interface consistency tests were not rerun. | openprogram/agent/resource_governance/ · JobResourceView / build_task_resource_view apps/server/openprogram_server/_webui/ws_actions/job.py openprogram/programs/tools/agents/agent/list_jobs/ |
02 Other projects: compare counting units, rejection timing, persistence, cancellation, and visibility
Reference values explain counting units and execution models; they do not determine OpenProgram defaults. Full collaboration evidence is in Agent collaboration implementation comparison, with existing internal designs in Agent collaboration designandAsync Job lifecycle.
| Project | Actual counting units and limits | Rejection/blocking point | Persistence and cancellation | User visibility | Relevance to this design |
|---|---|---|---|---|---|
| Claude Code | Binary snapshot: spawn depth 3, concurrency 20, inbox 50; matrix evidence separately records per-session cumulative spawn counts. | Depth/concurrency checks occur during creation; fork paths differ on retaining tools but rejecting at call time. | Traceable agentId; cascading cancellation confirmed. The binary and leaked source tree are different implementations. | list/output/stop and agent state. | Live and cumulative counts must be separate, and evidence versions explicit; do not adopt its values. |
| Codex CLI | Global concurrency 6; V2 per-session 4; V1 depth 1; V2 calculates depth without enforcing it. | The execution controller checks concurrency before starting. | ThreadId; list/wait/interrupt; complete V2 interrupt cascading is unconfirmed. | The model can query and interrupt. | Supports separating per-session live limits from global scheduler capacity; depth and concurrency cannot substitute for each other. |
| OpenClaw | Default depth 1, 5 live children per parent, concurrency pools 8/4, plus message-cycle limits. | Spawn rechecks depth, children, and concurrency. | runId/session key; host-maintained run; child cancellation cascading confirmed. | list and host state. | Adopt submission-time rechecks and cascading, not overlapping children/global settings. |
| OpenCode | Permission shape implies depth 1; no numeric concurrency limit confirmed. | Tool permissions constrain child-session creation/calls. | Child session id and background metadata; host cascading cancellation. | Background-task state is visible, but model queries are limited. | Permission constraints are not resource quotas; state should be exposed independently. |
| Hermes | Default depth 1 (configurable to 3), child concurrency 3, 50 iterations per child, caller timeout 600 seconds, and heartbeat. | Limited at thread-pool submission and caller wait. | Synchronous wait; no stable model-facing job id; host can cascade cancellation. | Visible to dashboard/host; limited model control surface. | Adopt runtime/idle signals; caller wait timeout is not proof of execution termination. |
| CodeBuddy | Official local-package docs specify at most 16 concurrent workflow agents and 1000 agents per run. | Workflow orchestrator enforces phase/run limits. | Resumable in the same session; agent-team docs also state teams are not restored by /resume. | Workflow UI shows phase agent counts, tokens, elapsed time, and abort. | Separate concurrency, cumulative counts, and visible usage; do not adopt 16/1000 or extend resume claims to teams. |
| pi-mono | Example extension: batch 8, concurrency 4, 50KB output per task. | The extension limits batch/concurrency before spawning subprocesses. | No durable job id/query; single-level process cancellation may leave deeper descendants. | Primarily call results. | Extension-local semaphores cannot cover persistent tasks and cascading recovery. |
Adopt
- Separate global scheduler capacity and per-session live limits.
- Count live, queued, and cumulative tasks separately.
- Stable job ids, cascading cancellation, and user-queryable state.
- Show tokens, elapsed time, and reason together in task views.
Adapt or reject
- Reject direct adoption of competitor values.
- Reject depth, fan-out, or tool permissions as substitutes for resource admission.
- Reject process-local semaphores alone; admission must be durable and atomic across processes.
- Do not describe caller timeout or a terminal marker as execution termination.
03 Planned user flow: establish one governance boundary
04 Inspect and configure global defaults and session overrides
- Current behavior
- The config schema includes collaboration limits and agent.resource_limits; the resolver handles global/session/parent/job sources, and environment configuration controls worker capacity.
- User impact
- Configuration presence does not establish verified enforcement at every entry point or UI. Show configured values, sources, and effective values without presenting incomplete enforcement as complete.
- Gap
- Multiple equivalent global, parent, and child settings make precedence difficult to explain; using 0 for both unlimited and disabled perpetuates ambiguity.
- Target behavior
- One minimal
agent.resource_limitsobject holds global defaults; session metadata overrides matching fields. APIs always return source, configured, and effective values. Tasks may only tighten effective session limits.
agent.resource_limits: max_live_per_session: integer | null max_queued_per_session: integer | null max_jobs_per_session: integer | null max_total_tokens: integer | null max_cost_usd: decimal-string | null max_runtime_seconds: integer | null idle_timeout_seconds: integer | null
All non-null values must exceed 0; null means unset/inherit. For compatibility, queued, cumulative, and the four budget types are disabled by default.max_live_per_session defaults to current scheduler capacity when unset; otherwise its effective value is the lesser of configuration and scheduler capacity.OPENPROGRAM_JOB_WORKERS remains the sole source of global execution capacity, exposed through the read-only scheduler_capacity field.
Only the owner may change global defaults and session overrides. Agent/task requests may supply only stricter token/cost/runtime/idle budgets, without changing live, queued, cumulative limits or increasing any owner limit. Extend existing config get/set with these fields and reuse session metadata; do not create a second quota configuration service.
max_total_tokens and max_cost_usd count governed agent/Job invocations carrying job_id within the session; ordinary foreground main conversations are excluded.max_runtime_seconds and idle_timeout_seconds is a time ceiling for each Job and its ancestor scopes. Changes to live/queued/cumulative limits do not terminate existing live/queued work, but immediately constrain new admission or queued → live; token/cost changes apply the new session ceiling at the next reservation. Runtime/idle defaults affect only subsequently admitted Jobs, avoiding retroactive deadlines.
05 Admission and atomic reservation when an agent creates a task
- Current behavior
- Synchronous/asynchronous agent tools and busy-target dispatch call runner/governor admission. The governor checks depth, fan-out, queue, and cumulative counts transactionally; the model tool retains a process-local fan-out precheck. Ordinary busy-target messages enter the inbox before Job creation on delivery.
- User impact
- Identical requests can differ by entry point; concurrent frontends/processes may pass non-atomic checks simultaneously; failed creation may leave Job, branch, inbox, or count residue.
- Gap
- Unified admission, cross-process transactions, and idempotent identity exist in source. Rejection without Job-creation side effects and notification of admission refusal after ordinary message queueing still require entry-point verification.
- Target behavior
- Every execution-task creation path must call
ResourceGovernor.admit_job. Perform pure validation first, atomically check depth/fan-out and resource limits and reserve in SQLite, then create the Job, any new branch, and Job-bound inbox entry and publish execution-accepted events only on success. Rejection creates no Job. An independently authorized ordinary message receipt may precede admission.
send_message may persist a message receipt before a Job exists, subject to message authorization and inbox limits. This is distinct from a Job-bound inbox entry created by agent(to=...). If admission later fails, mark that receipt failed and notify its sender with the admission reason; do not claim an execution was accepted. The transaction below governs Job creation, not the earlier receipt.Unified coverage
- Synchronous and asynchronous
agent();agent(to=...)busy-target precreation;run_agent_turn_async. - WebSocket/API spawn; model tools; any future direct
JobRunner.spawn_jobentry point. - Retries with the same
job_idand request fingerprint are idempotent reads and cannot increase fan-out, cumulative counts, or queue reservation again; differing content returnsquota.admission_conflict. send_messagecreates no Agent branch, but a new Job it triggers requires admission and counts against the execution target session’s cumulative jobs and resource budgets. Ordinary inbox receipt has not yet created a Job and must not claim execution admission succeeded.
Durable intent across SQLite and jobs.json
- Generate
job_id,admission_idand request fingerprint; validate arguments, target, and permissions without mutation. - In the usage DB’s first
BEGIN IMMEDIATEtransaction, check depth, per-turn fan-out, and session/parent remaining capacity, insert apreparingadmission, reserve one queue slot plus provisional fan-out and cumulative increments, then commit.preparingmust be durable before jobs.json writes; every concurrent admission check includes its provisional occupancy to prevent cross-process overallocation. - Use
openprogram._compat.flockto protect per-session task-file read-modify-write and persist the Job. Do not broadcast or create other irreversible side effects yet. - In a second
BEGIN IMMEDIATEtransaction, change admission frompreparingtoqueued, finalize provisional fan-out/cumulative counts, and commit; only then create the branch and Job-bound inbox entry and broadcast execution-accepted status. An existing ordinary message receipt is linked to the admitted Job at delivery. - Any failure triggers idempotent compensation. On restart, a durable
preparingwith a matching Job is finalized in the second transaction; without one, release queue and provisional counts transactionally. SQLite transactions must not span jobs.json file I/O.
{accepted:false, job_id:null, reason_code, retryable, effective_limits, usage, capacity}. A full queue is usually retryable; exhausted cumulative or parent budgets are not, unless the owner raises limits or creates a new session.06 Queueing and live concurrency
- Current behavior
- The runner uses durable admissions and a dispatcher. The governor checks session/global live capacity before claiming queued work; queued work has no live claim.
- User impact
- Queue/cumulative configuration and live claims exist, but complete cross-entry enforcement, fairness and queue diagnostics still require verified evidence.
- Gap
- A durable dispatcher and atomic resource claims exist in source; fairness, worker-loss recovery and cross-process admission/release require the full acceptance gate.
- Target behavior
- Admission first enters a durable queue. The dispatcher submits only when global worker and session live capacity are available, releasing queue and acquiring live in one transaction. Queued work consumes no live slot.
Compatible Job states remain pending/queued/running/completed/cancelled/errored; resource state persists separately.stopping is especially important: a Job may display cancelled, but its live lease cannot return until worker/thread/runtime termination is confirmed. This prevents overallocation when the 30-second watchdog only changes status.
The dispatcher selects the earliest globally ordered admitted_seq task whose session currently has live capacity, skipping temporarily ineligible sessions so their head task cannot block others. The global worker pool still determines total execution capacity.
Only the single execution process holding the existing worker lock may call queued → live and submit to the executor. Web/TUI/CLI/API processes only admit and durably enqueue, preserving the single-worker topology and preventing separate global thread pools per process. Multiple execution workers would require a separate scheduling design.
07 Parent-child budget allocation and tightening-only inheritance
- Current behavior
- Jobs carry budget_scope_id and effective_limits. The governor creates parent-linked scopes, resolves ancestor limits, and checks ancestor balances transactionally during provider reservations; §01 records provider integration status.
- User impact
- The scope model supports shared parent ceilings rather than independent child allowances. Users need the effective ceiling and shared_remaining to distinguish a local limit from currently available shared capacity.
- Gap
- The primitives and production reservation path exist; explicit-child allocation, sibling contention, and all entry-point propagation still require the full acceptance matrix.
- Target behavior
- A task request may carry one
limitsobject containing only token/cost/runtime/idle budgets. Explicit child limits cannot exceed effective session limits, parent-task limits, or current parent-scope remaining capacity.
Deterministic rule without explicit child allocation
Do not divide the parent budget equally in advance or predict future work from fan-out. Children join the shared parent budget scope. Every provider/tool reservation atomically checks the task’s own ceiling, every ancestor scope, and session scope; the first successful durable reservation acquires capacity. This avoids idle static shares and does not misrepresent current remaining capacity as a guaranteed child allotment.
The UI labels shared_remaining as a shared live balance, not an allotment. Explicit child budgets create stricter local ceilings without permanently deducting from the parent in advance; actual reservations consume parent balance.
08 Metering and enforced stopping for tokens, cost, wall-clock, and idle
- Current behavior
- Governed provider calls reserve through BudgetedRequest before credentials or network access, clamp output, and settle terminal usage. The runner has a budget loop and bounded-operation checks; ungoverned legacy paths retain best-effort usage recording.
- User impact
- Protection depends on Job attribution and an audited provider/operation boundary. Unknown cost must remain distinguishable from zero, and a timeout result alone does not prove a synchronous thread stopped.
- Gap
- Reservation, attribution, unknown-cost handling, activity tracking, and operation boundaries exist in source. Adapter coverage, late settlement, and operation-specific stop confirmation require runtime acceptance; those gates were not rerun here.
- Target behavior
- The existing usage ledger alone records actual usage; add reservation state in the same DB. Atomically reserve before model/tool calls, settle using final provider usage afterward. Active timers and boundary checks jointly trigger existing cascading cancellation.
| Budget | Unit and factual source | Pre-call enforcement | During/after the call | When reliable enforcement is impossible |
|---|---|---|---|---|
| token | Provider-authoritative input + output; cache reads/writes are separate diagnostics, not added twice. | Reserve input_upper_bound + max_output_tokens, and reduce the provider output cap to remaining capacity. | Settle with final usage events and release the difference; concurrent requests include open reservations. | Under a strict token budget, reject a new model request if no tokenizer or safe upper bound is available: quota.accounting_unavailable. |
| cost | Known provider/model/rate and actual tokens; retain cost_source. | Reserve a micro-USD upper bound only when all required prices are known. | Settle using actual tokens/rates; aggregation also returns cost_known and unknown_cost_events. | With a cost budget, unknown current-call pricing or existing unknown-cost events in the scope reject new LLM calls: quota.cost_unavailable. Without a cost budget, execution is allowed but cost displays as unknown, never $0; do not rewrite historical actual costs using later catalog prices. |
| runtime | Monotonic wall-clock from entering resource state live; queue wait is excluded. | Each provider/tool timeout is the minimum of the operation limit and task time remaining. | Timer expiry sets cancellation and cascades through existing parent-child relationships. | Tools unable to exit reliably within remaining time cannot enter strict-budget tasks; return error.nonpreemptible_operation. |
| idle | Monotonic time since the last meaningful activity. | Record the call-start boundary; while waiting for children, link their activity. | Provider data, tool progress, and child progress/terminal events update activity; transport keepalives do not. | A tool with neither progress nor bounded timeout is non-preemptible. |
Explicit bound for non-preemptible operations
Enforce exhaustion at model-request/tool-call boundaries, with active timers triggering cancellation promptly. Do not describe an executing operation as instantly terminated. Its maximum non-preemptible interval must equal the minimum of declared operation timeout, remaining task runtime, and system cancel grace. Synchronous in-process operations that cannot meet this must move to terminable subprocesses or be rejected for strict-budget tasks; asyncio.wait_for returning does not mean the executor thread has ended.
Accounting delay and crashes
- Reservation states are
reserved → started → settled, or, before provider I/O begins,reserved → released. Release not-yet-started reservations if credential resolution, request configuration, or provider-generator creation fails; after successfulstart, cancellation, transport errors, or missing terminal usage retain conservative exposure for late settlement of the same reservation. Absence of a first stream event does not prove the provider never received the request. - If a request started but final usage is unavailable, retain conservative unsettled exposure; do not fabricate actual tokens/cost or release immediately and allow repeated overspending.
- Budgeted tasks fail closed if the accounting DB is inaccessible before calling. Post-call settlement failure marks the task errored, stops further calls, and retains the reservation for reconciliation.
- Existing best-effort recording may remain for non-budgeted legacy paths; governed calls require acknowledged reserve/settle protocols.
09 Slot release and cumulative semantics after cancellation, failure, timeout, and completion
- Current behavior
- Job terminal state and resource admission/lease state are separate. The governor retains cumulative admissions and supports stopping and confirmed release; runner completion and recovery use these primitives.
- User impact
- A cancelled Job can still occupy live capacity while its execution is stopping. The UI must distinguish requested cancellation from confirmed exit and quota release.
- Gap
- The state separation and cumulative-retention rules are defined in code. Watchdog, late completion, and crash interleavings still need full-matrix verification; absence of this review evidence is not absence of the primitives.
- Target behavior
- Release queue/live only when execution termination is confirmed or lease-recovery rules hold. Increase cumulative count once on successful admission and never return it for any terminal state.
| Event | Queue slot | Live slot | Cumulative tasks | Terminal Job state |
|---|---|---|---|---|
| Admission rejected | Not occupied | Not occupied | No increment | No Job created |
| Preparing rollback | Release | Not occupied | No increment | No Job or restored consistency |
| User cancels after queued | Release immediately | Not occupied | Retain | cancelled |
| Cancellation/budget exhaustion after running | Already released | Enter stopping; release after confirmed exit | Retain | cancelled |
| Execution error/completion | Already released | Release after worker finally confirms | Retain | errored/completed |
| Retry identical job_id | No duplicate occupancy | No duplicate occupancy | No duplicate increment | Return original task/conflict reason |
Cumulative counts last for the session lifecycle. There is no implicit reset, and session archiving does not return quota. Users can create a new session or have the owner explicitly raise its override. Lowering limits cannot delete existing tasks; usage above the new limit only rejects later admissions.
10 Restart recovery, cross-process concurrency, and diagnosis
- Current behavior
- Job storage uses cross-process flock. Runner startup and its reconcile loop use durable admissions, owner leases, and ResourceSaga recovery; legacy unlinked Jobs follow the existing orphan policy. The worker lock keeps execution on one worker.
- User impact
- Admission and recovery span SQLite, Job files, and runtime ownership. Diagnosis must distinguish restored queued work, worker loss, and released resources while retaining the original execution identity.
- Gap
- Owner/lease, preparing reconciliation, file locking, and structured reason primitives exist. Process-death timing, simultaneous frontend writes, and repeated recovery still require the acceptance matrix; this review did not rerun it.
- Target behavior
- Single-worker execution remains supported; multiple UI/API processes compete through SQLite admission transactions. Each live admission carries owner/lease; reconciliation uses intent, Job, and lease facts to prevent permanent quota leaks.
Recovery rules
preparing + Job exists: validate admission/job ids, then finalize as queued;preparing + no Job: roll back queue and provisional cumulative delta.queuedre-enters the durable dispatcher after restart without consuming live capacity.live/stoppingwith an absent owner and expired lease: first mark the Job errored under the existing orphan policy with reasonerror.worker_lost, then release live; another instance cannot claim an unexpired lease. Initially renew every 10 seconds with a 30-second TTL, persisted using UTC wall-clock. If the original owner still holds the worker lock, an apparently expired wall-clock lease must not be released. These are not user quota settings.- A lease proves worker ownership, not rollback of external side effects. Preserve worker-lost diagnosis in Job output.
- All jobs.json read-modify-writes use cross-process
flock; SQLite uses WAL,BEGIN IMMEDIATE, and unique keys for idempotent admission/reservation.
This design adds no cluster consensus or remote scheduler. If multiple workers are later allowed, the same job_admissions lease/owner contract may extend, but leader election, clock assumptions, and external-runtime fencing require a separate design.
11 User-visible state and reason codes in Web, TUI, and CLI
- Current behavior
- JobResourceView and stable resource reasons exist, with Web subscriptions, terminal/CLI adapters, and list_jobs/job_output projections. Cancellation uses canonical execution control.
- User impact
- Users need the same queue, cumulative, cost-known, and stopping explanations across surfaces. Data projection alone does not establish that every field is correctly displayed in every state.
- Gap
- The shared DTO exists; cross-interface field/reason parity, legacy records, unknown-cost display, and recovery states still require runtime and visual acceptance. No standalone Tasks panel is claimed.
- Target behavior
- Web, TUI, CLI, and model tools consume the same
JobResourceView; wording may vary by interface, but fields and reason codes must match.
| View | Required display | Interaction | Compatibility |
|---|---|---|---|
| Web | Job status + resource state, queue position, session live/queued/cumulative, four remaining budgets, known/unknown cost, and reason. | Reuse existing task/branch cards and cancel; do not add background-session UI. | Existing job_status events gain optional resource fields. |
| TUI | Compact session capacity, selected-task remaining budgets, and stop reason. | Reuse task list/details/stop; no new full-screen product surface required. | Missing fields render as legacy tasks, not guessed zeros. |
| CLI | Human-readable tables and stable JSON: effective limits, usage, reservations, reason, retryable. | Reuse task-query and cancellation commands; JSON supports automated diagnosis. | New fields remain backward-compatible; the Job state enum stays unchanged. |
| Model tools | list_jobs, job_output, and canonical execution control return the same reason and simplified remaining budgets; JobRunner.cancel_execution is an internal method. | Admission rejection is a structured tool result, without fabricating a Job record. | Keep original text results and add machine-readable fields. |
Stable reason code
| Phase | reason_code | Meaning |
|---|---|---|
| admission | quota.queue_full | The session’s durable queue is full; retry after capacity is released. |
| admission | quota.jobs_exhausted | Session cumulative task count reached its limit. |
| admission | quota.parent_budget_exhausted | Parent-scope remaining capacity cannot accept the child limit. |
| admission | quota.token_exhausted / quota.cost_exhausted | Insufficient reservation capacity before the request. |
| admission | quota.cost_unavailable | A cost budget is set, but provider/model pricing is unknown. |
| admission | quota.invalid_limits | Invalid value or child limit looser than parent/session limit. |
| admission | quota.accounting_unavailable / quota.admission_conflict | Accounting is unsafe, or an idempotency key conflicts with existing request content. |
| terminal | cancel.user / cancel.parent / cancel.session | Explicit cancellation source. |
| terminal | budget.token_exhausted / budget.cost_exhausted / budget.runtime_exhausted / budget.idle_exhausted | A running task reached a budget boundary; Job becomes cancelled and cancellation cascades to descendants. |
| terminal | error.worker_lost / error.accounting_unavailable / error.nonpreemptible_operation | Execution safeguards failed; Job becomes errored. |
| terminal | completed | Normal completion. |
12 Stable schema and state contract
Actual usage retains usage_events as its sole source of truth. Admission/reservation tables below represent only occupied resources and unsettled exposure, never fabricated actual provider tokens/cost.
job_admissions(
admission_id PRIMARY KEY,
job_id UNIQUE NOT NULL,
session_id NOT NULL,
parent_job_id,
caller_turn_id,
creates_agent BOOLEAN NOT NULL,
request_fingerprint NOT NULL,
budget_scope_id NOT NULL,
state CHECK state IN ('preparing','queued','live','stopping','released'),
admitted_seq NOT NULL,
owner_instance_id,
lease_expires_at,
created_at, started_at, last_activity_at, released_at,
reason_code
)
budget_scopes(
budget_scope_id PRIMARY KEY,
scope_kind CHECK scope_kind IN ('session','task'),
session_id NOT NULL,
job_id UNIQUE,
parent_scope_id,
max_total_tokens,
max_cost_microusd,
max_runtime_seconds,
idle_timeout_seconds,
created_at
)
usage_reservations(
reservation_id PRIMARY KEY,
job_id NOT NULL,
budget_scope_id NOT NULL,
kind CHECK kind IN ('token','cost'),
state CHECK state IN ('reserved','started','settled','released'),
reserved_tokens,
reserved_cost_microusd,
request_started_at,
settled_event_id,
expires_at
)
usage_events + optional columns:
job_id, budget_scope_id, reservation_id
Job JSON + optional fields:
admission_id, budget_scope_id, effective_limits, reason_code
effective_limits is an audit snapshot at task start; configuration queries separately return each field’s configured, effective and source. Actual shared availability is computed live from the ledger and open reservations; shared_remaining explicitly denotes current ancestor/session shared-pool balances, not guaranteed Job allotments. Job JSON does not copy cumulative actual usage. Configure money as decimal fixed-point strings; store and compare integer micro-USD to avoid floating-point boundary differences.
JobResourceView
{
"job_id": "...",
"status": "running",
"resource_state": "live",
"reason_code": null,
"retryable": false,
"capacity": {
"scheduler_capacity": 4,
"session_live": {"used": 2, "limit": 3},
"session_queued": {"used": 1, "limit": 8},
"session_jobs": {"used": 17, "limit": 100},
"queue_position": null
},
"budget": {
"scope": "task_with_shared_ancestors",
"tokens": {"actual": 12000, "reserved": 4000, "limit": 50000},
"cost_usd": {"actual": "0.38", "reserved": "0.12", "limit": "2.00",
"known": true, "unknown_events": 0},
"runtime_seconds": {"used": 91, "limit": 600},
"idle_seconds": {"used": 4, "limit": 120},
"shared_remaining": {"tokens": 34000, "cost_usd": "1.50"}
}
}
13 Compatibility, migration, and implementation batches
Compatibility principles
- Mark old Jobs missing new fields as
legacy/unmetered; missing does not mean zero, and historical jobs.json is not rewritten. - Existing Job status enums, job ids, broadcasts, and cascading-cancel APIs remain compatible; resource state and reason are optional additions.
- Existing depth/messages/fanout settings and semantics remain unchanged. They guard topology/cycles and do not count quota.
- Existing
OPENPROGRAM_JOB_WORKERSremains the sole global worker capacity. No newmax_live_global. - Old-database migration adds only nullable columns/tables/indexes; usage events remain append-only.
| Batch | Implementation scope | Enablement conditions | Source status; acceptance not rerun |
|---|---|---|---|
| 1 Accounting foundation | Schema migration, task/budget attribution, unknown-cost aggregation, read-only effective limits/diagnostics. | Old-database upgrade/rollback, unknown-cost, and existing usage-query compatibility tests pass. | Migration and DTO source exist; compatibility gates not rerun |
| 2 Admission and queue | Unified creation entries, durable intent, per-session live/queued/cumulative, dispatcher, cross-process file lock. | All entries covered; no overallocation across processes; every terminal state and crash point recoverable. | Admission, dispatcher, and recovery source exist; complete entry-point and concurrency gates not rerun |
| 3 Tokens and cost | Shared parent-child scopes, reserve/settle, provider output cap, unknown-price fail-closed. | Concurrent requests, late usage, settlement failure, and price-change tests pass. | Shared provider call path is connected; coverage and fault-injection gates not rerun |
| 4 Runtime and idle | Active timer, activity contract, deadline clamp, bounded-operation contract, cascading stop. | Model, tool, synchronous thread/subprocess, and stopping-lease tests pass. | Runtime/idle loops and bounded-operation checks exist; operation-specific gates not rerun |
| 5 User surfaces and gate | One DTO across Web/TUI/CLI/model tools, reason wording, compatibility docs, and mechanical checks. | Cross-interface contract tests and all gates on this page pass. | Shared resource DTO and surface adapters exist; cross-interface gates not rerun |
14 Test matrix
| Test area | Minimum cases | Meaning of failure |
|---|---|---|
| Configuration | Global defaults, session overrides, parent/task tightening only; null/positive integer/decimal validation; read-only worker environment display. | Unpredictable effective values or overlapping global limits. |
| Collaboration/resource interaction | Depth exhausted still permits existing-target dispatch within message/admission limits; messages exhausted denies new delivery while authorized observation/cancel remains; send_message-created Jobs count in target-session admission; topology 0 and resource null remain distinct. | Resource limits can be bypassed or existing work cannot be observed/stopped. |
| Creation entries | Sync/async agents, target inbox, Web/API, runner internals, and future registered entries; rejected Job creation leaves no new Job, branch, or Job-bound inbox entry; an earlier ordinary message receipt becomes failed and notifies its sender if later admission fails. | A quota bypass exists. |
| Atomic admission | 20+ concurrent threads/processes; unique job ids; limit boundaries; fault injection before/after preparing. | Overallocation, duplicate cumulative counts, or permanent leaks. |
| Queue/live | Queue consumes no live slot; atomic queued→live exchange; oldest eligible across sessions; full queue; stopping retains live. | Session starvation, premature release, or unbounded executor queue. |
| Cumulative count | No return for completed/cancelled/errored; preparing rollback does not count; same-job-id retry does not duplicate; lowering limits. | Terminal-state-dependent cumulative semantics or retry bypass. |
| Parent-child budgets | Reject explicit child limits above parent remaining capacity; shared scope without explicit budget; concurrent sibling reservations; multi-level ancestor checks. | Children can expand budgets or overspend concurrently. |
| Token/cost | Safe token bounds, provider output cap, no duplicate cache tokens; known/unknown prices; late/missing usage; settlement DB failure. | Budgets are only observable afterward, or unknown costs become zero. |
| Runtime/idle | Exclude queue wait; provider data/tool progress/child activity count; keepalive does not reset; deadline clamp; reject non-preemptible tools. | Time budgets are unenforceable or incorrectly terminate active tasks. |
| Cancellation and recovery | User/parent/session/budget cascading; thread survives watchdog; worker crash at each transaction point; lease expiry; idempotent reconciliation. | Premature release, surviving descendants, or permanent occupancy after restart. |
| User visibility | Web/TUI/CLI/model DTO contract; queue position, shared remaining, unknown cost, reason/retryable; legacy task. | Interfaces disagree or display unknown as zero. |
15 Feature-matrix filled-circle mechanical gate
The feature-matrix row “concurrency and per-session cumulative spawn limits” at the foundational baseline current worktree baseline has not satisfied the following mechanical gates. Whether the current worktree satisfies them requires item-by-item source and verification evidence, not candidate-branch records:
- Stable configuration schema implemented; global defaults, session overrides, and effective values visible through Web/TUI/CLI.
- All agent/Job creation entries use unified admission, with no Web, synchronous-agent, target-inbox, or direct-runner bypass.
- Per-session live and cumulative admission are atomic across competing processes without overallocation or double counting.
- Queue/live occupancy, cumulative return, and stopping/live retention follow this page’s semantics.
- Completion, failure, cancellation, timeout, worker crash, and mid-admission crash release eligible queue/live capacity without permanent leaks.
- Web/TUI/CLI show effective limits, usage, queue/live/cumulative, and stable reason codes.
- Compatibility migration, concurrency/crash tests, documentation links, and feature-matrix scoring checks all pass.
16 Current implementation and verification boundaries
This section records implementations directly locatable in the current worktree and remaining acceptance boundaries. It excludes dates, commit SHAs, and migration history; consult Git for evolution.
| Scope | Current source evidence | Current status | Gaps requiring confirmation |
|---|---|---|---|
| Job entity and state | openprogram/agent/job/types.py: Job, JobStatus, legal transitions, and worktree_id; openprogram/agent/job/store.py: persistence and state updates. | Implemented | Crash recovery, terminal idempotence, and cross-process concurrency still require complete-matrix acceptance. |
| Runner and admission | openprogram/agent/job/runner/: JobRunner, durable dispatcher, reconciler, budget loop, spawn_job, cancel_execution, list_jobs, await_job; ResourceGovernor is used during runner initialization and execution. | Implemented and integrated into major execution paths | Whole-product gate, cross-process concurrency, and worker-loss evidence were not rerun in this round. |
| Job WebSocket and model tools | apps/server/openprogram_server/_webui/ws_actions/job.py: spawn/list/get; openprogram/programs/tools/agents/agent/list_jobs/, job_output/; runner provides cancellation. | Major entry points implemented | Cancellation-entry consistency and DTO/reason/retryable regressions were not rerun; a standalone Tasks panel is outside the current implementation. |
| Web state | apps/web/lib/net/use-ws.ts: job_status, spawn_job_result; apps/web/components/right-sidebar/branches/index.tsx and attach-card.tsx: state/resource subscriptions. | Major user surfaces implemented | A standalone Tasks panel is outside current implementation; remaining resources, queue position, unknown cost, and recovery-state regressions were not rerun. |
| ResourceGovernor admission/lease/reconcile | openprogram/agent/resource_governance/: admit_job, claim_next, renew_lease, reconcile, scope, and resource view; integrated with runner. | Primitives and major execution integration implemented | The token/cost provider boundary still needs adapter-by-adapter confirmation; full verification was not rerun. |
| Verification boundary | Current documentation and source sampling provide path/symbol evidence; historical verification remains in Git. | Not reverified | Documentation build and checks are separate from product execution acceptance. This review did not rerun product runtime gates; that does not establish missing implementation. |
Status rule.“Primitive exists,” “entry returns state,” and “complete execution path passed acceptance” are distinct states. Future updates must record each separately, using current worktree evidence rather than historical reviews or candidate-branch records.
Evidence index
| Topic | Repository evidence |
|---|---|
| JobRunner, single worker, cancellation, and orphan recovery | openprogram/agent/job/runner/; openprogram/agent/job/store.py; openprogram/agent/job/types.py; openprogram/worker/paths.py |
| Spawn and message budgets | openprogram/programs/tools/agents/agent/agent/agent.py; openprogram/programs/tools/agents/send_message/send_message/depth.py; openprogram/config_schema.py |
| Usage and provider recording | openprogram/usage/event.py; openprogram/usage/ledger.py; openprogram/usage/recorder.py; openprogram/providers/stream.py |
| Deadline and function timeout | openprogram/providers/utils/deadline.py; openprogram/programs/_runtime.py |
| Task UI and protocol | apps/server/openprogram_server/_webui/ws_actions/job.py, Web branch/task cards, openprogram/programs/tools/agents/agent/list_jobs/, job_output/, JobRunner.cancel_execution |
| Competitors and existing designs | agent-collab-comparison.html; Agent collaboration design; Async Job lifecycle; Usage metering |