Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion docs/engineering/codex_dgx_worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ separate. The launcher should unset `OPENAI_API_KEY` and `CODEX_API_KEY`, set
the dedicated `CODEX_HOME`, and execute the installed official Codex CLI.
Validate an actual agent-issued shell command before enabling polling.

Install the worker, `codex_von_inbox.py`, and both adjacent `_prompt.md` files
Install the worker, `codex_von_inbox.py`, `codex_von_retry.py`, and both adjacent `_prompt.md` files
from the same reviewed revision. Run them with the project's PDM-managed Python environment.
When these scripts are copied into a release bundle outside a complete checkout,
pass `--backend-root /path/to/reviewed/Von` to the worker. This selects the
Expand All @@ -178,6 +178,104 @@ point and list reads. The adapter requires `requested_model` and
`requested_reasoning_effort` keys even when their values are null. An older reader
that omits them fails visibly before pickup instead of silently choosing defaults.
Installing only a newer worker script cannot repair an older canonical reader.

### Waiting for a coding dependency

The retry policy in `scripts/codex_von_retry.py` separates task context from
permission to spend another coding attempt. After `blocked` or `needs_input`,
the existing task state retains the result, failed attempt, recovery owner and
expectation. Archive attachments, evidence/checkpoint updates, self-reports,
unrelated comments, CI, a pending status and elapsed time cannot admit a retry.
The task checkpoint and one idempotent message explain the wait. Other eligible
tasks continue, including when one retained report cannot be delivered.

A coding result may provide an optional `blocker` with `key`, `kind`
(`dependency` or `transient`), `owner`, `recovery`, `scope_json`,
`required_observations_json` and `probe_key` (usually null). The two JSON strings
hold objects describing the relevant candidate/profile and exact observations,
for example `{"authenticated":true,"profile_bound":true}`. The controller binds
the task, coding actor, organisation, failed attempt and observation time. This
describes an external prerequisite; code the assigned agent can safely repair
and independently useful work should be handled before returning a blocker.

There are three supported continuation routes:

- The delegator can write a task comment beginning exactly
`/retry-coding <failed-attempt> <reason>`. This is an explicit one-attempt
retry after review, not a claim that the dependency was repaired. Quoted
commands and other authors' comments do not qualify.
- For natural-language repair, answers or changed scope, use the existing inbox
reply route. Its bounded interpretation receives the retained result and
supplied receipt bytes. It must distinguish relevant recovery, explicit retry
and independently useful scope from a status report. The controller binds a
`resume_task` decision to that failed attempt and source message. This path
uses one ordinary inbox interpretation per new message; polling does not
repeatedly ask a model to reinterpret an unchanged failure.
- A delegator-authored canonical task comment may carry `source.coding_retry`,
an operator-verified repair receipt. Required fields are `attempt`,
`blocker_key`, `binding` (`task_id`, `agent_id`, `organisation_id`), `scope`
(matching the blocker's JSON object), `observed_at`, `observations`,
`evidence_reference` and `reason`. Observations must include every required
value with its exact JSON type, and be observed after the failed attempt.
Use the linked file-copy concept and checksum as the evidence reference when
applicable. The trusted producer must actually inspect/read back the repair;
a filename, receipt metadata or a top-level ready flag is insufficient. The
comparator does not discover or execute arbitrary evidence files. It rejects
wrong-task/actor/org, wrong-revision, stale, future and contradictory receipts.

Older text-only results are retained as **unclassified**, not silently inferred
to have machine-checkable recovery conditions. They can use explicit retry or
the contextual inbox route. Ordinary free-text task comments remain context;
they do not automatically constitute repair. The visible wait supplies the
supported continuation route, so a repair outside Git or with different wording
does not require changing a classifier or a filename convention. Missing local
state for a canonical blocked/in-progress task requires reconciliation instead
of assuming earlier effects never happened.

For known transient dependencies, the operator can register `retry_probes` in
private controller configuration, keyed by `probe_key`. Each entry contains an
`argv` list for an existing cheap read-only helper, `timeout_seconds`,
`interval_seconds` and `max_interval_seconds`. Choose bounds from that helper's
declared/observed duration with headroom; the subprocess timeout protects the
controller queue's liveness, independently of coding-run duration. The helper
receives the retained blocker JSON on stdin and returns the repair receipt
above on stdout. No task/model-supplied command, URL or executable is run. Failed
or unknown probes back off exponentially to the configured maximum, with the
next check persisted before execution; they never launch a coding model. No
probe is registered by default. Unknown coding-process outcomes always retain
their work/effects for reconciliation, rather than treating them as a cheap
transient failure.

The source token is consumed in the same durable write that marks the new
attempt running, before the subprocess starts. Restart first reconciles an
existing run/report/archive. A replayed inbox acknowledgement cannot bind the
old message to a newer failure or reopen a task whose retry already ran. Task
cancellation, reassignment, project writer and current authority are rechecked
before the admitted continuation. Model preferences still come from the
canonical task.

### DGX and Mac retry-policy handoff

Source merge and installation are distinct. Use the existing coherent release
procedure below at an idle controller boundary; retain the previous release,
state, worktrees, schedule, lock and configured model preferences. Do not run a
second worker to activate or test the upgrade. `--check-task` and the next
scheduled runtime receipt must identify the selected module paths/revision.
This change does not request a public web deployment.

The Mac bridge is operator-owned and is not source-controlled in this repository.
Its operator must import the same `codex_von_retry` module, map its retained
attempt/result and canonical inputs to `retain_blocker`/`admission`, and persist
`consume` with the running checkpoint before its existing launch call under its
existing lock. Bind config identity locally; never trust a comment's claimed
author or scope instead of the canonical author and current assignment. Keep
the same inbox source authorisation, repair producer verification, current-task
recheck, cheap-probe backoff and result/effect reconciliation boundaries as the
DGX adapter. Run the shared tests plus an isolated replay of that installed
bridge and record canonical wait/recovery read-back and zero duplicate launches.
An import test or a DGX replay alone is not evidence of Mac integration or
activation. Return the Mac adapter diff and both hosts' installation/runtime
receipts through the coordinating operator before claiming both are active.
Do not change global model defaults to compensate for a mismatched installation.

For an authorised upgrade, use the coherent installation below. Preserve the
Expand Down
57 changes: 53 additions & 4 deletions scripts/codex_von_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
authorised_task,
capability_context,
fingerprint,
resolve_execution_settings,
referenced_tasks,
resolve_execution_settings,
retry_policy,
stage_file_copy_evidence,
write_json,
)
Expand All @@ -23,8 +24,9 @@
authorised_task,
capability_context,
fingerprint,
resolve_execution_settings,
referenced_tasks,
resolve_execution_settings,
retry_policy,
stage_file_copy_evidence,
write_json,
)
Expand Down Expand Up @@ -139,6 +141,20 @@ def context_for(config, api, message):
tasks = [item["task"] for item in lookup["results"] if item["status"] == "found"]
for item in lookup["results"]:
if item["status"] == "found":
retained = retained_task_state(config, item["task_id"])
if retained:
item["retained_attempt"] = {
key: retained.get(key)
for key in (
"attempt",
"phase",
"result",
"blocker",
"worktree",
"finished_at",
"execution_settings",
)
}
try:
item["assignment_context"] = api.inputs(item["task"])
except Exception as exc:
Expand All @@ -163,6 +179,11 @@ def context_for(config, api, message):
}


def retained_task_state(config, task_id):
path = Path(config["state_root"]) / "tasks" / (fingerprint(task_id)[:16] + ".json")
return json.loads(path.read_text()) if path.exists() else {}


def validate_result(value, context):
fields = set(SCHEMA["required"])
# Retained pre-upgrade runs have four fields. Keep them recoverable.
Expand Down Expand Up @@ -323,7 +344,11 @@ def prepare_attachment_inputs(config, context, run):
inputs.append(item)
context["attachment_inputs"] = inputs
if len(references) > 8:
inputs.append({"error": "Only the first eight attachments were prepared in this run; remaining references have not been inspected."})
inputs.append(
{
"error": "Only the first eight attachments were prepared in this run; remaining references have not been inspected."
}
)


def launch(config, state, lock_fd):
Expand Down Expand Up @@ -496,18 +521,40 @@ def finish(config, api, state, path):
if task_id:
if result["action"] == "resume_task":
state["effect_stage"] = "task_resume"
retained = retained_task_state(config, task_id)
blocker = (
retry_policy.retain_blocker(config, retained) if retained else None
)
# Freeze before reopen/delivery; a lost acknowledgement must never
# bind the old request to a newer failed attempt on the next poll.
if "retry_intent" not in state:
state["retry_intent"] = (
{
"attempt": blocker["attempt"],
"binding": blocker["binding"],
"reason": result["answer"],
}
if blocker
else None
)
write_json(path, state)
# This exact follow-up is the new coding/deployment authority carrier.
followup = {
"message_id": message["message_id"],
"content": message["content"],
"deployment_requested": result["deployment_requested"],
**({"retry": state["retry_intent"]} if state["retry_intent"] else {}),
**(
{"attachments": message["attachments"]}
if message.get("attachments")
else {}
),
}
prior = task_followup(config, task_id)
if "message:" + message["message_id"] in retained.get(
"consumed_retry_tokens", []
):
state["resume_applied"] = True
if not state.get("resume_applied"):
if (
prior
Expand Down Expand Up @@ -694,7 +741,9 @@ def tick(config, api, lock_fd):
write_json(path, state)
if state["phase"] == "reporting":
finish_or_retain(config, api, state, path)
return True
# A retained delivery failure uses no model and must not prevent
# independent coding tasks from making progress on this poll.
return state["phase"] == "done"
pending = new_messages(config, api)
if not pending:
return False
Expand Down
9 changes: 9 additions & 0 deletions scripts/codex_von_inbox_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ Choose the action using contextual judgement:
- resume_task: the current message requests additional work on an identified
supplied task, or answers a question needed to continue that task. It may reopen
that task. Explain the work; the controller verifies the transition.
For a retained blocked attempt, compare the actual blocker and repair evidence.
Resume only for relevant verified recovery, an explicit authorised retry with
its reason, or a changed scope that permits independently useful work. Say which
applies. Status comments, worker archives, passing CI, elapsed time, quoted
readiness claims and setup_ready with contradictory observations are not repair.
Inspect supplied receipt bytes when available, including actor, organisation,
revision/profile and authentication as relevant. Unknown outcomes require
reconciliation of retained effects. If the dependency is unchanged, reply with
the waiting reason and recovery owner; do not queue another coding attempt.
- create_task: necessary new coding work is already authorised by the delegator
and no supplied task represents it. Use task_id="" and new_task with a short
title and requested_model/requested_reasoning_effort (null unless explicitly
Expand Down
9 changes: 9 additions & 0 deletions scripts/codex_von_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def verify(target, commit):
):
if not (target / name).is_file():
raise runtime.DeploymentError("Incomplete worker release: " + name)
# Older coherent releases predate the shared retry module. They remain valid
# rollback targets; newer workers must carry their actual imported dependency.
if (
"codex_von_retry" in (target / "scripts/codex_von_worker.py").read_text()
and not (target / "scripts/codex_von_retry.py").is_file()
):
raise runtime.DeploymentError(
"Incomplete worker release: scripts/codex_von_retry.py"
)
return {
"commit": commit,
"backend_root": str(target),
Expand Down
Loading
Loading