Skip to content

feat: async operator human-gate contract — fire-and-park prompt + derived request ids (#1402)#1635

Merged
obasilakis merged 2 commits into
devfrom
feature/1402-redelivery-human-gate
Jul 17, 2026
Merged

feat: async operator human-gate contract — fire-and-park prompt + derived request ids (#1402)#1635
obasilakis merged 2 commits into
devfrom
feature/1402-redelivery-human-gate

Conversation

@vybe

@vybe vybe commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Ships the remaining feat: MAX_REDELIVERY cap + async operator-queue human-gate lever (lease-reaper backstop) #1402 deliverable: the authored async human-gate contract agents receive. The MAX_REDELIVERY cap + poison-park mechanism already shipped in Phase 3 (feat(pull-migration): pull/work-stealing coordination Phases 0–3 (dark) + pilot (#1081) #1550) — verified in-code during planning; this PR is the contract surface + docs + tests.
  • Platform prompt "Operator Communication" rewritten around fire-and-park, never block-and-wait: park the request → end the turn → process responded items in a later turn; ask-before-irreversible-actions guidance; execution-id-derived request ids (approval-{execution_id}-{slug}) closing the global-PK collision trap and making re-parks under re-delivery idempotent; expires_at + resume-instructions guidance for agents with no future turn.
  • _mode_guidance task mode gains an explicit park carve-out — "execute to completion, do not ask questions" no longer contradicts the gate on the exact autonomous path it governs.
  • Synced copies: config/trinity-meta-prompt/prompt.md (dead reference copy, injection removed in refactor: Runtime injection of platform instructions via --append-system-prompt #136) + new agent-guide async-comms section (incl. explicit honor-system framing — this is a compliance contract and audit trail, not a security boundary).
  • Docs: requirements/scheduling.md §10.13 (raw-attempts / no-reset-on-progress cap semantics + rationale, cap=0 design note, the still-blocking list for pull default-on), TARGET_ARCHITECTURE.md stale claims fixed (cap was described as "all unbuilt today"), operating-room.md (async contract, poison-park item type, refactor: Runtime injection of platform instructions via --append-system-prompt #136 prompt-integration correction), learnings.md (prompt-delivery-path pitfall).

Changes

  • src/backend/services/platform_prompt_service.py — text-only: PLATFORM_INSTRUCTIONS Operator Communication rewrite + _mode_guidance carve-out
  • config/trinity-meta-prompt/prompt.md, docs/TRINITY_COMPATIBLE_AGENT_GUIDE.md — synced contract copies
  • docs/memory/requirements/scheduling.md, docs/planning/TARGET_ARCHITECTURE.md, docs/memory/feature-flows/operating-room.md, docs/memory/feature-flows.md, docs/memory/learnings.md
  • tests/unit/test_1402_prompt_contract.py (+ registry) — sentinel phrases in every runtime build, old block-inviting wording gone, Operator Communication section byte-identical across runtimes (MCP-name-free), task-mode carve-out, composed end-to-end prompt, synced-copy parity

Residual (filed, referenced from the docs)

Test Plan

  • pytest tests/unit/test_1402_prompt_contract.py — 17 passed
  • pytest tests/unit/test_platform_prompt_runtime.py tests/test_platform_prompt_unit.py tests/unit/test_public_channel_prompt.py — all passing (no assertions on the changed wording elsewhere; repo-wide grep confirmed the only other old-wording copy is a historical 2026-03 spec, left per point-in-time convention)
  • No code-path changes: diff touches one string constant + one pure string-returning function + docs + tests

Fixes #1402

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@dolho

dolho commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Validated via /validate-pr. Genuinely nice piece of work — the diff is exactly what it claims (I checked: zero def/return/if/assignment lines change; it's one string constant, one pure string-returning function, docs, and tests), the residual issues #1629#1632 all exist and are open, and the "config/trinity-meta-prompt/prompt.md is a dead reference copy" claim holds up — it's referenced only by the new test and an archived deploy script, is in no Dockerfile and no runtime path. 17/17 tests pass locally. The _mode_guidance carve-out closes a real contradiction, and the test that composes an end-to-end task-mode prompt is the right way to prove it.

One finding, verified against the code rather than inferred.


⚠️ The contract promises an expired flip that no code performs

The new prompt tells every agent:

Set expires_at on requests that gate an action. If it passes without a response the platform marks the item expired — treat that as "not approved; do not proceed."

db/operator_queue.py:430 implements mark_expired() correctly — UPDATE status=expired WHERE status=pending AND expires_at < now. Nothing calls it. Every reference in the repo:

src/backend/database.py:2265        return self._operator_queue_ops.mark_expired()   ← facade delegator
src/backend/db/operator_queue.py:430    def mark_expired(self) -> int:               ← the definition
docs/memory/feature-flows/operating-room.md:389,453                                  ← docs

No background service, no router, no APScheduler job, no sweep in cleanup_service, nothing in src/scheduler/. The 5s operator_queue_service sync loop doesn't do it either. So a pending item past expires_at stays pending forever.

Why it matters beyond a doc nit:

  • The failure direction is fail-safe — the agent never sees responded, so it never proceeds. Good. But the agent is told to rely on an event that will never fire, and the guidance sits on the irreversible-action path, which is the one place the contract most needs to be true.
  • pending rows are never retention-deleted by design (db/operator_queue.py:373, Add operator_queue retention sweep to cleanup_service #1142 — only terminal rows are pruned, and expired is one of the terminal states this would produce). Every gating request an operator ignores becomes a permanent row. That compounds bug: operator-queue create path has no rate limit or size caps — flooding/operator-fatigue surface (blocking pull default-ON) #1632 (no create rate/size caps), which this PR itself flags as blocking pull default-ON.
  • operating-room.md already documents the propagation half — "still-pending entries in the agent's file are flipped in place to their terminal DB status (cancelled or expired)" — so the write-back path for expired exists and is dead code for want of a producer.
  • This PR propagates the same promise into two more places: scheduling.md §10.13 and operating-room.md ("an expired flip means 'not approved — do not proceed'").

I searched open and closed issues — nothing tracks this, so it isn't a known-and-deferred gap.

Options, in the order I'd rank them:

  1. Wire db.mark_expired() into the cleanup_service sweep — it's an existing, tested-shaped method and a 5-min cycle is well inside the tolerance for a human-scale gate. Makes the contract true.
  2. Soften the prompt to "an operator can let a gating request lapse; treat a stale request as not approved" and file the expiry sweep as a follow-up alongside feat: operator-queue respond → re-trigger dispatch (async human-gate resolution re-entry) #1630/bug: operator-queue create path has no rate limit or size caps — flooding/operator-fatigue surface (blocking pull default-ON) #1632.

Either is fine; shipping the sentence as-is is what I'd avoid, because the honor-system framing this PR is careful to state elsewhere ("compliance contract and audit trail, not a security boundary") makes it more important that the platform-side promises are literally accurate.


Suggestions (non-blocking)

  • execution_id is conditional. _render_context emits - **Execution ID**: … only if ctx.execution_id, but the contract says "Derive the id from your current execution ID (see the Execution Context block)" with no fallback for when it's absent. Interactive/chat contexts hit that path today, and bug: pull-claimed turns bypass platform system prompt composition — platform contract never delivered on the pull path #1629 (pull-claimed turns bypass prompt composition) is a second way to land there. A clause like "if no Execution ID is present, use a random suffix" would close it.
  • Mixed placeholder syntax. The JSON example uses approval-<execution_id>-deploy while the prose right below uses approval-{execution_id}-{short-slug}. The <…> form matches the memory block at line 151, so it's defensible — but two syntaxes three lines apart invites a literal {short-slug} in someone's queue file.
  • The PR notes "the only other old-wording copy is a historical 2026-03 spec, left per point-in-time convention" — FWIW a repo-wide grep for by reading the file and looking for items now returns only the test file, so that copy appears already gone or worded differently. No action; just noting the claim is now stale in the safe direction.

Validation summary

Category Status Notes
Base branch dev
PR size 10 files (+350/−37)
Issue link Fixes #1402 — resolves, P2/type-feature/theme-reliability. Same-repo Fixes on a dev PR closes at the dev→main cut, per the documented SDLC
Commit messages conventional; the single commit explains why
Requirements scheduling.md §10.13 — cap semantics, rationale, cap=0 note, still-blocking list
Architecture no endpoint/schema/component change; architecture.md documents prompt plumbing (runtime-awareness), not prompt text
Feature flow operating-room.md updated + index row
Feature-flow format matches house style
Security sweep no keys, tokens, real emails, public IPs, .env, credential files
Infrastructure none touched
Build/config packaging no new top-level module, no new os.getenv()
Code quality text-only, verified; scope matches the stated purpose
Tests 17/17; sentinels across all 4 runtime builds, negative assertion on the old wording, cross-runtime section parity, task-mode carve-out, composed end-to-end, synced-copy parity

Recommendation

REQUEST CHANGES — for one line:

  • Either wire db.mark_expired() into a sweep, or reword the expires_at guidance (and its two doc echoes) so the contract doesn't promise platform behaviour that doesn't exist.

Everything else is ready. If you take option 2, I'd file the sweep as a follow-up next to #1632, since the never-expiring pending rows and the missing create caps are the same unbounded-growth story.

…t, derived request ids, cap semantics (#1402)

The MAX_REDELIVERY cap + poison-park mechanism shipped in Phase 3 (#1550);
this lands the remaining #1402 deliverable: the authored contract agents
receive.

- Platform prompt (PLATFORM_INSTRUCTIONS → Operator Communication): rewrite
  around fire-and-park / never block-and-wait (park → end the turn → process
  responded items in a later turn), ask-before-irreversible-actions guidance,
  execution-id-derived request ids (approval-{execution_id}-{slug} — global-PK
  collision safety + re-delivery dedup), expires_at + resume-instructions
  guidance for agents with no next turn. Section stays MCP-tool-name-free so
  the Codex transform can't fork it.
- _mode_guidance task mode: explicit park carve-out so "execute to completion,
  don't ask questions" no longer contradicts the gate on the exact autonomous
  path it targets.
- Synced the dead-but-present reference copy (config/trinity-meta-prompt/
  prompt.md, injection removed in #136) and added the agent-guide async-comms
  section (incl. honor-system framing).
- Docs: requirements/scheduling.md §10.13 (raw-attempts cap semantics + still-
  blocking list for pull default-on), TARGET_ARCHITECTURE.md stale claims
  fixed ("all unbuilt today" → shipped split), operating-room.md (async
  contract, poison-park items, removed-#136 prompt-integration correction),
  learnings.md (prompt-delivery-path pitfall).
- Tests: tests/unit/test_1402_prompt_contract.py — sentinel phrases across
  every runtime build, old block-inviting wording gone, section byte-identity
  across runtimes, carve-out, meta-prompt/guide sync (17 tests).

Residual work tracked: #1629 (pull-path prompt delivery + re-delivery banner),
create caps).

Fixes #1402

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@obasilakis obasilakis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checked the contract against the code rather than just the prose, and it holds up:

  • expires_at → expired is real — mark_expired runs in the 5s sync loop (operator_queue_service.py:106), so the prompt isn't promising behaviour the platform lacks.
  • execution_id is populated and rendered on the autonomous path (task_execution_service.py:1170platform_prompt_service.py:525), so the derived-id instruction resolves.
  • The collision rationale is well-founded: operator_queue.id is Column("id", Text, primary_key=True) and create_item does .on_conflict_do_nothing(index_elements=["id"]) — a colliding id genuinely is swallowed silently.
  • Fixes #1402 is legitimate: the #1550 mechanism (MAX_REDELIVERY, redelivery_count, alert-first poison-park) is on dev in lease_reaper_service.py, so this contract surface completes the ACs.

Tests: 17 passed, and still pass rebased onto current dev. One necessary change before merge.

The #1629 residual claims are stale — that fix merged to dev a minute before this PR opened

Three docs assert present-tense code state that is false on the merge target:

  1. docs/memory/requirements/scheduling.md §10.13, in the "Still blocking pull default-on" list:

    pull-path platform-prompt delivery + re-delivery banner (#1629 — pull-claimed turns currently receive NO platform instructions at all)

  2. docs/planning/TARGET_ARCHITECTURE.md:

    the pull-path delivery of that prompt (pull-claimed turns receive no platform instructions today) is #1629, still blocking default-on for effect-bearing agents

  3. docs/memory/feature-flows/operating-room.md:

    Known gap (#1629): pull-claimed turns bypass platform-prompt composition entirely and currently receive none of these instructions

f6567b56 ("fix(pull): compose the platform system prompt on the pull-claim path + re-delivery banner", #1633) merged to dev at 14:00:26Z; this PR opened at 14:01:39Z. On dev, pull_coordination_service.py:87 calls compose_system_prompt(...) for pull-claimed turns, and #1633 shipped the re-delivery banner alongside it. Pure timing — the branch predates the fix by about a minute.

Citing #1629 as a tracked residual is still correct: it's status-in-dev, open only until the release cut. What needs changing is the present-tense code claims and its place in the still-blocking list. That list is the gate for defaulting pull mode on, so a resolved blocker sitting in it means the next reader either redoes #1629's work or mis-sequences the epic — which is exactly the kind of thing these docs exist to prevent.

Suggested wording: #1629 — fix merged to dev (#1633), pending release, and drop it from the still-blocking list. The remaining blockers there are unaffected (#548/#333 trace fidelity, #1401 prior_trace injection, fail-closed execution_id, #1632 queue caps).

Worth noting the irony: this PR's own learnings.md entry is precisely about the pull path not receiving the platform prompt. That lesson stands on its own — it's what caused #1629 to be filed and fixed. Only the "still open / never delivered" framing needs updating.

Rebase needed

The branch conflicts with dev on tests/registry.json only, and it's additive on both sides (dev's #1600/#1649/#1615 entries vs this PR's #1402 entry). I rebased locally to check: resolution is keep-both (80 dev entries + 1 = 81, no duplicate files), and the prompt + pull suites pass afterwards — 68 passed, 1 skipped.

Everything else looks good. The sentinel-lock approach, the cross-runtime byte-identity check, and the meta-prompt/agent-guide parity tests are a good fit for a contract that lives in prose.

…ry claims

The branch predates #1633 by ~1 minute (merged to dev 14:00:26Z; this PR
opened 14:01:39Z), so three docs assert a code state that is false on the
merge target:

- requirements/scheduling.md — #1629 listed under "Still blocking pull
  default-on" ("pull-claimed turns currently receive NO platform
  instructions at all")
- TARGET_ARCHITECTURE.md — "receive no platform instructions today ... still
  blocking default-on", and #1629 in the residual list
- feature-flows/operating-room.md — "Known gap (#1629): ... bypass
  platform-prompt composition entirely"

On dev, pull_coordination_service.py composes the platform prompt on the
claim path (fail-open) and injects the re-delivery banner. The still-blocking
list gates the pull default-on decision, so a resolved blocker sitting in it
costs a future reader real work.

#1629 stays referenced (status-in-dev — open until the release cut), now
described as shipped-pending-release rather than unbuilt. learnings.md is
left as-is: it records why #1629 was filed, which remains accurate history.
@obasilakis
obasilakis force-pushed the feature/1402-redelivery-human-gate branch from 48d6600 to e2d182a Compare July 17, 2026 13:22

@obasilakis obasilakis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Resolving my own changes-request: I rebased the branch onto current dev and pushed the fix myself (e2d182a).

Rebase — only tests/registry.json conflicted, additive on both sides. Resolution is keep-both: dev's 80 entries + this PR's #1402 entry = 81, no duplicate files. The feature commit is unchanged at +350/−37.

The fix (docs only) — corrected the three stale #1629 claims to say shipped-pending-release instead of unbuilt, and moved it out of the "Still blocking pull default-on" list in both scheduling.md and TARGET_ARCHITECTURE.md. #1629 stays referenced (it's status-in-dev, open until the release cut). learnings.md I deliberately left untouched — it records why #1629 was filed, which is accurate history and still the durable lesson.

Verified after the rebase: test_1402_prompt_contract.py + test_platform_prompt_runtime.py + test_1081_pull_endpoints.py → 68 passed, 1 skipped.

Since I both requested and made this change, please sanity-check my wording on the blocker lists before merging — I'd rather you disagree with the phrasing now than inherit it. Approving on the substance: the contract is well-founded (I verified expires_at → expired fires, execution_id resolves, and the global-PK collision is real), and the sentinel-lock + cross-runtime byte-identity tests are a good fit for a contract that lives in prose.

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.

3 participants