mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-19 10:04:16 +00:00
Merge development updates before OSWorld review fixes
Integrate published e707e3ba79 and regenerate
the data-layout source digest over both architecture descriptions.
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
commit
6b63340935
30 changed files with 503 additions and 174 deletions
|
|
@ -216,7 +216,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
|
|||
├── project_dialogue.py ← Read-only chat lens + append-only `logs/chat_annotations.jsonl`; the sidecar routes nothing but the token-bound `needs_manual_target` decision card; `build_owner_message_ref`, `routing_refusal_cause` (the owner-facing `cause` sentence), `room_membership` (§3 Chat and Projects)
|
||||
├── project_lease.py ← One-writer-per-project lease in `assign_tasks`; same-project subagent swarms exempt; `""` is no lane
|
||||
├── context.py ← Main context assembly and Available-subagents catalog; ordinary shared context retains authored knowledge summaries and an explicit missing-overview gap
|
||||
├── context_input_selection.py ← Optional declared-source API-child composition and validation; ordinary shared continuity stays default (§6 Selected first-input sources)
|
||||
├── context_input_selection.py ← Optional declared-source child composition and validation (API model or configured session); ordinary shared continuity stays default (§6 Selected first-input sources)
|
||||
├── main_context_authority.py ← Deep-copies the context authority; replaces only oversized raw result strings with source-resolvable narrative or a typed gap
|
||||
├── client_surface.py ← Closed-key bounded client-surface normalizer; surface identity excludes viewport/narrow_layout (§4 WebSocket protocol)
|
||||
├── context_fit.py ← Deterministic Max/Low/Nano context projections from one immutable core with labelled measurement + typed reclaim deficit; owns the ONE message-side transcript cache seal; no routing/retry/global-mode authority (§6 Context fitting, retry, and compaction)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ A workspace task's completion compares against the captured preflight base (task
|
|||
|
||||
`promote_chat_to_task`, `route_to_project`, `steer_task` and `ensure_project_scope` ride one receipt rail: an act succeeds only once its token-matched supervisor facts are durable in the task result, queue snapshot, annotation or mailbox authority; among several possible tasks the LLM chooses, code auto-delivers only the unambiguous one-target case, and an unconfirmed or stale receipt fails visibly instead of launching a second root. Receipts are retained per `(owner message, routing token)`: an earlier act's receipt stays readable by its token (`chat_annotation_receipt`) while the message's latest row is the UI projection and the picker's liveness test. A KNOWN rejection returns `rejected` with its reason, never a timeout, so `UNCONFIRMED` keeps its one meaning: no matching receipt exists. A receipt proves admission, not completion; an unread indicator proves a visible revision, not memory isolation.
|
||||
|
||||
WHO is speaking is ONE host-minted fact on the event (`control_routing._routing_issuer`; the model has no argument): an OWNER TURN (stamped by the owner door: `is_direct_chat` and `run_origin.owner_ingress`) or a TASK speaking for itself — a promoted root (inheriting the stamp as ancestry, not issuer), a root relaying an owner message it just drained, or a consciousness wake-up, Presence event or direct-lane auto-resume template, which nobody typed (a wake's promotes mint consciousness roots inheriting its origin, ledger category and autonomy level). An owner turn's steer travels as owner text: `[Message from my human]`, the owner corpus, the generation bump superseding a reviewed answer, the issuing chat's registry-lane room veto (a Project room reaches its own roots, Main every host-listed root) and the owner acknowledgement. A task's own words NEVER travel as owner text: the task-message writer shared with `forward_to_worker` sends them as `independent_task` provenance to any host-listed active independent root (hidden roots included, no room veto; a Presence sender only its own binding's work, chapter 12), rendered `[Message from independent task <id>]`, with no owner corpus (`owner_source_sha256` and the acceptance premises stay the owner's) and no attachments, confirmed WRITTEN or refused with the host's reason. A relay keys/publishes its acknowledgement on the drained owner message; other task acts use their own synthetic receipt id without chat acknowledgement. Neither grants owner authority; author and target ride the receipt and one `task_message_routed` Logs row, and the receiver's `task_message_injected` row names the sender. Independent roots learn the roster from an `[INDEPENDENT_ROOTS]` TAIL note (`peer_roster.py`, `ROSTER_NOTE_CAP` = 40 rows shown, cut disclosed), appended only on roster change, never merged into a sent row. Direct rows omit unrecorded facts: suggested name, start, the typed `run_origin` subset (provenance, never authority) and the waits the root's durable result names (owner, review, model, budget), each dated by its own start (`owner_wait.parked_at`, the model wait's `started_at`, the pause's `paused_at`), never the task's; queue status stays separate; the direct fragment carries a live turn's name, start and origin until its result exists. Messaging is model judgment; `forward_to_worker` also reaches inline Presence via its existing mailbox (§12). A root may publish bounded `update_focus(text, source_ref)` for other live roots, shown in grouped notes and paginated `live_roots` while unsettled (waits included) and retained by `recent_tasks` on dormant results; publishing is the model's judgment; no project-level focus, refresh or timer. `update_focus` answers the `source_ref` through the reader it names under the caller's registry admission (`disabled_tools` included) and retains the exact answer (≤256 KiB) write-once on the canonical root as `focus.source_handle`, a native `task_source` ref peers read from any drive via `get_task_result(include_focus_source=True, focus_source_sha256=…)` against the PHYSICAL author's record; the digest selects the immutable historical file, so no later focus/retry can substitute its evidence (the roster's `retained_source`). A refusing reader or changed snapshot refuses the focus (`FOCUS_SOURCE_UNRESOLVED`); the roster drops only settled roots' focus. Focus has authored time separate from host observation, no TTL or owner authority. Explicit authorized project journal/workpad reads return exactly the requested source; foreign scoped writes refuse; children/Presence keep ceilings.
|
||||
WHO is speaking is ONE host-minted fact on the event (`control_routing._routing_issuer`; the model has no argument): an OWNER TURN (stamped by the owner door: `is_direct_chat` and `run_origin.owner_ingress`) or a TASK speaking for itself — a promoted root (inheriting the stamp as ancestry, not issuer), a root relaying an owner message it just drained, or a consciousness wake-up, Presence event or direct-lane auto-resume template, which nobody typed (a wake's promotes mint consciousness roots inheriting its origin, ledger category and autonomy level). An owner turn's steer travels as owner text: `[Message from my human]`, the owner corpus, the generation bump superseding a reviewed answer, the issuing chat's registry-lane room veto (a Project room reaches its own roots, Main every host-listed root) and the owner acknowledgement. A task's own words NEVER travel as owner text: the task-message writer shared with `forward_to_worker` sends them as `independent_task` provenance to any host-listed active independent root (through `forward_to_worker` a root inside the sender's own tree is a `peer_task` recipient instead; `steer_task` keeps `independent_task`; hidden roots included, no room veto; a Presence sender only its own binding's work, chapter 12), rendered `[Message from independent task <id>]`, with no owner corpus (`owner_source_sha256` and the acceptance premises stay the owner's) and no attachments, confirmed WRITTEN or refused with the host's reason. A relay keys/publishes its acknowledgement on the drained owner message; other task acts use their own synthetic receipt id without chat acknowledgement. Neither grants owner authority; author and target ride the receipt and one `task_message_routed` Logs row, and the receiver's `task_message_injected` row names the sender. Independent roots learn the roster from an `[INDEPENDENT_ROOTS]` TAIL note (`peer_roster.py`, `ROSTER_NOTE_CAP` = 40 rows shown, cut disclosed), appended only on roster change, never merged into a sent row. Direct rows omit unrecorded facts: suggested name, start, the typed `run_origin` subset (provenance, never authority) and the waits the root's durable result names (owner, review, model, budget), each dated by its own start (`owner_wait.parked_at`, the model wait's `started_at`, the pause's `paused_at`), never the task's; queue status stays separate; the direct fragment carries a live turn's name, start and origin until its result exists. Messaging is model judgment; `forward_to_worker` also reaches inline Presence via its existing mailbox (§12). A root may publish bounded `update_focus(text, source_ref)` for other live roots, shown in grouped notes and paginated `live_roots` while unsettled (waits included) and retained by `recent_tasks` on dormant results; publishing is the model's judgment; no project-level focus, refresh or timer. `update_focus` answers the `source_ref` through the reader it names under the caller's registry admission (`disabled_tools` included) and retains the exact answer (≤256 KiB) write-once on the canonical root as `focus.source_handle`, a native `task_source` ref peers read from any drive via `get_task_result(include_focus_source=True, focus_source_sha256=…)` against the PHYSICAL author's record; the digest selects the immutable historical file, so no later focus/retry can substitute its evidence (the roster's `retained_source`). A refusing reader or changed snapshot refuses the focus (`FOCUS_SOURCE_UNRESOLVED`); the roster drops only settled roots' focus. Focus has authored time separate from host observation, no TTL or owner authority. Explicit authorized project journal/workpad reads return exactly the requested source; foreign scoped writes refuse; children/Presence keep ceilings.
|
||||
|
||||
`steer_task` relays the owner's exact ingress bytes only on the turn's FIRST routing act, while it still acts on the message that started it; the window ends with the latest owner message the turn DRAINED (`ToolContext.last_owner_delivery`, stamped at the loop's mailbox drain; a message merely written to the mailbox ends nothing) or with a landed promote/route/steer receipt already on the origin message (a refused or unconfirmed act carried nothing, so the next act still relays). Past either, the turn RELAYS its own words, its receipt keyed on the message relayed (the drained delivery's client id, else the synthetic `agent-steer:<routing token>` id), never again on an origin this turn already routed. An agent-authored steer belonging to no owner message earns its receipt under that synthetic id, confirmable through the same `routing_wait` poll; no chat row carries the id, and the owner message's own receipt (what a later decision turn reads) stands. Each steer's mailbox entry is keyed by its routing token, so several instructions under one origin are several deliveries while a retried emit of one steer stays one.
|
||||
|
||||
|
|
@ -319,13 +319,13 @@ After alternatives fail, cognition retains one `loop_transport.py` episode. A fr
|
|||
|
||||
Ordinary delegation requests no extra engine review panel; new ordinary runs on Claudexor 3.9.8+ default to no panel — a version boundary of the behavior, not a second release pin. The start receipt's `engine_version` is the handshaken serving version, distinct from the release pin; engine review outcome, execution success, the parent's integration decision and Ouroboros review gates remain separate facts. Delegated snapshot capture stays relative to its recorded baseline: committed bytes can be captured with a disclosed `head_moved`, the instruction still forbids committing, and the distinct `self_worktree` unchanged-HEAD check is preserved.
|
||||
|
||||
Children coordinate through `tree_note` and `tree_read`; read-only children can read/list the project-scoped knowledge store without writing its index; only the parent may use `override_delegation_constraint`, and a `review_requested` note carries an exact evidence reference/hash and wakes the parent without starting a paid cycle. Read-only and acting children alike hold the descendant-scoped `forward_to_worker`, `peek_task`, `cancel_task` and `discard_child_result` controls, and recursive delegation never widens filesystem, budget, depth, deadline, commit or owner authority. `forward_to_worker` also reaches the child's parent or a sibling (same parent and root) as `peer_task` provenance, its prefix naming the stamped `relation` (`sibling`|`parent`): context-only, relay refused, 8000-char bound; a queued target gets the row with a `queued` receipt (not started, not read). An accepted terminal write keeps mail no attempt read as `unread_mailbox` (exact rows, no ACK, union-only), shown by `get_task_result`. `delegation_budget` governs descendants (`may_delegate`, `may_fan_out`, additive depth provenance; a free-form intent note is never authority); persisted admission facts outrank later Settings changes, and a lower permitted depth is reported `capability_reduced`, never a silent flat tree.
|
||||
Children coordinate through `tree_note` and `tree_read`; read-only children can read/list the project-scoped knowledge store without writing its index; only the parent may use `override_delegation_constraint`, and a `review_requested` note carries an exact evidence reference/hash and wakes the parent without starting a paid cycle. Read-only and acting children alike hold the descendant-scoped `forward_to_worker`, `peek_task`, `cancel_task` and `discard_child_result` controls, and recursive delegation never widens filesystem, budget, depth, deadline, commit or owner authority. `forward_to_worker` also reaches any task in the same durable tree (the blackboard's `root_task_id` scope: the parent, a sibling, a cousin, the root itself, a predecessor's child from a continuation root) as `peer_task` provenance, its prefix naming the stamped `relation` (`sibling`|`parent`|`tree`; one label map, `owner_mailbox.PEER_RELATION_LABELS`, read by the drain prefix, the sender receipt and telemetry): context-only (no owner directive, no corpus growth), relay refused, cancel state read strictly, 8000-char bound; a shared root label grants context, never ancestry (relay and steering keep `durable_descendant_of`); a queued target gets the row with a `queued` receipt (not started, not read). An accepted terminal write keeps mail no attempt read as `unread_mailbox` (exact rows, no ACK, union-only), shown by `get_task_result`. `delegation_budget` governs descendants (`may_delegate`, `may_fan_out`, additive depth provenance; a free-form intent note is never authority); persisted admission facts outrank later Settings changes, and a lower permitted depth is reported `capability_reduced`, never a silent flat tree.
|
||||
|
||||
**Registry.** `configured_subagents.py` owns `OUROBOROS_SUBAGENTS`: strict `{enabled, items}`, at most ten rows with hidden `subagent_id`, route-handle name (§3 Available subagents), any-language `recommended_use`, normalized API/session route, optional effort, account pin, access and row `enabled`. Session access defaults to `full` or the owner's `workspace_write`; snapshots lacking access retain workspace_write. Legacy env inputs fail closed. Row `enabled` defaults true, rejects nonbooleans and serializes only false, preserving fingerprints/receipts. Off rows retain configuration but leave catalog, alternatives, legacy matching, local autostart and reviewer resolution. Explicit selection returns `subagent_disabled`, distinct from global `subagents_disabled` and live unavailability. Tools take handle or stored id; roster edits refuse twins; records name their own engine. Captured snapshots never consult live settings. Descriptions guide the LLM, never host ranking/keyword routing; exact selection starts that route or refuses without substitution.
|
||||
|
||||
**Scheduling.** `schedule_subagent` requires `subagent_id`, `objective` and `expected_output`. Optional `workspace_root` selects the start (readonly Git subdirectories included); omission inherits it. Focus grants no writes and preserves inherited reads. The folder survives its contract, admission, recovery and session project binding. `parent_workspace` records root/mode/source before child selection, or missing focus without inventing a root; read roots derive from delegation edges and canonical runtime, without a grant list. Relative roots stay local; explicit wrong roots refuse. Other fields name context, constraints, capabilities, write surface, narrower deadline, delegation budget and acceptance claims. `access=inherit` (default or omitted) preserves the configured session access; `readonly|workspace_write` can only lower it. API-model rows ignore this field with a result disclosure; `write_surface` stays the read/write authority. No model-visible lane/executor axis or public `effort` override exists: the selected row is the complete execution choice, lineage/bounds/route/budget are host-derived, and omission never inherits the parent's acceptance claims. `subagent_runtime.select_subagent_snapshot` copies an immutable snapshot of the exact enabled row into the child task: an `api_model` row becomes an ordinary recursive API child on that exact model/effort, an `agent_session` row an ordinary recursive Ouroboros nanny on that exact external route. Burst cash (each sibling launched before the first sibling's first response pays its own prefix write on cache-write-priced routes) is a tool-description affordance, never host-scheduled; so is an exchange of addressed turns, stated in `objective`/`constraints` (never a contract field or host section; no stage, topology or participant count fixed). The old lane/executor resolver serves only old durable records: for historical lane envelopes `schedule_subagent` reports the requested lane only (effective facts remain on the dispatched child record), and a task carrying `configured_subagent` goes straight to `subagent_runtime`, so legacy policy cannot reinterpret an active selection.
|
||||
|
||||
**Selected first-input sources.** `schedule_subagent(input_sources="declared")` selects a scheduled API child's declared assignment/evidence, full SYSTEM/BIBLE, actual authority/tools and own observations. Default `shared` preserves continuity independently of disk `memory_mode`. `context_input_selection.py` excludes automatic identity/WORLD, shared dialogue/knowledge, project workpad/journal, inherited context/attachments/review notes, rosters/tree summaries and installation diagnostics. Predecessors retain pointers/read grants, not narratives. Task contracts preserve and inherit selection through descendants, restore and rebind; unsupported root/session shapes and direct delegated start/retry refuse before dispatch.
|
||||
**Selected first-input sources.** `schedule_subagent(input_sources="declared")` selects a scheduled child's declared assignment/evidence, full SYSTEM/BIBLE, actual authority/tools and own observations, for an API-model child and for a configured-session child alike (the receipt names the session's two recipients: the nanny's host-composed core and the leaf's compiled work order; harness-loaded project files and vendor memory stay unobserved, and the inherited `delegation_budget.intent_note` is disclosed as retained parent advice). Default `shared` preserves continuity independently of disk `memory_mode`. `context_input_selection.py` excludes automatic identity/WORLD, shared dialogue/knowledge, project workpad/journal, inherited context/attachments/review notes, rosters/tree summaries and installation diagnostics. Predecessors retain pointers/read grants, not narratives. Task contracts preserve and inherit selection through descendants, restore and rebind; a root, a missing task identity, an empty route or an unknown route kind refuses in the composition validator, while a direct delegated start or retry composes the same selected contract.
|
||||
|
||||
Selection is not filesystem confinement, semantic/prior independence, a first-position gate or a model/effort change. Reads, tools and later mail/collaboration may broaden evidence. The author decides the retained-first-position boundary and exchange of originals; qualification covers physical sends through that boundary, not merely request one or `memory_mode=empty`. `test_first_input_selection_wire.py` exercises native HTTP with tools, compatibility retry, explicit rebind and first-position mail, including early broadening; automatic fallback/overflow-compaction and vendor-hidden input remain unqualified.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ The context-delivery registry:
|
|||
| Plan review (`tools/plan_review.py`) | full for a SELF-MODIFICATION plan; otherwise a runtime heading-derived navigation map, never a copy | full for a self-modification plan (`api_chat` inline, `agent_session` mandatory full read); otherwise book navigation + a resolvable pointer | not resident: a named on-demand pointer; a reviewer needing it returns `need_evidence` with an exact `::lines=A-B` range |
|
||||
| Deep self-review (`deep_self_review.py`) | full inline through shared tier 1 on native and session deliveries, without a duplicate-read demand; the seven-file memory whitelist stays byte-exact inline with per-entry dispositions | Tier 3: book navigation and chapters on demand | Tier 2 within this row’s transcript-bound share; deep keeps its own report criteria and CHECKLISTS navigation (ARCHITECTURE §6 "Deep self-review") |
|
||||
|
||||
Scheduled API-child `input_sources="declared"` selects assignment/common sources; governance delivery and ordinary shared children stay unchanged. Qualify actual physical sends and source receipts, not disk `memory_mode=empty`. Tools/mail may broaden inputs; first position and exchange remain author decisions. Mechanism/limits: ARCHITECTURE §6 "Selected first-input sources".
|
||||
A scheduled child's `input_sources="declared"` (API model or configured session) selects assignment/common sources; governance delivery and ordinary shared children stay unchanged. Qualify actual physical sends and source receipts, not disk `memory_mode=empty`. Tools/mail may broaden inputs; first position and exchange remain author decisions. Mechanism/limits: ARCHITECTURE §6 "Selected first-input sources".
|
||||
|
||||
Scope's change-relative source manifest (`tools/scope_required_sources.py`)
|
||||
names touched protected runtime, frozen contracts and prompts, their declared
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Machine extraction of the `docs/ARCHITECTURE.md` "Data layout (`~/Ouroboros/`)" tree — the durable-file orientation carrier (this tree's counterpart of the reference PERSISTENCE_OWNERS derivation checklist) — regenerated by `python scripts/regenerate_inventories.py`. Do not edit. Every entry is probed against reality: repo entries must exist as tracked paths; data-plane entries must appear as a literal in the runtime sources that construct them. A durable file renamed or removed in code while its tree row survives = red (`tests/test_generated_inventories.py`).
|
||||
|
||||
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 633-726; UTF-8 SHA-256 `d34254b54d070f2e7ea7628cb6562e3bc037298307b11d0851871810dd266469`.
|
||||
Source: `docs/architecture/01-high-level-architecture.md`, physical LF lines 633-726; UTF-8 SHA-256 `0f532622f97503457ea5a57e6e1fc0097224f17595d7adcebe88745df3cb9646`.
|
||||
|
||||
- entries: **83** (code-ref: 76, repo-dir: 6, repo-path: 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -82,8 +82,10 @@ def _validate_declared_input_task(task: Dict[str, Any]) -> None:
|
|||
role = task.get("delegation_role") or meta.get("delegation_role") or lineage.get("delegation_role")
|
||||
actor = task.get("configured_subagent") or meta.get("configured_subagent") or {}
|
||||
route = actor.get("route") if isinstance(actor, dict) else {}
|
||||
if not task.get("id") or role != "subagent" or not isinstance(route, dict) or route.get("kind") != "api_model":
|
||||
raise ValueError("INPUT_SOURCE_SELECTION_UNSUPPORTED: declared inputs require a scheduled API-model child")
|
||||
if (not task.get("id") or role != "subagent" or not isinstance(route, dict)
|
||||
or route.get("kind") not in {"api_model", "agent_session"}):
|
||||
raise ValueError("INPUT_SOURCE_SELECTION_UNSUPPORTED: declared inputs require a scheduled child "
|
||||
"(API model or configured session)")
|
||||
|
||||
|
||||
def _capture_declared_context_core(
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ def _addressed_wakes(ctx: Any, state: dict[str, Any]) -> list[dict[str, Any]]:
|
|||
"provenance": str(entry.get("provenance") or "owner"),
|
||||
"source_task_id": str(entry.get("source_task_id") or ""),
|
||||
"relayed_from_task_id": str(entry.get("relayed_from_task_id") or ""),
|
||||
# The peer relation the drain projected (sibling / parent): the sender's
|
||||
# The peer relation the drain projected (a PEER_RELATION_LABELS key): the sender's
|
||||
# typed place, carried so a wake never signs a child or sibling as an
|
||||
# ancestor or owner (the same fact the round-top prefix reads).
|
||||
**({"relation": str(entry["relation"])} if str(entry.get("relation") or "") else {}),
|
||||
|
|
|
|||
|
|
@ -24,14 +24,22 @@ KIND_TASK_MESSAGE = "task_message"
|
|||
# own prefix, enters no owner corpus and supersedes no reviewed answer.
|
||||
PROVENANCE_INDEPENDENT_TASK = "independent_task"
|
||||
# Provenance of a contribution written by a task INSIDE the recipient's tree
|
||||
# that holds no authority over it: a sibling (same parent) or a child speaking
|
||||
# to its parent. ``relation`` names the recipient's place relative to the
|
||||
# sender (``sibling`` / ``parent``) and is stamped at write time; the render
|
||||
# prefix reads it, so a peer is never signed "ancestor" or "owner". Like an
|
||||
# independent task's words it is context the receiving model judges: it
|
||||
# enters no owner corpus and records no directive.
|
||||
# that holds no authority over it: a sibling (same parent), a child speaking
|
||||
# to its parent, or any other task sharing the recipient's durable root (the
|
||||
# blackboard's own scope). ``relation`` names the recipient's place relative
|
||||
# to the sender and is stamped at write time; the render prefix reads it, so a
|
||||
# peer is never signed "ancestor" or "owner". Like an independent task's words
|
||||
# it is context the receiving model judges: it enters no owner corpus and
|
||||
# records no directive.
|
||||
PROVENANCE_PEER_TASK = "peer_task"
|
||||
PEER_TASK_RELATIONS = frozenset({"sibling", "parent"})
|
||||
# The one relation map: the recipient-side prefix label and the sender-side
|
||||
# receipt phrase are the two perspectives of the same stamped fact. The drain
|
||||
# prefix, the forward_to_worker receipts and the telemetry filter all read it.
|
||||
PEER_RELATION_LABELS = {
|
||||
"sibling": {"prefix": " (sibling)", "receipt": "your sibling"},
|
||||
"parent": {"prefix": " (your child)", "receipt": "your parent"},
|
||||
"tree": {"prefix": " (same tree)", "receipt": "a task in your tree"},
|
||||
}
|
||||
# Upper bound on one forwarded task-message body. Every message later enters
|
||||
# plan packets through dialogue evidence, so forward_to_worker refuses an
|
||||
# oversized body whole — never truncated, never spilled to an artifact handle
|
||||
|
|
@ -298,7 +306,7 @@ def write_task_message(
|
|||
"""Write an addressed task-tree message without forging owner provenance.
|
||||
|
||||
``relation`` is the peer_task sender's typed place relative to the
|
||||
recipient (``sibling`` / ``parent``); stored only when non-empty.
|
||||
recipient (a ``PEER_RELATION_LABELS`` key); stored only when non-empty.
|
||||
``sender_origin`` is the sending run's host-recorded origin (a Presence
|
||||
room/event), never the author of the words it quotes.
|
||||
"""
|
||||
|
|
@ -431,9 +439,9 @@ def deliver_task_message(
|
|||
elif provenance == PROVENANCE_PEER_TASK:
|
||||
# A contribution from inside the tree without authority over the
|
||||
# recipient: the stamped relation names the sender's place, so a
|
||||
# child writing up or a sibling writing across is never signed
|
||||
# "ancestor" (an authority the sender does not hold).
|
||||
label = {"sibling": " (sibling)", "parent": " (your child)"}.get(relation, "")
|
||||
# child writing up, a sibling writing across or a cousin in the same
|
||||
# tree is never signed "ancestor" (an authority the sender does not hold).
|
||||
label = PEER_RELATION_LABELS.get(relation, {}).get("prefix", "")
|
||||
prefix = f"[Message from peer task {source}{label}]"
|
||||
else:
|
||||
prefix = f"[Message from ancestor task {source}]"
|
||||
|
|
|
|||
|
|
@ -538,10 +538,16 @@ def peer_relation_to(
|
|||
tid: str,
|
||||
data: Dict[str, Any],
|
||||
) -> str:
|
||||
"""``parent`` / ``sibling`` when ``tid`` is the caller's parent or shares its
|
||||
parent inside ONE durable tree, else ``""``. The caller's own lineage comes
|
||||
from its task metadata, falling back to its durable result; a recipient in
|
||||
another root is never a peer even when the parent ids coincide."""
|
||||
"""``parent`` / ``sibling`` / ``tree`` when ``tid`` is the caller's parent,
|
||||
shares its parent, or shares its durable tree root (the blackboard's own
|
||||
``root_task_id`` scope: a cousin, an uncle, the root itself, a predecessor's
|
||||
child reached by a continuation root), else ``""``. The caller's own lineage
|
||||
comes from its task metadata, falling back to its durable result; a caller
|
||||
with no parent is a root and its own tree. A recipient with no root carrier
|
||||
and no parent is its own root (a direct-chat root); a missing sibling root
|
||||
is never inferred, and a recipient in another root is never a peer even
|
||||
when the parent ids coincide. Shared-root labels grant context, never
|
||||
ancestry: relay and steering keep ``durable_descendant_of``."""
|
||||
from ouroboros.task_status import load_effective_task_result
|
||||
|
||||
if tid == current_task_id:
|
||||
|
|
@ -552,20 +558,21 @@ def peer_relation_to(
|
|||
own = load_effective_task_result(status_drive_root, current_task_id) or {}
|
||||
caller_parent = caller_parent or str(own.get("parent_task_id") or "").strip()
|
||||
caller_root = caller_root or str(own.get("root_task_id") or "").strip()
|
||||
if not caller_parent or not caller_root:
|
||||
return ""
|
||||
if tid == caller_parent:
|
||||
relation = "parent"
|
||||
elif str(data.get("parent_task_id") or "").strip() == caller_parent:
|
||||
relation = "sibling"
|
||||
else:
|
||||
if not caller_root and not caller_parent:
|
||||
caller_root = current_task_id
|
||||
if not caller_root:
|
||||
return ""
|
||||
recipient_parent = str(data.get("parent_task_id") or "").strip()
|
||||
recipient_root = str(data.get("root_task_id") or "").strip()
|
||||
# A direct-chat root may have no root_task_id carrier. The child's exact
|
||||
# parent/root pair still proves this root; never infer a missing sibling root.
|
||||
if not recipient_root and not data.get("parent_task_id") and tid == caller_root == caller_parent:
|
||||
if not recipient_root and not recipient_parent:
|
||||
recipient_root = tid
|
||||
return relation if recipient_root == caller_root else ""
|
||||
if recipient_root != caller_root:
|
||||
return ""
|
||||
if caller_parent and tid == caller_parent:
|
||||
return "parent"
|
||||
if caller_parent and recipient_parent == caller_parent:
|
||||
return "sibling"
|
||||
return "tree"
|
||||
|
||||
|
||||
def peer_contribution_admission(
|
||||
|
|
@ -580,8 +587,8 @@ def peer_contribution_admission(
|
|||
"""``(relation, refusal)`` for a ``forward_to_worker`` recipient that is not
|
||||
the caller's descendant (serial addressed turns, peer contributions).
|
||||
|
||||
``relation`` is ``parent``/``sibling`` when the recipient is a peer inside the
|
||||
caller's tree and the write may proceed. ``refusal`` is a typed ``ToolResult``
|
||||
``relation`` is a ``PEER_RELATION_LABELS`` key (``parent``/``sibling``/``tree``)
|
||||
when the recipient is a peer inside the caller's tree and the write may proceed. ``refusal`` is a typed ``ToolResult``
|
||||
when it IS a peer but relay was asked (an ancestor-only act), or its
|
||||
cancellation is pending, or that state cannot be read: a peer holds no
|
||||
authority over the recipient, so it never writes blind — the fail-soft
|
||||
|
|
@ -590,6 +597,7 @@ def peer_contribution_admission(
|
|||
path. ``("", None)`` means the recipient is no peer at all.
|
||||
"""
|
||||
from ouroboros.cancel_intents import cancel_pending
|
||||
from ouroboros.owner_mailbox import PEER_RELATION_LABELS
|
||||
from ouroboros.tools.tool_result import ToolResult
|
||||
|
||||
relation = peer_relation_to(status_drive_root, current_task_id, metadata, tid, data)
|
||||
|
|
@ -598,7 +606,7 @@ def peer_contribution_admission(
|
|||
if relayed_from:
|
||||
return "", ToolResult(status="blocked", code="LEGACY_BLOCKED", text=(
|
||||
f"⚠️ TASK_FORBIDDEN: a relayed message reaches only your own descendants; "
|
||||
f"task {tid} is your {relation} — relay is an ancestor-only act."))
|
||||
f"task {tid} is {PEER_RELATION_LABELS[relation]['receipt']} — relay is an ancestor-only act."))
|
||||
try:
|
||||
pending = cancel_pending(status_drive_root, tid, strict=True)
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ from typing import Any, Mapping
|
|||
def input_source_selection_receipt(task: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Composition fact retained by existing work-order/model-send sources.
|
||||
|
||||
This selects automatic inputs, never filesystem access or semantic purity.
|
||||
This selects automatic inputs, never filesystem access or semantic purity;
|
||||
an API-model child and a configured-session child (nanny plus leaf) carry
|
||||
the same selection, each recipient named in ``limitations``.
|
||||
The selection persists for the task; first-position/mail ordering is the
|
||||
caller's ordinary collaboration procedure, not a second host controller.
|
||||
"""
|
||||
|
|
@ -24,6 +26,7 @@ def input_source_selection_receipt(task: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"SYSTEM.md and BIBLE.md; existing reference-book projections",
|
||||
"explicit assignment, question, evidence and normalized task authority",
|
||||
"runtime access, tools, workspace, clock, resource and budget facts",
|
||||
"the inherited delegation_budget.intent_note: parent-authored advice carried in the task contract, never authority",
|
||||
"this child's own retained progress, tool and event history",
|
||||
],
|
||||
"omitted_automatic": [
|
||||
|
|
@ -33,7 +36,7 @@ def input_source_selection_receipt(task: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"parent context, notes, review_notes, predecessor narrative and inherited attachments",
|
||||
"task-tree blackboard, routing manifests and other-task summaries",
|
||||
],
|
||||
"lifetime": "Entire task, including tools, retries, fallback, compaction and selected API descendants.",
|
||||
"lifetime": "Entire task, including tools, retries, fallback, compaction and selected descendants.",
|
||||
"collaboration": (
|
||||
"Ordinary task messages remain available; input selection imposes no collaboration order. "
|
||||
"The host does not gate mailbox delivery or detect the first position."
|
||||
|
|
@ -46,7 +49,9 @@ def input_source_selection_receipt(task: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"limitations": (
|
||||
"No sandbox, no semantic filtering of declared facts or governance/authority, and no "
|
||||
"promise about learned priors. Vendor-side context is unobserved. This receipt records "
|
||||
"host composition, not blanket blindness. Native agent sessions are unsupported."
|
||||
"host composition, not blanket blindness. A configured-session child has two recipients: "
|
||||
"the nanny receives this host-composed core and the session leaf receives the compiled work "
|
||||
"order; files the harness loads from its project and vendor-side memory are unobserved."
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -489,7 +489,8 @@ def _tree_ledger_snapshot_rows(
|
|||
|
||||
def _format_tree_ledger_row(row: Dict[str, Any]) -> str:
|
||||
flag = " ⚠needs_parent_attention" if row.get("needs_parent_attention") else ""
|
||||
who = str(row.get("role") or "") or str(row.get("task_id") or "")[:8]
|
||||
# Role AND the full task id: a row is addressable with forward_to_worker/peek_task.
|
||||
who = " ".join(part for part in (str(row.get("role") or ""), str(row.get("task_id") or "")) if part)
|
||||
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
|
||||
payload_note = ""
|
||||
if str(row.get("kind") or "") == DELEGATION_CONSTRAINT_KIND and payload:
|
||||
|
|
|
|||
|
|
@ -163,24 +163,14 @@ _SCHEDULE_SUBAGENT_DESCRIPTION = (
|
|||
"is allowed within configured depth/cap limits — use delegation_intent / may_mutate / "
|
||||
"may_fan_out to tell a child to recurse further, so a 'maximum subagents / grandchildren' "
|
||||
"request propagates structurally instead of collapsing into one flat layer. "
|
||||
"BURST + ABSORB: when several children are INDEPENDENT, emit them in ONE batch (parallel "
|
||||
"schedule_subagent calls in the same round) so they run concurrently, then absorb with "
|
||||
"wait_tasks(any_terminal) — handling whichever finishes first — instead of scheduling and "
|
||||
"blocking on them one at a time with serial wait_task calls — on cache-write-priced "
|
||||
"BURST + ABSORB: independent children scheduled in the same round run concurrently; absorb "
|
||||
"them with wait_tasks(any_terminal), which returns whichever finishes first. On cache-write-priced "
|
||||
"routes each sibling launched before the first sibling's first response pays its own full "
|
||||
"prefix write, so burst buys latency and spacing buys cash; your call. "
|
||||
"For an independently composed first position, use an API-model child with "
|
||||
"input_sources=declared; put the question and common evidence explicitly in context. "
|
||||
"This omits automatic prior-case memory and inherited parent references while retaining "
|
||||
"governance and actual authority. Omission keeps ordinary shared inputs; memory_mode=empty "
|
||||
"controls only the child drive. The assignment defines any first-position retention and "
|
||||
"subsequent collaboration; this selector imposes no exchange sequence or transport. "
|
||||
"Tool reads and messages add inputs, so an independence claim must account for them; "
|
||||
"learned priors and unobserved vendor context are outside this selection. "
|
||||
"EXCHANGE OF ADDRESSED TURNS: to make children participants whose position is not "
|
||||
"their whole participation, state the rules in objective/constraints (what is interim, "
|
||||
"whom to address, what ends participation); a native child reaches you or a sibling with "
|
||||
"forward_to_worker and waits with await_messages, and its final answer ends its "
|
||||
"whom to address, what ends participation); a native child reaches you, a sibling or any "
|
||||
"task in its tree with forward_to_worker and waits with await_messages, and its final answer ends its "
|
||||
"participation; a session (delegate_start) continues in the SAME session through "
|
||||
"delegate_answer when it can ask mid-run, else a later turn is a NEW run. Always retrieve "
|
||||
"the handoff with get_task_result, wait_task, or wait_tasks before relying on its results."
|
||||
|
|
|
|||
|
|
@ -721,12 +721,6 @@ def _schedule_task(ctx: ToolContext, internal: Dict[str, Any] | None = None, /,
|
|||
except SubagentSelectionError as exc:
|
||||
return _publish_scheduling_refusal(ctx, "error", "TOOL_ARG_ERROR", f"⚠️ {exc.code}: {exc.detail}")
|
||||
route = configured_subagent.get("route") if isinstance(configured_subagent.get("route"), dict) else {}
|
||||
if fields.get("input_sources") == "declared" and route.get("kind") != "api_model":
|
||||
return _publish_scheduling_refusal(
|
||||
ctx, "error", "TOOL_ARG_ERROR",
|
||||
"⚠️ INPUT_SOURCE_SELECTION_UNSUPPORTED (schedule_subagent): input_sources=declared "
|
||||
"requires an api_model actor; agent_session composition is not qualified.",
|
||||
reason="INPUT_SOURCE_SELECTION_UNSUPPORTED")
|
||||
if fields.get("directory_strategy") == "copy" and route.get("kind") != "agent_session":
|
||||
return _publish_scheduling_refusal(
|
||||
ctx, "error", "TOOL_ARG_ERROR",
|
||||
|
|
|
|||
|
|
@ -49,10 +49,11 @@ def schedule_subagent_properties() -> Dict[str, Any]:
|
|||
"type": "string", "enum": ["shared", "declared"],
|
||||
"description": (
|
||||
"Omit or shared for ordinary shared context. declared selects only the authored "
|
||||
"objective/context/constraints and full governance/task authority as automatic inputs; "
|
||||
"objective/context/constraints and full governance/task authority as automatic inputs, "
|
||||
"for an independently composed first position; "
|
||||
"it excludes automatic shared memory, dialogue, project knowledge, inherited parent "
|
||||
"context and attachments. Put common evidence explicitly in context. API-model "
|
||||
"children only; inherited declared cannot be widened by descendants. Selection lasts "
|
||||
"context and attachments. Put common evidence explicitly in context. Inherited "
|
||||
"declared cannot be widened by descendants. Selection lasts "
|
||||
"for the task; the assignment defines first-position retention and collaboration. "
|
||||
"Tool results and messages can broaden the input. This selector prescribes no "
|
||||
"exchange sequence or transport and is not access isolation or a claim about "
|
||||
|
|
|
|||
|
|
@ -1126,11 +1126,13 @@ def _forward_to_worker(
|
|||
ctx: ToolContext, task_id: str, message: str, relayed_from_task_id: str = "",
|
||||
) -> str:
|
||||
"""Write task context to the recipient's mailbox, never owner text.
|
||||
Descendants receive ancestor/relayed context; parent/sibling contributions
|
||||
retain their relation. Listed roots and inline Presence receive independent
|
||||
task context. Receipts prove persistence, not a read, in the recipient's drive."""
|
||||
Descendants receive ancestor/relayed context; contributions inside one tree
|
||||
(parent, sibling, any task sharing the root) retain their relation. Listed
|
||||
roots and inline Presence receive independent task context. Receipts prove
|
||||
persistence, not a read, in the recipient's drive."""
|
||||
from ouroboros.owner_mailbox import (
|
||||
PROVENANCE_INDEPENDENT_TASK, PROVENANCE_PEER_TASK, TASK_MESSAGE_MAX_CHARS, write_task_message,
|
||||
PEER_RELATION_LABELS, PROVENANCE_INDEPENDENT_TASK, PROVENANCE_PEER_TASK, TASK_MESSAGE_MAX_CHARS,
|
||||
write_task_message,
|
||||
)
|
||||
from ouroboros.peer_roster import (
|
||||
durable_descendant_of, independent_message_target, peer_contribution_admission,
|
||||
|
|
@ -1181,9 +1183,10 @@ def _forward_to_worker(
|
|||
relation = ""
|
||||
listed_root = None
|
||||
if not durable_descendant_of(status_drive_root, tid, data, current_task_id):
|
||||
# A peer inside the tree (the caller's parent or sibling) before the host
|
||||
# roster; its typed admission (relay refused, cancel state read strictly)
|
||||
# lives beside the roster's other addressability rules in peer_roster.
|
||||
# A peer inside the tree (the caller's parent, a sibling, or any task sharing
|
||||
# its root) before the host roster; its typed admission (relay refused, cancel
|
||||
# state read strictly) lives beside the roster's other addressability rules
|
||||
# in peer_roster.
|
||||
relation, refusal = peer_contribution_admission(
|
||||
status_drive_root, current_task_id, metadata, tid, data, relayed_from=relayed_from)
|
||||
if refusal is not None:
|
||||
|
|
@ -1193,8 +1196,9 @@ def _forward_to_worker(
|
|||
else:
|
||||
listed_root = independent_message_target(status_drive_root, tid, data)
|
||||
if listed_root is None:
|
||||
return (f"⚠️ TASK_FORBIDDEN: task {tid} is neither a descendant, the parent nor a sibling "
|
||||
"of the current task, nor an active independent root the host lists or an inline Presence mailbox.")
|
||||
return (f"⚠️ TASK_FORBIDDEN: task {tid} is neither a descendant of the current task, nor a task "
|
||||
"in its tree (the parent nor a sibling nor any task sharing its root), nor an active "
|
||||
"independent root the host lists or an inline Presence mailbox.")
|
||||
if relayed_from:
|
||||
return f"⚠️ TASK_FORBIDDEN: a relayed message reaches only your own descendants; task {tid} is an independent recipient."
|
||||
provenance = PROVENANCE_INDEPENDENT_TASK
|
||||
|
|
@ -1247,17 +1251,26 @@ def _forward_to_worker(
|
|||
"(independent_task, never owner text). This proves persistence, not that its model read it; "
|
||||
"if the turn continues, its checkpoint can read it. "
|
||||
f"execution_observation={observation}. Files cannot be attached to messages between tasks.")
|
||||
# A Presence root reached from inside its own tree keeps the shared execution
|
||||
# observation on the receipt (persistence proof, not a read), as the roster path gives it.
|
||||
presence = data.get("execution_observation") if provenance == PROVENANCE_PEER_TASK else None
|
||||
observed = (f" execution_observation={json.dumps(presence, ensure_ascii=False, sort_keys=True)}."
|
||||
if isinstance(presence, dict) and presence.get("kind") == "presence" else "")
|
||||
as_peer = PEER_RELATION_LABELS.get(relation, {}).get("receipt") or relation
|
||||
if receipt == MAIL_QUEUED:
|
||||
as_from = (f" as a message from a peer task (your {relation}; never owner text or an ancestor's steering)"
|
||||
as_from = (f" as a message from a peer task ({as_peer}; never owner text or an ancestor's steering)"
|
||||
if provenance == PROVENANCE_PEER_TASK else " as a message from this task (never owner text)"
|
||||
if listed_root is not None else "")
|
||||
return (f"Message forwarded to task {tid}: written to its mailbox{as_from} ({MAIL_QUEUED}); task {tid} has not "
|
||||
"started, so nothing has read it: it reads it when it starts, and if it ends unstarted its result keeps "
|
||||
"it as unread mail. Files cannot be attached to messages between tasks.")
|
||||
f"it as unread mail.{observed} Files cannot be attached to messages between tasks.")
|
||||
if provenance == PROVENANCE_PEER_TASK:
|
||||
# A Presence turn may already be over: persistence is proven, a read is not.
|
||||
read_note = ("This proves persistence, not that its model read it; if the turn continues, "
|
||||
"its checkpoint can read it." if observed else "it reads it at its next checkpoint.")
|
||||
return (f"Message forwarded to task {tid}: written to its mailbox as a message from a peer task "
|
||||
f"(your {relation}; never owner text or an ancestor's steering); it reads it at its next "
|
||||
"checkpoint. Files cannot be attached to messages between tasks.")
|
||||
f"({as_peer}; never owner text or an ancestor's steering){'.' if observed else ';'} "
|
||||
f"{read_note}{observed} Files cannot be attached to messages between tasks.")
|
||||
if listed_root is not None:
|
||||
return (f"Message forwarded to task {tid}: written to its mailbox as a message from this task "
|
||||
"(never owner text); it reads it at its next checkpoint. Files cannot be attached to messages between tasks.")
|
||||
|
|
@ -1430,11 +1443,12 @@ def get_tools() -> List[ToolEntry]:
|
|||
"name": "forward_to_worker",
|
||||
"description": (
|
||||
"Write an addressed task-tree message into a running or queued task's mailbox: a child "
|
||||
"or descendant of yours (delivered as the ancestor's message), your own parent "
|
||||
"or a sibling (delivered as a message from a peer task naming the relation — "
|
||||
"a contribution it weighs, never steering; relay is refused there), or any active "
|
||||
"independent root the host lists or a source-bound inline Presence turn (a message "
|
||||
"from an independent task). Presence observation gaps are disclosed; a write never "
|
||||
"or descendant of yours (delivered as the ancestor's message), any other task in your "
|
||||
"tree — your parent, a sibling, or any task sharing your root (delivered as a message "
|
||||
"from a peer task naming the relation — a contribution it weighs, never steering; relay "
|
||||
"is refused there), or any active independent root the host lists or a source-bound "
|
||||
"inline Presence turn (a message from an independent task). Presence observation gaps "
|
||||
"are disclosed, also when the Presence root is in your own tree; a write never "
|
||||
"proves a read. It is never labelled owner dialogue, files cannot be attached, the body "
|
||||
"is limited to 8000 chars (longer is refused, never truncated), and the "
|
||||
"result says written, not read: a running task drains it at its next checkpoint, a queued "
|
||||
|
|
|
|||
|
|
@ -339,21 +339,6 @@ def _start_argument_refusal(ctx: ToolContext, text: str, selector_root: str, ret
|
|||
continuation selector shapes one call cannot combine: a retry replays an old
|
||||
key byte-identically while a continuation is a NEW intention over a settled
|
||||
run, and a skill-payload selector run keeps its own target semantics."""
|
||||
from ouroboros.contracts.task_contract import task_input_sources
|
||||
|
||||
# Retry replay bypasses assignment composition, so reject the unsupported
|
||||
# source selection before either start path can prepare or replay a request.
|
||||
if task_input_sources({
|
||||
"task_contract": getattr(ctx, "task_contract", {}),
|
||||
"metadata": getattr(ctx, "task_metadata", {}),
|
||||
}) == "declared":
|
||||
return "", _fail(
|
||||
"delegate_start", "INPUT_SOURCE_SELECTION_UNSUPPORTED",
|
||||
"Declared input selection supports scheduled API-model children only; "
|
||||
"native-session composition is not qualified.",
|
||||
definitely_unrun=True, host_fallback=False,
|
||||
)
|
||||
|
||||
if not text.strip():
|
||||
return "", _fail("delegate_start", "empty_prompt", "prompt is required")
|
||||
refusal = _payload_selector_refusal(selector_root, retry_of, bucket, skill_name)
|
||||
|
|
|
|||
|
|
@ -403,9 +403,13 @@ def evidence_manifest_hash(manifest: Mapping[str, Any]) -> str:
|
|||
|
||||
|
||||
def task_evidence_reader(root: pathlib.Path) -> Callable[[str], Optional[str]]:
|
||||
"""Task-result projection; the evidence resolver hashes, budgets and redacts it."""
|
||||
"""Task-result projection with the WHOLE result; the evidence resolver hashes,
|
||||
budgets and redacts it. One bound: ``EVIDENCE_PER_ITEM_BYTES`` cuts the attached
|
||||
head with the cut named (``truncated_to_N``) while sha256/bytes describe the full
|
||||
projection, and a locator selector reads the original. An inner preview here would
|
||||
hash the preview, so two results differing past it would share one identity and a
|
||||
tail selector would read the omission marker instead of the result."""
|
||||
from ouroboros.task_results import load_task_result
|
||||
from ouroboros.utils import truncate_review_artifact
|
||||
def _read(task_id: str) -> Optional[str]:
|
||||
try:
|
||||
record = load_task_result(root, task_id)
|
||||
|
|
@ -418,9 +422,10 @@ def task_evidence_reader(root: pathlib.Path) -> Callable[[str], Optional[str]]:
|
|||
"status": record.get("status"),
|
||||
"reason_code": record.get("reason_code"),
|
||||
"ts": record.get("ts"),
|
||||
"result": truncate_review_artifact(str(record.get("result") or ""), limit=6_000),
|
||||
}
|
||||
if "terminal_host_notice" in record:
|
||||
# Before the result: a bounded head keeps the host's own disclosure.
|
||||
projection["terminal_host_notice"] = str(record["terminal_host_notice"] or "")
|
||||
projection["result"] = str(record.get("result") or "")
|
||||
return json.dumps(projection, ensure_ascii=False, indent=2, default=str)
|
||||
return _read
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ouroboros.owner_mailbox import PEER_RELATION_LABELS
|
||||
from ouroboros.utils import append_jsonl, utc_now_iso
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -50,7 +51,7 @@ def _handle_task_message_injected(evt: Dict[str, Any], ctx: Any) -> None:
|
|||
"task_id": evt.get("task_id", ""),
|
||||
"source_task_id": evt.get("source_task_id", ""),
|
||||
"provenance": evt.get("provenance", ""),
|
||||
**({"relation": evt["relation"]} if evt.get("relation") in {"parent", "sibling"} else {}),
|
||||
**({"relation": evt["relation"]} if evt.get("relation") in PEER_RELATION_LABELS else {}),
|
||||
"relayed_from_task_id": evt.get("relayed_from_task_id", ""),
|
||||
"text_preview": str(evt.get("text_preview") or "")[:200],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
30
|
||||
The module-map row for context_input_selection.py names both scheduled child
|
||||
routes (API model or configured session) now that declared selection covers
|
||||
configured-session children.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
406
|
||||
Same-tree addressing for forward_to_worker (the `tree` relation beside `sibling`
|
||||
and `parent`, one label map, strict cancel read, the context-only guarantee and
|
||||
the ancestry/context distinction) is stated in the coordination paragraph; the
|
||||
chapter sat 25 bytes under its base when the change landed.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
569
|
||||
Declared first-input selection now covers configured-session children; the
|
||||
paragraph names the session's two recipients, the unobserved harness inputs, the
|
||||
disclosed inherited intent note, and what still refuses at composition; the
|
||||
routing paragraph says a root inside the sender's tree is a peer, not an
|
||||
independent recipient.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
34
|
||||
The declared-selection rule now names both scheduled child routes (API model or
|
||||
configured session); the chapter sat 19 bytes under its base.
|
||||
|
|
@ -1018,7 +1018,9 @@ def test_ancestor_can_relay_to_a_true_grandchild_without_owner_spoof(tmp_path):
|
|||
assert entry["source_task_id"] == "parent"
|
||||
assert entry["relayed_from_task_id"] == "peer"
|
||||
|
||||
# A shared root label plus a parent-cycle is not a descendant proof.
|
||||
# A shared root label plus a parent-cycle is not a descendant proof: the label
|
||||
# admits CONTEXT (a peer_task row the recipient judges, relation ``tree``), never
|
||||
# ancestor steering or a relay, which keep the durable parent chain.
|
||||
write_task_result(
|
||||
tmp_path, "cycle-a", STATUS_RUNNING, parent_task_id="cycle-b",
|
||||
root_task_id="parent", child_drive_root=str(child_drive), result="running",
|
||||
|
|
@ -1027,7 +1029,12 @@ def test_ancestor_can_relay_to_a_true_grandchild_without_owner_spoof(tmp_path):
|
|||
tmp_path, "cycle-b", STATUS_RUNNING, parent_task_id="cycle-a",
|
||||
root_task_id="parent", result="running",
|
||||
)
|
||||
assert "TASK_FORBIDDEN" in _forward_to_worker(ctx, "cycle-a", "must not deliver")
|
||||
relayed = _forward_to_worker(ctx, "cycle-a", "peer evidence", relayed_from_task_id="peer")
|
||||
assert "TASK_FORBIDDEN" in relayed and "ancestor-only act" in relayed
|
||||
assert "message from a peer task" in _forward_to_worker(ctx, "cycle-a", "context, not steering")
|
||||
[row] = drain_owner_entries(child_drive, "cycle-a", seen_ids=set())
|
||||
assert (row["provenance"], row["relation"]) == ("peer_task", "tree")
|
||||
assert row["source_task_id"] == "parent" and not row.get("relayed_from_task_id")
|
||||
|
||||
|
||||
def test_replacement_is_refused_before_gateway_or_post(monkeypatch, tmp_path):
|
||||
|
|
|
|||
|
|
@ -127,9 +127,12 @@ def test_core_catalog_schema_bytes_and_handler_owners_are_stable():
|
|||
# again for TZ-1 V10: forward_to_worker also writes into a queued task's mailbox, so its
|
||||
# description and `task_id` description say "running or queued" and when each reads it.
|
||||
# Presence peer mail expands only forward_to_worker's description; the
|
||||
# catalog comparison preserves every parameter and handler owner.
|
||||
# catalog comparison preserves every parameter and handler owner. Rolled again
|
||||
# for same-tree mail: the forward_to_worker description names its peer addressees
|
||||
# as any other task in the caller's tree (parent, sibling, any task sharing the
|
||||
# root) and says Presence observation gaps are disclosed inside the tree too.
|
||||
assert hashlib.sha256(schema_bytes).hexdigest() == (
|
||||
"d51c4783df9c532f763a7d57c0ff732b86ca397c0875e151fcc1430fb8e3489e"
|
||||
"25ab2df2f9cc9c65e4971b7d4e2a5d1013041933aa070849639a01c9a4398e83"
|
||||
)
|
||||
assert {
|
||||
entry.name: (entry.handler.__module__, entry.handler.__name__)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def test_selected_runtime_omits_other_task_narratives_but_keeps_authority(tmp_pa
|
|||
|
||||
@pytest.mark.parametrize("overrides", [
|
||||
{"delegation_role": "root"},
|
||||
{"configured_subagent": {"route": {"kind": "agent_session"}}},
|
||||
{"configured_subagent": {"route": {"kind": "weird"}}},
|
||||
{"configured_subagent": {}},
|
||||
{"id": ""},
|
||||
])
|
||||
|
|
@ -91,6 +91,22 @@ def test_unsupported_builder_task_class_is_not_silently_shared(tmp_path, overrid
|
|||
context._capture_context_core(env, memory, task, None, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["api_model", "agent_session"])
|
||||
def test_both_scheduled_child_routes_compose_declared_inputs(tmp_path, kind):
|
||||
"""A configured-session child's nanny composes the declared core exactly as an API
|
||||
child does; the receipt rides the capture for either route."""
|
||||
from ouroboros import context
|
||||
|
||||
env, memory = _make_env_and_memory(tmp_path)
|
||||
task = {"id": "child", "type": "task", "delegation_role": "subagent", "text": "DECLARED_QUESTION",
|
||||
"context": "DECLARED_COMMON_FACTS", "configured_subagent": {"route": {"kind": kind}},
|
||||
"task_contract": {"input_sources": "declared"}}
|
||||
core = context._capture_context_core(env, memory, task, None, None)
|
||||
text = core.base_prompt + core.bible_md + core.semi_stable_text + core.dynamic_text + core.user_content_json
|
||||
assert "DECLARED_QUESTION" in text and "DECLARED_COMMON_FACTS" in text
|
||||
assert "Input source selection" in text
|
||||
|
||||
|
||||
def test_workorder_projection_and_digest_retain_selection_receipt():
|
||||
from hashlib import sha256
|
||||
from ouroboros.subagent_work_order import (
|
||||
|
|
@ -107,6 +123,10 @@ def test_workorder_projection_and_digest_retain_selection_receipt():
|
|||
assert reason == "" and projected is not None
|
||||
assert projected["text"] == rendered
|
||||
assert projected["complete_sha256"] == work_order_fingerprint(task)
|
||||
# A configured-session leaf's compiled work order carries the same receipt, naming
|
||||
# the session's two recipients.
|
||||
session_task = {**task, "configured_subagent": {"route": {"kind": "agent_session"}}}
|
||||
assert "two recipients" in compile_external_work_order(session_task)
|
||||
# Existing callers and work-order hashes gain no empty/default receipt.
|
||||
task["task_contract"].pop("input_sources")
|
||||
assert "INPUT SOURCE SELECTION" not in compile_external_work_order(task)
|
||||
|
|
@ -124,6 +144,12 @@ def test_declared_receipt_records_sources_without_prescribing_collaboration_orde
|
|||
assert "first-position handoff" not in receipt[key]
|
||||
assert "before asking" not in receipt[key]
|
||||
assert "model-send/tool-source" in receipt["later_inputs"]
|
||||
# The receipt names what selection keeps and whom it reaches: the inherited intent
|
||||
# note is parent-authored advice that survives, and a configured-session child has
|
||||
# two recipients (nanny core, compiled leaf work order) with harness inputs unobserved.
|
||||
assert any("delegation_budget.intent_note" in item for item in receipt["included"])
|
||||
assert "two recipients" in receipt["limitations"] and "compiled work order" in receipt["limitations"]
|
||||
assert "unsupported" not in receipt["limitations"].lower()
|
||||
|
||||
|
||||
def test_declared_tool_descriptions_leave_exchange_strategy_to_assignment():
|
||||
|
|
@ -135,5 +161,8 @@ def test_declared_tool_descriptions_leave_exchange_strategy_to_assignment():
|
|||
assert "assignment defines" in selection
|
||||
assert "prescribes no exchange sequence or transport" in selection
|
||||
assert "forward_to_worker" not in selection
|
||||
assert "The assignment defines any first-position retention" in schema["description"]
|
||||
# The field carries the one cue for choosing the selector; the tool description no
|
||||
# longer repeats the selector paragraph (one SSOT, fewer cached-prefix bytes).
|
||||
assert "independently composed first position" in selection
|
||||
assert "input_sources=declared" not in schema["description"]
|
||||
assert "Retain the first position via forward_to_worker before" not in schema["description"]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
"""#1351: direct sessions refuse declared inputs before fresh or retry side effects."""
|
||||
"""Declared inputs reach a configured session through the same start path as shared
|
||||
ones: a model-authored delegate_start and its retry replay send the complete normalized
|
||||
contract (selection, declared context, inherited intent note) as authority; the nanny's
|
||||
compiled work order (test_first_input_selection_context) carries the receipt. No route
|
||||
is refused for its kind."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
|
@ -12,74 +16,74 @@ from tests._delegated_transport_shared import ( # noqa: F401 - installs the off
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retry_of", [None, "pending-before-selection"])
|
||||
@pytest.mark.parametrize("carrier", ["contract", "metadata_contract", "metadata"])
|
||||
def test_declared_direct_start_refuses_before_any_start_work(tmp_path, monkeypatch, retry_of, carrier):
|
||||
from ouroboros import claudexor_daemon, delegate_custody
|
||||
def test_declared_direct_start_sends_the_normalized_contract_authority(tmp_path, monkeypatch, carrier):
|
||||
from ouroboros.gateways import claudexor as gateway
|
||||
from ouroboros.tools import delegate
|
||||
|
||||
requests = []
|
||||
|
||||
class RecordingStub(_LiveRunStub):
|
||||
def start_run(self, request, *, idempotency_key=""):
|
||||
requests.append(json.loads(json.dumps(request)))
|
||||
return super().start_run(request, idempotency_key=idempotency_key)
|
||||
|
||||
monkeypatch.setenv("OUROBOROS_SUBAGENT_HARNESS", "some-route=ordinary-model:high")
|
||||
monkeypatch.setattr(gateway, "ClaudexorGateway", lambda *_a, **_kw: RecordingStub())
|
||||
ctx = _nanny_ctx(tmp_path)
|
||||
contract = {"input_sources": "declared", "context": "COMMON_EVIDENCE", "constraints": "PARENT_AUTHORITY",
|
||||
"delegation_budget": {"intent_note": "ADVICE_NOTE"}}
|
||||
if carrier == "contract":
|
||||
ctx.task_contract = {"input_sources": "declared"}
|
||||
ctx.task_contract = contract
|
||||
elif carrier == "metadata_contract":
|
||||
ctx.task_metadata["task_contract"] = {"input_sources": "declared"}
|
||||
ctx.task_metadata["task_contract"] = contract
|
||||
else:
|
||||
ctx.task_metadata["input_sources"] = "declared"
|
||||
|
||||
def unexpected(*_args, **_kwargs):
|
||||
pytest.fail("declared direct start reached argument processing, custody, or transport")
|
||||
result = delegate._delegate_start(ctx, "Inspect the declared evidence", max_seconds=120)
|
||||
|
||||
for name in ("_payload_selector_refusal", "deadline_expired", "bounded_max_seconds",
|
||||
"prepare_work_order_start_binding", "prepare_delegate_start_actor",
|
||||
"_resolve_retry_invocation", "_assignment_instructions", "_emit"):
|
||||
monkeypatch.setattr(delegate, name, unexpected)
|
||||
monkeypatch.setattr(delegate_custody, "custody_root", unexpected)
|
||||
monkeypatch.setattr(claudexor_daemon, "ensure_owned_gateway", unexpected)
|
||||
|
||||
result = delegate._delegate_start(ctx, "Inspect the declared evidence", retry_of=retry_of)
|
||||
|
||||
assert (result.status, result.code) == ("error", "TOOL_REPORTED_FAILURE")
|
||||
payload = json.loads(result.text)
|
||||
assert payload["status"] == "refused" and payload["ok"] is False
|
||||
assert payload["reason"] == "INPUT_SOURCE_SELECTION_UNSUPPORTED"
|
||||
assert payload["definitely_unrun"] is True and payload["host_fallback"] is False
|
||||
assert "scheduled API-model children only" in payload["detail"]
|
||||
assert not list(tmp_path.iterdir())
|
||||
assert payload["status"] == "started", payload
|
||||
assert "INPUT_SOURCE_SELECTION_UNSUPPORTED" not in result.text
|
||||
[request] = requests
|
||||
assert request["prompt"] == "Inspect the declared evidence"
|
||||
sent = "\n".join(str(value) for value in request.values())
|
||||
if carrier != "metadata":
|
||||
# A direct start carries the complete normalized contract as authority: the leaf
|
||||
# hears the selection, the declared evidence and the inherited intent note.
|
||||
assert "HOST TASK CONTRACT AUTHORITY" in sent
|
||||
assert '"input_sources":"declared"' in sent
|
||||
assert "COMMON_EVIDENCE" in sent and "PARENT_AUTHORITY" in sent and "ADVICE_NOTE" in sent
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retry", [False, True])
|
||||
def test_registered_direct_start_publishes_typed_selection_refusal(tmp_path, monkeypatch, retry):
|
||||
from ouroboros import claudexor_daemon, config, safety
|
||||
from ouroboros.tools import delegate
|
||||
def test_registered_direct_start_with_declared_selection_starts(tmp_path, monkeypatch):
|
||||
"""The registered tool path (subagent_id → configured session) starts a declared leaf
|
||||
through work-order preparation and the gateway, like any shared start."""
|
||||
from ouroboros import config, safety
|
||||
from ouroboros.gateways import claudexor as gateway
|
||||
from ouroboros.tools.registry import ToolRegistry
|
||||
|
||||
monkeypatch.setattr(config, "runtime_settings", lambda: {
|
||||
"OUROBOROS_SUBAGENTS": {"enabled": True, "items": [{
|
||||
"subagent_id": "session", "recommended_use": "Inspect the source.",
|
||||
"route": {"kind": "agent_session", "target_id": "codex=gpt-5.6-sol"},
|
||||
"route": {"kind": "agent_session", "target_id": "some-route=weak"},
|
||||
}]},
|
||||
})
|
||||
monkeypatch.setattr(safety, "check_safety", lambda *_a, **_kw: (True, ""))
|
||||
|
||||
def unexpected(*_args, **_kwargs):
|
||||
pytest.fail("registered declared start reached work-order preparation or gateway")
|
||||
|
||||
monkeypatch.setattr(delegate, "prepare_work_order_start_binding", unexpected)
|
||||
monkeypatch.setattr(claudexor_daemon, "ensure_owned_gateway", unexpected)
|
||||
monkeypatch.setattr(gateway, "ClaudexorGateway", lambda *_a, **_kw: _LiveRunStub())
|
||||
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
|
||||
registry._ctx.task_id = "declared-child"
|
||||
registry._ctx.task_contract = {"input_sources": "declared"}
|
||||
args = {"prompt": "Inspect declared evidence"}
|
||||
args.update({"retry_of": "pending-before-selection"} if retry else {"subagent_id": "session"})
|
||||
registry._ctx.task_metadata = {"root_task_id": "declared-child", "parent_task_id": "declared-child"}
|
||||
registry._ctx.task_contract = {"input_sources": "declared", "context": "COMMON_EVIDENCE"}
|
||||
|
||||
result = registry.execute_result("delegate_start", args)
|
||||
result = registry.execute_result("delegate_start", {"prompt": "Inspect declared evidence", "subagent_id": "session"})
|
||||
|
||||
assert (result.status, result.code) == ("error", "TOOL_REPORTED_FAILURE"), result.text
|
||||
assert json.loads(result.text)["reason"] == "INPUT_SOURCE_SELECTION_UNSUPPORTED"
|
||||
assert not (registry._ctx.drive_root / "logs" / "events.jsonl").exists()
|
||||
payload = json.loads(result.text)
|
||||
assert payload.get("status") == "started", result.text
|
||||
assert payload.get("reason") != "INPUT_SOURCE_SELECTION_UNSUPPORTED"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selection", [None, "shared"])
|
||||
@pytest.mark.parametrize("selection", [None, "shared", "declared"])
|
||||
def test_ordinary_direct_start_and_retry_preserve_request_and_parent_context(tmp_path, monkeypatch, selection):
|
||||
from ouroboros.gateways import claudexor as gateway
|
||||
from ouroboros.tools import delegate
|
||||
|
|
|
|||
|
|
@ -191,20 +191,25 @@ def test_declared_parent_cannot_request_shared_descendant(registry):
|
|||
assert registry._ctx.pending_events == []
|
||||
|
||||
|
||||
def test_session_route_refused_before_child_or_attachment_side_effects(registry, monkeypatch):
|
||||
def test_session_route_accepts_declared_with_the_same_selected_contract(registry, monkeypatch):
|
||||
"""A configured-session child (nanny plus leaf) takes the same selection an API child
|
||||
does: the selected contract is stored, inherited inputs are not materialized, and the
|
||||
child dispatches to the harness executor. No route is refused for its kind."""
|
||||
import ouroboros.tools.control_scheduling as scheduling
|
||||
|
||||
monkeypatch.setattr(control, "load_settings", lambda: _settings("agent_session"))
|
||||
monkeypatch.setattr(scheduling, "_prepare_child_drive",
|
||||
lambda *_args, **_kwargs: pytest.fail("drive prepared before route refusal"))
|
||||
monkeypatch.setattr(scheduling, "_materialize_child_attachment_manifest",
|
||||
lambda *_args, **_kwargs: pytest.fail("attachments prepared before route refusal"))
|
||||
result = _schedule(registry, input_sources="declared")
|
||||
assert (result.status, result.code) == ("error", "TOOL_ARG_ERROR"), result.text
|
||||
assert result.meta["reason"] == "INPUT_SOURCE_SELECTION_UNSUPPORTED"
|
||||
assert registry._ctx.pending_events == []
|
||||
# The launch fence may create its lock parent; a refusal creates no child result.
|
||||
assert not list((registry._ctx.drive_root / "task_results").glob("*.json"))
|
||||
lambda *_args, **_kwargs: pytest.fail("inherited inputs were materialized"))
|
||||
result = _schedule(registry, input_sources="declared", context="COMMON_FACTS")
|
||||
assert (result.status, result.code) == ("ok", "OK"), result.text
|
||||
event = registry._ctx.pending_events[-1]
|
||||
assert event["requested_executor"] == "harness"
|
||||
assert event["configured_subagent"]["route"]["kind"] == "agent_session"
|
||||
selected = event["task_contract"]
|
||||
assert selected["input_sources"] == "declared" and selected["context"] == "COMMON_FACTS"
|
||||
assert selected["attachment_manifest"] == []
|
||||
assert "PREVIOUS_CASE" not in json.dumps(selected)
|
||||
assert "INPUT_SOURCE_SELECTION_UNSUPPORTED" not in result.text
|
||||
|
||||
|
||||
def test_contract_selection_is_additive_strict_and_has_consistent_precedence():
|
||||
|
|
|
|||
|
|
@ -394,7 +394,10 @@ class TestForwardToWorkerTool(unittest.TestCase):
|
|||
[row] = drain_owner_entries(sib_drive, "sib")
|
||||
self.assertEqual(row["relation"], "sibling")
|
||||
|
||||
def test_a_siblings_child_and_another_root_are_forbidden(self):
|
||||
def test_a_siblings_child_is_a_tree_peer_and_another_root_stays_forbidden(self):
|
||||
"""A nephew shares the caller's root: admitted as context-only ``peer_task`` with
|
||||
the stamped relation ``tree`` (the blackboard's own scope). The same parent id
|
||||
under another root is still no peer at all."""
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
|
|
@ -402,17 +405,144 @@ class TestForwardToWorkerTool(unittest.TestCase):
|
|||
canonical, _ = self._peer_tree(tmp)
|
||||
ctx = self._caller(canonical)
|
||||
|
||||
nephew = _forward_to_worker(ctx, "nephew", "a sibling's child is no peer of mine")
|
||||
nephew = _forward_to_worker(ctx, "nephew", "a sibling's child shares my tree")
|
||||
stranger = _forward_to_worker(ctx, "stranger", "same parent id, other root")
|
||||
sibling = _forward_to_worker(ctx, "sib2", "same parent, same root")
|
||||
|
||||
for refused in (nephew, stranger):
|
||||
self.assertIn("TASK_FORBIDDEN", refused)
|
||||
self.assertIn("the parent nor a sibling", refused)
|
||||
self.assertIn("nor an active independent root", refused)
|
||||
self.assertIn("message from a peer task", sibling)
|
||||
for task_id in ("nephew", "stranger"):
|
||||
self.assertEqual(drain_owner_entries(canonical, task_id), [])
|
||||
self.assertIn("message from a peer task", nephew)
|
||||
self.assertIn("a task in your tree", nephew)
|
||||
self.assertNotIn("independent", nephew)
|
||||
[row] = drain_owner_entries(canonical, "nephew")
|
||||
self.assertEqual((row["provenance"], row["relation"], row["source_task_id"]),
|
||||
("peer_task", "tree", "me"))
|
||||
self.assertIn("TASK_FORBIDDEN", stranger)
|
||||
self.assertIn("the parent nor a sibling", stranger)
|
||||
self.assertIn("sharing its root", stranger)
|
||||
self.assertIn("nor an active independent root", stranger)
|
||||
self.assertEqual(drain_owner_entries(canonical, "stranger"), [])
|
||||
self.assertIn("your sibling", sibling)
|
||||
|
||||
@staticmethod
|
||||
def _grandchild(canonical):
|
||||
from types import SimpleNamespace
|
||||
|
||||
return SimpleNamespace(drive_root=canonical, task_id="kid", task_metadata={
|
||||
"parent_task_id": "me", "root_task_id": "root1", "budget_drive_root": str(canonical)})
|
||||
|
||||
def test_grandchild_reaches_its_root_as_a_tree_peer_under_the_strict_cancel_read(self):
|
||||
"""On a live install a grandchild's message to its own root rode the host-listed-roots
|
||||
path as ``independent_task`` (mislabelled, fail-soft cancel read). It is a tree peer:
|
||||
context-only, labelled, and refused typed when the recipient's cancel state is unreadable."""
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
canonical, _ = self._peer_tree(tmp)
|
||||
out = _forward_to_worker(self._grandchild(canonical), "root1", "a grandchild's interim position")
|
||||
|
||||
self.assertIn("message from a peer task", out)
|
||||
self.assertIn("a task in your tree", out)
|
||||
self.assertNotIn("independent", out)
|
||||
[row] = drain_owner_entries(canonical, "root1")
|
||||
self.assertEqual((row["provenance"], row["relation"], row["source_task_id"]),
|
||||
("peer_task", "tree", "kid"))
|
||||
|
||||
projection = canonical / "state" / "cancel_intents.json"
|
||||
projection.parent.mkdir(parents=True, exist_ok=True)
|
||||
projection.write_bytes(b'{"intents": [broken')
|
||||
refused = _forward_to_worker(self._grandchild(canonical), "root1", "second position")
|
||||
self.assertIn("TASK_CANCEL_STATE_UNAVAILABLE", refused)
|
||||
self.assertIn("NOT written", refused)
|
||||
self.assertNotIn("second position", [r["text"] for r in drain_owner_entries(canonical, "root1")])
|
||||
|
||||
def test_continuation_root_without_a_parent_reaches_its_predecessors_child(self):
|
||||
"""A continuation root carries its predecessor's root label and has no parent (the
|
||||
one refusal observed live, 2026-09-28): same tree, admitted as ``tree``; another
|
||||
root stays refused."""
|
||||
from types import SimpleNamespace
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
from ouroboros.task_results import STATUS_RUNNING, write_task_result
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
canonical, sib_drive = self._peer_tree(tmp)
|
||||
write_task_result(canonical, "cont", STATUS_RUNNING, root_task_id="root1", result="running")
|
||||
ctx = SimpleNamespace(drive_root=canonical, task_id="cont",
|
||||
task_metadata={"root_task_id": "root1", "budget_drive_root": str(canonical)})
|
||||
|
||||
out = _forward_to_worker(ctx, "sib", "the predecessor's child hears its successor")
|
||||
stranger = _forward_to_worker(ctx, "stranger", "other root")
|
||||
|
||||
self.assertIn("a task in your tree", out)
|
||||
[row] = drain_owner_entries(sib_drive, "sib")
|
||||
self.assertEqual((row["provenance"], row["relation"], row["source_task_id"]),
|
||||
("peer_task", "tree", "cont"))
|
||||
self.assertIn("TASK_FORBIDDEN", stranger)
|
||||
self.assertEqual(drain_owner_entries(canonical, "stranger"), [])
|
||||
|
||||
def test_grandchild_reaches_a_root_without_a_root_carrier_but_no_sibling_root_is_inferred(self):
|
||||
from types import SimpleNamespace
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
from ouroboros.task_results import STATUS_RUNNING, write_task_result
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
canonical = pathlib.Path(tmp) / "canonical"
|
||||
write_task_result(canonical, "root0", STATUS_RUNNING, root_task_id=None, _is_direct_chat=True, result="running")
|
||||
write_task_result(canonical, "c0", STATUS_RUNNING, parent_task_id="root0", root_task_id="root0", result="running")
|
||||
write_task_result(canonical, "g0", STATUS_RUNNING, parent_task_id="c0", root_task_id="root0", result="running")
|
||||
# A sibling of c0 whose root carrier is missing: its root is never inferred.
|
||||
write_task_result(canonical, "s0", STATUS_RUNNING, parent_task_id="root0", result="running")
|
||||
ctx = SimpleNamespace(drive_root=canonical, task_id="g0", task_metadata={
|
||||
"parent_task_id": "c0", "root_task_id": "root0", "budget_drive_root": str(canonical)})
|
||||
|
||||
to_root = _forward_to_worker(ctx, "root0", "up two levels")
|
||||
to_uncarried = _forward_to_worker(ctx, "s0", "a sibling root is never inferred")
|
||||
|
||||
self.assertIn("a task in your tree", to_root)
|
||||
[row] = drain_owner_entries(canonical, "root0")
|
||||
self.assertEqual((row["provenance"], row["relation"]), ("peer_task", "tree"))
|
||||
self.assertIn("TASK_FORBIDDEN", to_uncarried)
|
||||
self.assertEqual(drain_owner_entries(canonical, "s0"), [])
|
||||
|
||||
def test_relay_to_a_tree_peer_is_an_ancestor_only_act(self):
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
canonical, _ = self._peer_tree(tmp)
|
||||
refused = _forward_to_worker(self._caller(canonical), "nephew", "relayed", relayed_from_task_id="kid")
|
||||
|
||||
self.assertIn("TASK_FORBIDDEN", refused)
|
||||
self.assertIn("ancestor-only act", refused)
|
||||
self.assertIn("a task in your tree", refused)
|
||||
self.assertEqual(drain_owner_entries(canonical, "nephew"), [])
|
||||
|
||||
def test_a_presence_root_reached_from_its_tree_keeps_the_execution_observation(self):
|
||||
"""The roster path put the Presence root's shared observation on the receipt; a
|
||||
sender inside its tree keeps it (persistence proof, not a read). An ordinary peer
|
||||
receipt carries none."""
|
||||
from ouroboros.task_results import STATUS_RUNNING, write_task_result
|
||||
from ouroboros.tools.core import _forward_to_worker
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
canonical, _ = self._peer_tree(tmp)
|
||||
# The observation is derived by the effective reader from the Presence record
|
||||
# (a direct-chat row whose metadata carries the presence binding), never stored.
|
||||
write_task_result(canonical, "root1", STATUS_RUNNING, root_task_id="root1", result="running",
|
||||
_is_direct_chat=True, metadata={"source": "presence", "presence": {"room": "r"}})
|
||||
|
||||
observed = _forward_to_worker(self._grandchild(canonical), "root1", "to the presence root")
|
||||
plain = _forward_to_worker(self._caller(canonical), "sib", "an ordinary peer")
|
||||
|
||||
self.assertIn("a task in your tree", observed)
|
||||
self.assertIn("execution_observation=", observed)
|
||||
self.assertIn('"kind": "presence"', observed)
|
||||
# The roster path's conditional wording survives: a Presence turn may be over.
|
||||
self.assertIn("if the turn continues, its checkpoint can read it", observed)
|
||||
self.assertNotIn("reads it at its next checkpoint", observed)
|
||||
self.assertNotIn("execution_observation", plain)
|
||||
self.assertIn("reads it at its next checkpoint", plain)
|
||||
|
||||
def test_relay_on_the_peer_branch_is_forbidden(self):
|
||||
from ouroboros.owner_mailbox import drain_owner_entries
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""peer_task provenance: a contribution from inside the tree without authority.
|
||||
|
||||
A sibling or a child writing to its parent is rendered under a prefix that names
|
||||
A sibling, a child writing to its parent, or any task sharing the recipient's root
|
||||
(a cousin, a grandchild, a continuation root) is rendered under a prefix that names
|
||||
the RELATION, never "ancestor" or "owner"; it wakes the mind but enters no owner
|
||||
corpus and records no directive (serial addressed turns, first delivery).
|
||||
"""
|
||||
|
|
@ -17,7 +18,7 @@ from ouroboros.loop_messages import _initialize_owner_directives, owner_authorit
|
|||
from ouroboros.loop_round_limits import _drain_incoming_messages
|
||||
from ouroboros.owner_mailbox import (
|
||||
CONTEXT_ONLY_TASK_PROVENANCES,
|
||||
PEER_TASK_RELATIONS,
|
||||
PEER_RELATION_LABELS,
|
||||
PROVENANCE_PEER_TASK,
|
||||
TASK_MESSAGE_PROVENANCES,
|
||||
deliver_task_message,
|
||||
|
|
@ -30,16 +31,20 @@ def test_peer_task_is_a_closed_context_only_provenance():
|
|||
assert PROVENANCE_PEER_TASK == "peer_task"
|
||||
assert PROVENANCE_PEER_TASK in TASK_MESSAGE_PROVENANCES
|
||||
assert PROVENANCE_PEER_TASK in CONTEXT_ONLY_TASK_PROVENANCES
|
||||
assert PEER_TASK_RELATIONS == frozenset({"sibling", "parent"})
|
||||
# One relation map: the recipient-side prefix and the sender-side receipt phrase
|
||||
# per stamped relation; the drain prefix, the receipts and telemetry all read it.
|
||||
assert set(PEER_RELATION_LABELS) == {"sibling", "parent", "tree"}
|
||||
assert all(set(spec) == {"prefix", "receipt"} for spec in PEER_RELATION_LABELS.values())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relation, label", [
|
||||
("sibling", "[Message from peer task sib-1 (sibling)]"),
|
||||
("parent", "[Message from peer task kid-1 (your child)]"),
|
||||
("tree", "[Message from peer task cousin-1 (same tree)]"),
|
||||
("", "[Message from peer task kid-1]"),
|
||||
])
|
||||
def test_render_prefix_names_the_relation_never_ancestor_or_owner(relation, label):
|
||||
source = "sib-1" if relation == "sibling" else "kid-1"
|
||||
source = {"sibling": "sib-1", "tree": "cousin-1"}.get(relation, "kid-1")
|
||||
entry = {"provenance": PROVENANCE_PEER_TASK, "source_task_id": source,
|
||||
"text": "interim position: prefer the smaller change", "msg_id": "m1"}
|
||||
if relation:
|
||||
|
|
@ -91,8 +96,11 @@ def test_a_drained_peer_contribution_records_no_owner_directive(tmp_path):
|
|||
write_task_message(tmp_path, "Interim: I would drop the mailbox redesign.", "child",
|
||||
source_task_id="kid-2", provenance=PROVENANCE_PEER_TASK, relation="parent",
|
||||
msg_id="peer-2")
|
||||
write_task_message(tmp_path, "From across the tree: the schema changed under you.", "child",
|
||||
source_task_id="cousin-3", provenance=PROVENANCE_PEER_TASK, relation="tree",
|
||||
msg_id="peer-3")
|
||||
pending = drain_owner_entries(tmp_path, "child", seen_ids=set())
|
||||
assert [row["msg_id"] for row in pending] == ["peer-1", "peer-2"]
|
||||
assert [row["msg_id"] for row in pending] == ["peer-1", "peer-2", "peer-3"]
|
||||
assert owner_authority_kinds(pending) == []
|
||||
|
||||
before = len(ctx._owner_directives)
|
||||
|
|
@ -102,5 +110,25 @@ def test_a_drained_peer_contribution_records_no_owner_directive(tmp_path):
|
|||
delivered = "\n".join(str(m["content"]) for m in messages)
|
||||
assert "[Message from peer task sib-7 (sibling)]" in delivered
|
||||
assert "[Message from peer task kid-2 (your child)]" in delivered
|
||||
assert "[Message from peer task cousin-3 (same tree)]" in delivered
|
||||
assert "the closure code lets the author close it alone" in delivered
|
||||
assert "ancestor task sib-7" not in delivered and "ancestor task kid-2" not in delivered
|
||||
assert "ancestor task cousin-3" not in delivered
|
||||
|
||||
|
||||
def test_supervisor_telemetry_keeps_every_mapped_relation_and_drops_unknown_ones(tmp_path, monkeypatch):
|
||||
"""`events.jsonl` keeps the stamped relation for every key of the one label map
|
||||
(`tree` included) and drops a value the map does not know, so the filter and the
|
||||
prefix can never disagree about which relations exist."""
|
||||
from types import SimpleNamespace
|
||||
import supervisor.log_addressing as addressing
|
||||
from supervisor.telemetry_events import _handle_task_message_injected
|
||||
|
||||
recorded = []
|
||||
monkeypatch.setattr(addressing, "address_ctx_event", lambda ctx, row: row)
|
||||
ctx = SimpleNamespace(DRIVE_ROOT=tmp_path, append_jsonl=lambda path, row: recorded.append(row),
|
||||
bridge=SimpleNamespace(push_log=lambda row: None))
|
||||
for relation in list(PEER_RELATION_LABELS) + ["cousin", ""]:
|
||||
_handle_task_message_injected({"task_id": "b", "source_task_id": "a", "provenance": PROVENANCE_PEER_TASK,
|
||||
"relation": relation, "text_preview": "interim"}, ctx)
|
||||
assert [row.get("relation") for row in recorded] == ["sibling", "parent", "tree", None, None]
|
||||
|
|
|
|||
71
tests/test_plan_evidence_task_results.py
Normal file
71
tests/test_plan_evidence_task_results.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""A ``task:<id>`` evidence item is bounded ONCE, by the resolver.
|
||||
|
||||
sha256/bytes describe the whole task-result projection, the attached head is cut at
|
||||
``EVIDENCE_PER_ITEM_BYTES`` with the cut named, and a selector reads the original.
|
||||
Before, an inner 6,000-character display preview ran first: two results that differed
|
||||
only past the preview shared one evidence identity (so one plan fingerprint), and a
|
||||
tail selector read the omission marker instead of the result.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ouroboros.task_results import STATUS_COMPLETED, write_task_result
|
||||
from ouroboros.tools.plan_evidence import EVIDENCE_PER_ITEM_BYTES, resolve_evidence, task_evidence_reader
|
||||
|
||||
|
||||
def _resolve(root, locator):
|
||||
return resolve_evidence([locator], active_root=root, allowed_roots=[root],
|
||||
resolve_task=task_evidence_reader(root))
|
||||
|
||||
|
||||
def test_results_differing_only_in_the_tail_have_distinct_identities_and_a_named_cut(tmp_path):
|
||||
body = "x" * 50_000
|
||||
write_task_result(tmp_path, "task-a", STATUS_COMPLETED, result=body + "ALPHA")
|
||||
write_task_result(tmp_path, "task-b", STATUS_COMPLETED, result=body + "BRAVO")
|
||||
|
||||
a, b = _resolve(tmp_path, "task:task-a"), _resolve(tmp_path, "task:task-b")
|
||||
|
||||
[row_a], [row_b] = a["attached"], b["attached"]
|
||||
assert row_a["sha256"] != row_b["sha256"]
|
||||
assert row_a["bytes"] > EVIDENCE_PER_ITEM_BYTES >= row_a["attached_bytes"]
|
||||
assert [o["reason"] for o in a["omissions"]] == [f"truncated_to_{EVIDENCE_PER_ITEM_BYTES}"]
|
||||
assert "OMISSION NOTE" not in row_a["text"]
|
||||
|
||||
|
||||
def test_a_tail_selector_reads_the_original_end_not_an_omission_marker(tmp_path):
|
||||
write_task_result(tmp_path, "task-c", STATUS_COMPLETED, result="x" * 50_000 + "BRAVO")
|
||||
|
||||
out = _resolve(tmp_path, "task:task-c::tail=40")
|
||||
|
||||
[row] = out["attached"]
|
||||
assert row["selector"]["kind"] == "tail"
|
||||
assert "BRAVO" in row["text"] and "OMISSION NOTE" not in row["text"]
|
||||
assert out["omissions"] == []
|
||||
|
||||
|
||||
def test_a_short_result_is_attached_whole_with_no_omission(tmp_path):
|
||||
write_task_result(tmp_path, "task-d", STATUS_COMPLETED, result="a short finding")
|
||||
|
||||
out = _resolve(tmp_path, "task:task-d")
|
||||
|
||||
[row] = out["attached"]
|
||||
assert "a short finding" in row["text"]
|
||||
assert row["bytes"] == row["attached_bytes"] and out["omissions"] == []
|
||||
|
||||
|
||||
def test_the_head_cut_never_splits_a_multibyte_character(tmp_path):
|
||||
write_task_result(tmp_path, "task-e", STATUS_COMPLETED, result="я" * 30_000)
|
||||
|
||||
[row] = _resolve(tmp_path, "task:task-e")["attached"]
|
||||
|
||||
assert len(row["text"].encode("utf-8")) <= EVIDENCE_PER_ITEM_BYTES
|
||||
assert row["text"].endswith("я")
|
||||
|
||||
|
||||
def test_a_host_notice_rides_in_the_bounded_head_of_a_long_result(tmp_path):
|
||||
write_task_result(tmp_path, "task-f", STATUS_COMPLETED, result="x" * 50_000,
|
||||
terminal_host_notice="First source limitation.")
|
||||
|
||||
[row] = _resolve(tmp_path, "task:task-f")["attached"]
|
||||
|
||||
assert '"terminal_host_notice": "First source limitation."' in row["text"]
|
||||
assert row["attached_bytes"] <= EVIDENCE_PER_ITEM_BYTES < row["bytes"]
|
||||
20
tests/test_tree_ledger_rows.py
Normal file
20
tests/test_tree_ledger_rows.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Blackboard rows name the task they came from, so a row is addressable
|
||||
(forward_to_worker / peek_task take the id as written)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ouroboros.task_tree_ledger import _format_tree_ledger_row
|
||||
|
||||
|
||||
def test_a_row_carries_the_role_and_the_full_task_id():
|
||||
row = {"ts": "2026-10-03T10:00:00Z", "kind": "note", "role": "critic",
|
||||
"task_id": "0123456789abcdef", "text": "objection"}
|
||||
line = _format_tree_ledger_row(row)
|
||||
assert "(critic 0123456789abcdef)" in line
|
||||
assert "(critic)" not in line
|
||||
|
||||
|
||||
def test_a_row_without_a_role_shows_the_whole_id_not_an_eight_char_prefix():
|
||||
row = {"ts": "2026-10-03T10:00:00Z", "kind": "note", "task_id": "0123456789abcdef", "text": "objection"}
|
||||
line = _format_tree_ledger_row(row)
|
||||
assert "(0123456789abcdef)" in line
|
||||
assert "(01234567)" not in line
|
||||
Loading…
Add table
Add a link
Reference in a new issue