mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Merge pull request #1249 from ouroboros-agent/claude/predecessor-door-20260924
Continue any settled result from any room: predecessor door, forked-drive registry reads, predecessor file reads
This commit is contained in:
commit
8d9e17461e
12 changed files with 659 additions and 114 deletions
|
|
@ -74,7 +74,7 @@ WHO is speaking is ONE host-minted fact on the event (`control_routing._routing_
|
|||
|
||||
Every promote/route refusal names its typed reason; producers holding a fact beyond the code (the workspace family, source and attachment staging, persistence failures, the project fence) add `detail`, the cause with its repair (self-explanatory codes such as `duplicate_task_id` or `worker_pool_unavailable` get no prose), and `_persist_promote_rejection` is the single durable writer. `workspace_admission.workspace_repair_hint` composes the workspace repair from the typed source of the refused folder; an empty Project whose genesis provisioning fails is typed `workspace_provisioning_failed` on the promotion path, never a fallback onto the system repo. The exact Ouroboros repository root named as `workspace_root` is the documented default, mapped to the `workspace="none"` sentinel and disclosed; a subfolder of the repository, the data drive and every `/api/tasks`, Presence and `source` caller stay refused, typed. Who is told follows who can narrate: a tool-issued act gets its receipt, error row and `detail`, and the model narrates; a host-issued act (`host_initiated`) gets ONE typed System row (`task_not_started`, or `task_start_unconfirmed` for an unconfirmed admission) in the chat the owner wrote in, from the one publication boundary `_handle_promote_chat_to_task` wraps around every outcome, never a bubble in Ouroboros's voice. A steering refusal whose owner act carries no owner-message receipt uses `steer_not_delivered` through `supervisor/steering.py::_refusal_row`, in the issuing chat. The Project start row is announced only once the task is enqueued. An admitted promote reports the destination the admission receipt returns, not the requested `project_id`/`project_name` (an implicit scope is re-resolved under the origin claim lock, §6 Project binding by task and by origin); a promote landing in a project other than the one the request's work already has says so: promote stays a free choice, disclosed. A promote by a root whose Swarm planning obligation is still UNMET (`force_plan` with no plan wave recorded, `owner_hurry.unmet_force_plan_obligation`) carries the obligation onto the new root and releases the promoter in the admission transaction (`supervisor/plan_obligation.py`), disclosed in the result; a met obligation moves nothing, and `ensure_project_scope` keeps it.
|
||||
|
||||
A routing/promote decision turn receives host-built ground truth (each project's registry `working_dir` plus bounded typed projections of the lane's recent ROOT results and live roots, never raw result text) through the registry row's durable `last_task_result_id` pointer, stamped by ROOTS only, with a bounded scan fallback; only the absent-pointer case writes back (a failing non-empty pointer is usually a split-drive result mid copy-back). A swarm child is no predecessor (reachable through its root) and counts as its own omission instead of evicting a root. The list is a HINT sized by `runtime_limits`; the door is the predicate in §10. A routing receipt on the SAME owner message is a fact, never a ban: a second root stays the model's judgment.
|
||||
A routing/promote decision turn receives host-built ground truth (each project's registry `working_dir` plus bounded typed projections of the lane's recent ROOT results and live roots, never raw result text) through the registry row's durable `last_task_result_id` pointer, stamped by ROOTS only, with a bounded scan fallback; only the absent-pointer case writes back (a failing non-empty pointer is usually a split-drive result mid copy-back). A swarm child is not offered (reachable through its root) and counts as its own omission instead of evicting a root. The list is a HINT sized by `runtime_limits` and scoped to the lane that receives it (a pooled task holds none and names any id it read through `recent_tasks`/`get_task_result`); the door is the predicate in §10 on the result itself, never on the caller's room or the landing project, and a helper predecessor or a landing outside the predecessor's project is disclosed in the receipt (`_predecessor_notes`), a free choice like the second-project note. `list_projects`, `route_to_project` and the promote receipt read the project registry through the canonical data root, because a task on a forked execution drive never carries `state/projects.json`. A routing receipt on the SAME owner message is a fact, never a ban: a second root stays the model's judgment.
|
||||
|
||||
### Tool capability and execution
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ Tool API v2 exposes neutral canonical names directly (`read_file`, `list_files`,
|
|||
|
||||
Filesystem tool output is self-locating: results use canonical `root:path` labels and `run_command`/`run_script` echo the resolved `cwd`. A direct Project room selects one active physical folder for reads, writes, edits, process cwd, VCS and delegation; governance stays at `system_repo`, and a missing folder keeps its selected address with an availability note, never a fallback to Ouroboros source. Plain folders support ordinary file/process work and directory delegation (`delegate_directory.py`); Git-only snapshot and integration paths fail typed when their target lacks Git. Physical file resolution preserves the requested address: an absolute path inside any selected base normalizes to that base, an absolute path with no root binds to the permitted root holding it, an outside absolute path is refused before `safe_relpath` can turn it into a similarly named file, and the repo basename-prefix, lineage and delegated-artifact read redirects share the resolver guards and handlers use (`ToolContext.repo_path`/`drive_path`). A Docker workspace's mapped backend absolute address (`workspace_executor.map_backend_path`) is accepted only inside `active_workspace`; other roots and unmapped absolute paths stay confined.
|
||||
|
||||
`user_files` is the first-class root for user-visible files under the owner's home (the Ouroboros repo and runtime control plane are refused); `task_drive` is task-scoped scratch; `artifact_store` is task-scoped under `data/task_results/artifacts/<task_id>/`, created lazily on a write or output registration, where deliverables written through `user_files` or declared process `outputs` are copied for audit, and a rewritten user-visible file keeps its prior copy under `task_results/artifact_versions/<task_id>/` (§1 tree). `subagent_projects` and `deliverables` grant `read`/`list`/`search` only; a read-only child reads `deliverables` and its lineage's (parent/root) `task_drive`/`artifact_store`, never a sibling's; a top-level task writes the Deliverables container through `user_files`. Process admission keeps the original argv and the prepared physical target, and that one binding serves every other authorized destination; command words, script examples and unknown interpreter effects establish no write intent — the selected Safety Supervisor receives the full task source (§6 Safety and runtime mode) — and the post-execution shell audit is observational: no replay, rollback, interpreter or attribution proof. Invalid path-like prose yields no finding. A failed audit adds a bounded diagnostic (typed results record its exception class in `output_audit_unavailable`) while keeping stdout/stderr and completed exit, signal, timeout and runtime facts; it cannot invent a spawn failure or undeclared output. Computed targets stay a disclosed parser limit, not grounds for a new semantic scanner.
|
||||
`user_files` is the first-class root for user-visible files under the owner's home (the Ouroboros repo and runtime control plane are refused); `task_drive` is task-scoped scratch; `artifact_store` is task-scoped under `data/task_results/artifacts/<task_id>/`, created lazily on a write or output registration, where deliverables written through `user_files` or declared process `outputs` are copied for audit, and a rewritten user-visible file keeps its prior copy under `task_results/artifact_versions/<task_id>/` (§1 tree). `subagent_projects` and `deliverables` grant `read`/`list`/`search` only; a read-only child reads `deliverables` and its lineage's (parent/root) `task_drive`/`artifact_store`, never a sibling's; a continuation also reads the `task_drive`/`artifact_store` of the one predecessor its contract names (`predecessor_authority.source.task_id`, one hop, read-only; a child carrying the envelope reads it too) (issue #1232); a top-level task writes the Deliverables container through `user_files`. Process admission keeps the original argv and the prepared physical target, and that one binding serves every other authorized destination; command words, script examples and unknown interpreter effects establish no write intent — the selected Safety Supervisor receives the full task source (§6 Safety and runtime mode) — and the post-execution shell audit is observational: no replay, rollback, interpreter or attribution proof. Invalid path-like prose yields no finding. A failed audit adds a bounded diagnostic (typed results record its exception class in `output_audit_unavailable`) while keeping stdout/stderr and completed exit, signal, timeout and runtime facts; it cannot invent a spawn failure or undeclared output. Computed targets stay a disclosed parser limit, not grounds for a new semantic scanner.
|
||||
|
||||
#### Credential fence and byte masking
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ This chapter is the short list of properties the rest of the book must not contr
|
|||
24. **The thread that answers workers runs only queue-bounded work.** Work that scales with history, the daemon or the network runs off-thread, reads candidates before liveness (one in-memory live source under `_queue_lock`), stops mutating when its loop generation ends and is attach-only to the daemon once a stop is in flight. Residuals on the loop thread: the 300-s zombie reconcile, the exact ledger reads of invariant 28. Owner: `ouroboros/server_maintenance.py`.
|
||||
25. **An answer that has not arrived is a gap — never a refusal, a failure, a verdict or an owner message.** A direct turn applies its acceptance fence in-process (admission lock, then `_queue_lock`, never the reverse); a pooled request is idempotent by token and acknowledged per request (`<token>.<req>.json`); a transition is re-sent once, a read never; only `sealed` is a seal, an absent row is not one; a fence that did not answer buys no model round: the panel advises on `admission_fence_available=false`, delivery seals again, and a blocking install accepts a reviewer-approved answer with the typed note `admission_close_unconfirmed`. Owners: `ouroboros/agent.py`, `supervisor/queue_transitions.py`, `ouroboros/loop_delivery.py`.
|
||||
26. **A cross-process guard derives from the durable artifact it guards, never from process memory.** The usage-ledger compaction floor is the `source_size_bytes` the last committed pass stamped into the live header, so one pass throttles every process, fresh ones included; the per-process memo throttles only a pass that changed no bytes. Owner: `ouroboros/usage_compaction.py`.
|
||||
27. **The predecessor list is a hint; the door is a predicate.** A room continues any result of its OWN project that is a ROOT, readable and not live (a live root is `steer_task`, not a second root); only a ROOT stamps the pointer. An emitted promote is durably `promotion_admission{status:"emitted"}`: pending, granting no schedule, owning no id, never `unknown`. Owners: `tools/control_routing.py`, `tools/control_events.py`.
|
||||
27. **The predecessor list is a hint; the door is a predicate on the result.** Any actor holding a routing verb continues any settled, readable result (a live root is `steer_task`, not a second root; a pending promote is not a result), whichever project it belongs to, wherever the continuation lands and whether it is a root's or a helper's: the pointer is rebuilt from the task id, the successor's ceiling, origin and contract come from the caller and admission, and the predecessor's project or helper origin is disclosed in the receipt, never an admission condition. Only a ROOT stamps the pointer and only roots are offered. An emitted promote is durably `promotion_admission{status:"emitted"}`: pending, granting no schedule, owning no id, never `unknown`. Owners: `tools/control_routing.py`, `tools/control_events.py`.
|
||||
28. **Money never reads a snapshot; a display never waits on money.** A usage-ledger display reader on the supervisor loop or a gateway thread passes `allow_stale`: a contended lock serves the last validated snapshot against the live limit; a cold memo raises — unknown, never zero. Whatever admits, reserves, settles or refuses spend reads under the lock: a snapshot may say "there is money", never refuse. Owners: `ouroboros/_usage_rows_memo.py`, `supervisor/state.py`.
|
||||
|
||||
### 10.1 Continuity data-flow map
|
||||
|
|
|
|||
|
|
@ -171,9 +171,10 @@ def _task_result_ground_truth(row: Dict[str, Any]) -> Dict[str, Any]:
|
|||
def _is_child_result(facts: Dict[str, Any]) -> bool:
|
||||
"""A result that is NOT an owner root: it has a parent, or the subagent role.
|
||||
|
||||
ONE predicate for both readers of that fact - the manifest window that skips
|
||||
children (owner decision batch 3, answer 6b=A) and the promote door, which
|
||||
refuses them by the same rule. Reads a memoized fact row or a full result row.
|
||||
ONE predicate for every reader of that fact - the manifest window that skips
|
||||
children (owner decision batch 3, answer 6b=A), the pointer stamp that only a
|
||||
root moves, and the promote receipt, which continues a named child with its
|
||||
root disclosed. Reads a memoized fact row or a full result row.
|
||||
"""
|
||||
return bool(str(facts.get("parent_task_id") or "").strip()) or str(
|
||||
facts.get("delegation_role") or "") == "subagent"
|
||||
|
|
@ -248,14 +249,14 @@ def _cancel_state_facts(ctx: Any, task_id: str) -> Dict[str, Any]:
|
|||
def _project_routing_manifest(ctx: Any, project_id: str) -> Dict[str, Any]:
|
||||
"""The room's bounded HINT for a "continue this work" decision: the project's
|
||||
recent ROOT results and the roots still live in it, each with the small typed
|
||||
facts that separate the two choices - a finished root is promote's predecessor,
|
||||
facts that separate the two choices - a settled root is promote's predecessor,
|
||||
a live one is ``steer_task``.
|
||||
|
||||
A hint, never the door: promote's predicate admits an older root of the same
|
||||
project too (ch. 10), so this window may be bounded without deciding what the
|
||||
room can continue. Until it existed a room saw exactly ONE candidate, the
|
||||
registry pointer, so a room whose pointer had moved could not name its own
|
||||
interrupted root at all.
|
||||
A hint, never the door: promote's predicate admits any settled result, listed
|
||||
or not, of any project (ch. 10), so this window may be bounded without
|
||||
deciding what the room can continue. Until it existed a room saw exactly ONE
|
||||
candidate, the registry pointer, so a room whose pointer had moved could not
|
||||
name its own interrupted root at all.
|
||||
"""
|
||||
finals, omissions = _recent_root_results(ctx, project_id)
|
||||
active = [
|
||||
|
|
@ -280,7 +281,7 @@ def _not_a_root_result(row: Dict[str, Any]) -> bool:
|
|||
|
||||
def _latest_project_task_result(ctx: Any, project_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Newest ROOT task result bound to ``project_id`` (a child's is never the room's
|
||||
continuation: the promote door refuses it, ``_is_child_result``) WITHOUT replaying the whole
|
||||
last-result pointer: the hint offers roots only, ``_is_child_result``) WITHOUT replaying the whole
|
||||
store (DEVELOPMENT "Projection over replay"). The registry row's durable
|
||||
``last_task_result_id`` pointer (stamped at project-task finalization) is
|
||||
read FIRST — one direct file fetch, immune to how many newer foreign
|
||||
|
|
@ -544,8 +545,8 @@ def main_lane_routing_metadata(ctx: Any, chat_id: int) -> Dict[str, Any]:
|
|||
Exactly what an owner turn in the same chat is handed — the Main routing manifest
|
||||
and this chat's addressable roots — minus what is bound to an owner message (there
|
||||
is none). One seam over the owner path, so a wake can never drift from what the
|
||||
host says is addressable: without the manifest every predecessor the wake names is
|
||||
refused as "not addressable" and it cannot continue prior work at all.
|
||||
host shows an owner turn: the manifest is the hint both decide from, while the
|
||||
door judges the named result itself, listed or not.
|
||||
"""
|
||||
facts = _decision_turn_metadata(ctx, int(chat_id or 0), "", {})
|
||||
return dict(facts) if isinstance(facts, dict) else {}
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
|
|||
lane = str(effective_lane or "").strip()
|
||||
if lane:
|
||||
bits.append(f"model_lane={lane}")
|
||||
lineage = (" (task_drive/artifact_store: its own, its parent's and its root task's files,"
|
||||
" never a sibling's)" if "task_drive" in read_roots else "")
|
||||
lineage = (" (task_drive/artifact_store: its own, its parent's, its root task's and, when its"
|
||||
" contract names one, its predecessor's files, never a sibling's)" if "task_drive" in read_roots else "")
|
||||
return (
|
||||
"child capabilities — " + " · ".join(bits)
|
||||
+ f"\nreadable={', '.join(read_roots) or 'none'}{lineage}"
|
||||
|
|
@ -116,20 +116,22 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
|
|||
|
||||
|
||||
def lineage_task_ids(ctx: Any) -> tuple[str, ...]:
|
||||
"""Task ids whose ``task_drive``/``artifact_store`` this actor may READ: its own,
|
||||
then ``parent_task_id`` and ``root_task_id`` from its own lineage fields (T4=A,
|
||||
#1105) — never a sibling's, nothing found by walking the disk, malformed ids dropped."""
|
||||
meta = getattr(ctx, "task_metadata", None)
|
||||
meta = meta if isinstance(meta, dict) else {}
|
||||
"""Task ids whose ``task_drive``/``artifact_store`` this actor may READ: its own, its parent's
|
||||
and its root's (own lineage fields, T4=A #1105) and the ONE predecessor its contract names
|
||||
(``task_contract.predecessor_authority.source.task_id``, one hop, #1232; a child carrying the
|
||||
envelope reads it too) — never a sibling's, nothing found by walking the disk, malformed ids dropped."""
|
||||
meta, contract = (v if isinstance(v, dict) else {} for v in (
|
||||
getattr(ctx, "task_metadata", None), getattr(ctx, "task_contract", None)))
|
||||
authority = (contract or meta).get("predecessor_authority")
|
||||
source = authority.get("source") if isinstance(authority, dict) else None
|
||||
ids = [task_id_for_artifacts(ctx)]
|
||||
for key in ("parent_task_id", "root_task_id"):
|
||||
for raw in (meta.get("parent_task_id"), meta.get("root_task_id"),
|
||||
source.get("task_id") if isinstance(source, dict) else None):
|
||||
try:
|
||||
candidate = validate_task_id(meta.get(key))
|
||||
ids.append(validate_task_id(raw))
|
||||
except ValueError:
|
||||
continue
|
||||
if candidate not in ids:
|
||||
ids.append(candidate)
|
||||
return tuple(ids)
|
||||
return tuple(dict.fromkeys(ids))
|
||||
|
||||
|
||||
def _task_root_drives(ctx: Any) -> list[pathlib.Path]:
|
||||
|
|
|
|||
|
|
@ -119,9 +119,11 @@ _PROMOTE_CHAT_DESCRIPTION = (
|
|||
"task). `project_id` starts the new task in an existing project. If your task "
|
||||
"carries a planning obligation (Swarm force_plan) that no plan review has met, the "
|
||||
"obligation moves to the new task and your own further work here is unplanned. "
|
||||
"When this new task continues one specific "
|
||||
"completed result shown by the host (the Main manifest or Project last-result "
|
||||
"preview), pass its internal id as `predecessor_task_id`; pass an empty string for fresh work. "
|
||||
"When this new task continues one specific settled result (any settled status; from "
|
||||
"the host manifest, recent_tasks or get_task_result — any project, the "
|
||||
"list is a hint; a helper's result is continued with its root named), pass its internal "
|
||||
"id as `predecessor_task_id`; pass an empty string for fresh work. A live root "
|
||||
"(steer_task instead) or a pending promote is refused. "
|
||||
"`workspace_root` points at a working folder. A project-scoped task inherits "
|
||||
"the project's working folder as its ACTIVE WORKSPACE by default (its file/"
|
||||
"shell/git tools operate there, not on the Ouroboros repo); pass "
|
||||
|
|
@ -214,7 +216,7 @@ def get_tools() -> List[ToolEntry]:
|
|||
"workspace_root": {"type": "string", "description": "Optional absolute working-folder path (validated at admission as an ordinary folder or Git worktree root outside the Ouroboros repo/data). Git-specific operations require a Git worktree; ordinary file and process work is supported directly in a validated folder. When omitted for a project-scoped task, the project's registered working_dir is used by default. Leave empty to work in Ouroboros's own repository (the Main default).", "default": ""},
|
||||
"workspace": {"type": "string", "description": "Pass 'none' to opt OUT of the project room's default working folder (a folder-less task in a folder-ful project). Leave empty otherwise.", "default": ""},
|
||||
"source": {"type": "string", "description": "Attach or clone the project's working folder in ONE move: a git URL (https://... or git@host:path — cloned server-side into the projects root; private repos fail typed auth_required) or an existing folder path (validated attach). The folder is registered on the project (provenance + trusted_at) and becomes this task's active workspace. Use for 'help me debug this GitHub repo / this folder' asks.", "default": ""},
|
||||
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the completed result id shown by the host routing manifest to continue it."},
|
||||
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the id of a settled result (any settled status; any project, the host list is a hint; a helper's result is continued with its root named) to continue it. A live root or a pending promote is refused."},
|
||||
},
|
||||
"required": ["objective", "predecessor_task_id"],
|
||||
},
|
||||
|
|
@ -264,15 +266,15 @@ def get_tools() -> List[ToolEntry]:
|
|||
"CALL THIS TOOL with project_id='' and the owner's message: it emits the typed "
|
||||
"needs_manual_target acknowledgement with host-validated task options and New task "
|
||||
"in Project; prose alone cannot emit that typed choice. For brand-new work that is not yet a project, "
|
||||
"use promote_chat_to_task instead. When continuing one completed result from the "
|
||||
"Main host manifest, pass its internal `predecessor_task_id`; pass an empty string for fresh work. "
|
||||
"Returns a visible routing receipt."
|
||||
"use promote_chat_to_task instead. When continuing one settled result (any project; "
|
||||
"the host list is a hint), pass its internal `predecessor_task_id`; pass an empty "
|
||||
"string for fresh work. Returns a visible routing receipt."
|
||||
),
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"project_id": {"type": "string", "default": "", "description": "Target project id (filesystem-clean; see list_projects), or empty to emit typed needs_manual_target."},
|
||||
"message": {"type": "string", "description": "The owner message / work to route into the project."},
|
||||
"reason": {"type": "string", "default": "", "description": "Optional short why-this-project note (provenance)."},
|
||||
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the completed result id listed by the Main host manifest to continue it."},
|
||||
"predecessor_task_id": {"type": "string", "description": "Required explicit selector: pass an empty string for fresh work, or the id of a settled result (any settled status; any project, the host list is a hint; a helper's result is continued with its root named) to continue it. A live root or a pending promote is refused."},
|
||||
"candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional, ONLY with project_id='': the task/project ids you consider plausible, in preference order. The typed picker shows them first; ids not in the host-built option list are ignored."},
|
||||
}, "required": ["message", "predecessor_task_id"]},
|
||||
}, _route_to_project),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from ouroboros.tools.control_events import (
|
|||
_emit_and_wait_for_routing,
|
||||
_promotion_pool_disabled_from_snapshot,
|
||||
)
|
||||
from ouroboros.tool_access_paths import canonical_data_root
|
||||
from ouroboros.tools.registry import ToolContext
|
||||
from ouroboros.utils import append_jsonl, utc_now_iso
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ def _predecessor_selector_error(value: Any, tool_name: str) -> str:
|
|||
if value is _MISSING_PREDECESSOR_SELECTOR or value is None:
|
||||
return (
|
||||
f"⚠️ TOOL_ARG_ERROR ({tool_name}): predecessor_task_id is required; "
|
||||
"pass an empty string for fresh work or the host-listed result id to continue it"
|
||||
"pass an empty string for fresh work or the id of a settled result to continue it"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
|
@ -109,46 +110,46 @@ def _host_listed_predecessors(metadata: Dict[str, Any]) -> list:
|
|||
return rows
|
||||
|
||||
|
||||
def _predecessor_door_refusal(
|
||||
ctx: ToolContext, result: Dict[str, Any], listed: bool,
|
||||
) -> str:
|
||||
"""Why this readable result may NOT be continued from here, or ``""``.
|
||||
def _predecessor_door_refusal(result: Dict[str, Any]) -> str:
|
||||
"""Why this readable result may NOT be continued, or ``""``.
|
||||
|
||||
The door is a PREDICATE, not membership of a bounded list: same project, a
|
||||
ROOT, a readable result, not live. The list is a hint - a window that fits
|
||||
16 rows became a ceiling saying "exactly one result is addressable", so a
|
||||
room could not name its own interrupted root and promoted again instead,
|
||||
minting the duplicate root the night ended with. Outside a room there is no
|
||||
project to compare against, and an absent project is not a match: there the
|
||||
host's own list still decides.
|
||||
A PREDICATE on the result itself - settled, and a result rather than a pending
|
||||
admission - never on where the caller sits, where the work lands or whether it
|
||||
is a root's or a helper's: the pointer is rebuilt from the task id alone, the
|
||||
successor inherits DATA (ceiling, origin and contract come from the caller and
|
||||
admission), and a fresh root in the predecessor's project was always reachable.
|
||||
The host list stays a hint; a foreign landing or a helper is disclosed, never refused.
|
||||
"""
|
||||
from ouroboros.routing_wait import is_emitted_admission_stub
|
||||
from ouroboros.server_routing_context import _is_child_result
|
||||
from ouroboros.task_status import SETTLED_STATUSES
|
||||
|
||||
if _is_child_result(result):
|
||||
return (
|
||||
"the selected predecessor is a delegated child result; name the ROOT task it "
|
||||
"belongs to - a child's work is reachable through its root"
|
||||
)
|
||||
status = str(result.get("status") or "")
|
||||
if is_emitted_admission_stub(result):
|
||||
return ("the selected predecessor is a promote whose admission is still pending, not a "
|
||||
"result; read get_task_result on it, and name a finished root instead")
|
||||
"result; read get_task_result on it, and name a settled result instead")
|
||||
if status not in SETTLED_STATUSES:
|
||||
return (
|
||||
f"the selected predecessor is still live (status {status or 'unknown'}); "
|
||||
"steer_task continues a live root, and promoting it would start a second one"
|
||||
)
|
||||
room = _inherited_project_scope(ctx)
|
||||
if not listed and not (room and room == str(result.get("project_id") or "")):
|
||||
return (
|
||||
"predecessor_task_id is not an addressable result in the host routing "
|
||||
"manifest, and it is not a finished root task of this room's project"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _predecessor_notes(predecessor_id: str, facts: Dict[str, Any], landed: str) -> str:
|
||||
"""What the receipt says about the predecessor once, like the second-project note: a
|
||||
helper's result names its root (or parent), a foreign landing names both projects."""
|
||||
if not predecessor_id:
|
||||
return ""
|
||||
home, root, parent = (str(facts.get(k) or "") for k in ("project_id", "root_task_id", "parent_task_id"))
|
||||
lineage = f"its root is {root}" if root else (f"its parent is {parent}" if parent else "its root is unknown")
|
||||
notes = f" Note: predecessor {predecessor_id} is a delegated helper's result; {lineage}." if facts.get("helper") else ""
|
||||
if home != str(landed or ""):
|
||||
where = f"project '{home}'" if home else "the main chat"
|
||||
here = f"project '{landed}'" if landed else "the main chat"
|
||||
notes += f" Note: predecessor {predecessor_id} belongs to {where}; this continuation runs in {here} (your choice)."
|
||||
return notes
|
||||
|
||||
|
||||
def _attach_predecessor_authority_from_metadata(
|
||||
ctx: ToolContext, evt: Dict[str, Any], predecessor_task_id: str = "",
|
||||
) -> str:
|
||||
|
|
@ -161,15 +162,10 @@ def _attach_predecessor_authority_from_metadata(
|
|||
row for row in _host_listed_predecessors(metadata)
|
||||
if str(row.get("task_id") or "") == selected_id
|
||||
), None)
|
||||
status_root = Path(str(
|
||||
metadata.get("budget_drive_root")
|
||||
or getattr(ctx, "budget_drive_root", "")
|
||||
or ctx.drive_root
|
||||
))
|
||||
result = load_effective_task_result(status_root, selected_id, materialize_artifacts=False)
|
||||
result = load_effective_task_result(canonical_data_root(ctx), selected_id, materialize_artifacts=False)
|
||||
if not isinstance(result, dict) or not result:
|
||||
return "the selected predecessor task result is missing or unreadable"
|
||||
refusal = _predecessor_door_refusal(ctx, result, listed is not None)
|
||||
refusal = _predecessor_door_refusal(result)
|
||||
if refusal:
|
||||
return refusal
|
||||
if listed is not None:
|
||||
|
|
@ -184,8 +180,17 @@ def _attach_predecessor_authority_from_metadata(
|
|||
from ouroboros.agent_startup_checks import valid_task_result_authority_source
|
||||
|
||||
if valid_task_result_authority_source(source, selected_id):
|
||||
from ouroboros.server_routing_context import _is_child_result
|
||||
|
||||
evt["predecessor_task_id"] = selected_id
|
||||
evt["predecessor_authority_source"] = dict(source)
|
||||
# Render-only facts for the receipt's notes; the caller pops them before
|
||||
# emission, so the event carries nothing the supervisor never reads.
|
||||
evt["predecessor_facts"] = {
|
||||
"project_id": str(result.get("project_id") or ""), "helper": _is_child_result(result),
|
||||
"root_task_id": str(result.get("root_task_id") or ""),
|
||||
"parent_task_id": str(result.get("parent_task_id") or ""),
|
||||
}
|
||||
else:
|
||||
return "the selected predecessor has no readable authority source"
|
||||
return ""
|
||||
|
|
@ -281,7 +286,7 @@ def _effective_scope_note(ctx: ToolContext, project_id: str) -> str:
|
|||
try:
|
||||
from ouroboros.projects_registry import get_project
|
||||
|
||||
name = str((get_project(Path(ctx.drive_root), pid) or {}).get("name") or "").strip()
|
||||
name = str((get_project(canonical_data_root(ctx), pid) or {}).get("name") or "").strip()
|
||||
except Exception:
|
||||
log.debug("promote: effective project name lookup failed", exc_info=True)
|
||||
return f" in project '{name}' ({pid})" if name and name != pid else f" in project '{pid}'"
|
||||
|
|
@ -464,6 +469,7 @@ def _promote_chat_to_task(
|
|||
"⚠️ AUTHORITY_SOURCE_UNAVAILABLE (promote_chat_to_task): "
|
||||
+ predecessor_error
|
||||
)
|
||||
predecessor_facts = dict(evt.pop("predecessor_facts", None) or {})
|
||||
_attach_client_surface(ctx, evt)
|
||||
_attach_unmet_obligation(ctx, evt)
|
||||
already_bound = _durable_project_of_request(ctx)
|
||||
|
|
@ -483,6 +489,7 @@ def _promote_chat_to_task(
|
|||
"Use wait_task/get_task_result if its result "
|
||||
"is needed in this conversation."
|
||||
+ _second_project_note(ctx, already_bound, effective_pid)
|
||||
+ _predecessor_notes(str(evt.get("predecessor_task_id") or ""), predecessor_facts, effective_pid)
|
||||
+ _obligation_moved_note(ctx, tid, confirmation.get("force_plan_transfer"))
|
||||
)
|
||||
return _finish_swarm_handoff(ctx, evt, response, status="scheduled")
|
||||
|
|
@ -577,10 +584,12 @@ def _second_project_note(ctx: ToolContext, already_bound: str, effective_pid: st
|
|||
|
||||
def _list_projects(ctx: ToolContext, limit: int = 50) -> str:
|
||||
"""Enumerate the owner's projects (id, name, recency) so the one mind can
|
||||
decide whether a main-chat message belongs to an existing project."""
|
||||
decide whether a main-chat message belongs to an existing project. The registry
|
||||
lives on the CANONICAL data root: a forked execution drive never carries
|
||||
``state/projects.json``, so reading the task's own drive answered "no projects"."""
|
||||
try:
|
||||
from ouroboros.projects_registry import projects_summary
|
||||
rows = projects_summary(Path(ctx.drive_root), limit=max(1, min(int(limit or 50), 200)))
|
||||
rows = projects_summary(canonical_data_root(ctx), limit=max(1, min(int(limit or 50), 200)))
|
||||
except Exception as exc:
|
||||
return f"⚠️ PROJECTS_ERROR: {type(exc).__name__}: {exc}"
|
||||
if not rows:
|
||||
|
|
@ -635,9 +644,10 @@ def _route_to_project(
|
|||
)
|
||||
if predecessor_error:
|
||||
return "⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project): " + predecessor_error
|
||||
predecessor_facts = dict(predecessor_event.pop("predecessor_facts", None) or {})
|
||||
requested_pid = str(project_id or "").strip()
|
||||
pid = sanitize_project_id(requested_pid) if requested_pid and explicit_project_id_ok(requested_pid) else ""
|
||||
proj = get_project(Path(ctx.drive_root), pid) if pid else None
|
||||
proj = get_project(canonical_data_root(ctx), pid) if pid else None
|
||||
failure = (
|
||||
"target_unspecified" if not requested_pid
|
||||
else "invalid_project_id" if not pid
|
||||
|
|
@ -753,6 +763,8 @@ def _route_to_project(
|
|||
response = (
|
||||
f"✉️ Routed to project '{name}' ({pid}) as task {tid}; admission is durably "
|
||||
f"scheduled ({mode}). I'll continue there; this chat stays free for you."
|
||||
+ _predecessor_notes(str(evt.get("predecessor_task_id") or ""), predecessor_facts,
|
||||
str(receipt.get("effective_project_id") or pid))
|
||||
+ _obligation_moved_note(ctx, tid, receipt.get("force_plan_transfer"))
|
||||
)
|
||||
return _finish_swarm_handoff(ctx, evt, response, status="scheduled")
|
||||
|
|
|
|||
|
|
@ -280,9 +280,8 @@ def record_task_finalization(
|
|||
if is_root:
|
||||
# The pointer answers "continue from here" for the ROOM, so only a ROOT may
|
||||
# stamp it: a child finalizing after its root moved the room's single
|
||||
# candidate onto work no owner ever addressed, and the room was then left
|
||||
# naming a result the promote door refuses (the mirror below is root-only
|
||||
# for the same reason).
|
||||
# candidate onto work no owner ever addressed - a helper the hint never
|
||||
# offers (the mirror below is root-only for the same reason).
|
||||
record_project_last_result(project_id, tid, drive_root)
|
||||
try:
|
||||
_record_work_location(project_id, task)
|
||||
|
|
|
|||
|
|
@ -935,22 +935,33 @@ def test_a_wake_starts_fresh_work_with_no_predecessor_and_needs_no_manifest(tmp_
|
|||
assert "predecessor_task_id" not in promoted.pending_events[0]
|
||||
|
||||
|
||||
def test_a_wake_without_the_manifest_still_refuses_an_unaddressable_predecessor(tmp_path):
|
||||
"""The typed refusal is unchanged; only the facts the wake is given are new."""
|
||||
def test_a_wake_without_the_manifest_continues_a_settled_root_and_still_refuses_a_live_one(tmp_path):
|
||||
"""The door judges the root, not the facts a wake was handed: with no manifest at all
|
||||
a wake continues a settled root on both verbs (the pointer is rebuilt from the durable
|
||||
result and equals the one the manifest would have shown), while a live root keeps its
|
||||
typed refusal toward steer_task and emits nothing."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
|
||||
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
_addressable_result(tmp_path)
|
||||
preview = _addressable_result(tmp_path)
|
||||
|
||||
routed = _wake_routing_ctx(tmp_path)
|
||||
out = _route_to_project(routed, "racer", "Continue the racer", predecessor_task_id="racer-old")
|
||||
assert out.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project)")
|
||||
assert "not an addressable result in the host routing manifest" in out
|
||||
assert routed.pending_events == []
|
||||
assert out.startswith("⚠️ ROUTE_UNCONFIRMED"), out
|
||||
[route_evt] = routed.pending_events
|
||||
assert route_evt["predecessor_authority_source"] == preview["authority_source"]
|
||||
|
||||
promoted = _wake_routing_ctx(tmp_path)
|
||||
refused = _promote_chat_to_task(promoted, "Finish the racer", workspace="none",
|
||||
predecessor_task_id="racer-old")
|
||||
assert refused.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (promote_chat_to_task)")
|
||||
assert promoted.pending_events == []
|
||||
_promote_chat_to_task(promoted, "Finish the racer", workspace="none", predecessor_task_id="racer-old")
|
||||
[promote_evt] = promoted.pending_events
|
||||
assert promote_evt["predecessor_task_id"] == "racer-old"
|
||||
assert promote_evt["initiator"] == "consciousness"
|
||||
|
||||
(tmp_path / "task_results" / "racer-live.json").write_text(json.dumps({
|
||||
"_schema_version": 1, "task_id": "racer-live", "status": "running", "project_id": "racer",
|
||||
}), encoding="utf-8")
|
||||
refused = _wake_routing_ctx(tmp_path)
|
||||
out = _route_to_project(refused, "racer", "Continue the racer", predecessor_task_id="racer-live")
|
||||
assert out.startswith("⚠️ AUTHORITY_SOURCE_UNAVAILABLE (route_to_project)") and "steer_task" in out
|
||||
assert refused.pending_events == []
|
||||
|
|
|
|||
282
tests/test_predecessor_lineage_read.py
Normal file
282
tests/test_predecessor_lineage_read.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
"""A top-level continuation reads the ONE predecessor it continues (owner 3A, #1232).
|
||||
|
||||
Measured on a live install: a continuation admitted with ``memory_mode=forked``
|
||||
runs on its own headless drive (``data/state/headless_tasks/<id>/data``), called
|
||||
``get_task_result(include_authority=True)`` on its predecessor, saw a registered
|
||||
artifact path plus hash, and ``read_file`` on that path was refused ("outside
|
||||
artifact root"). The lineage rule (T4=A, #1105) named the actor's own, parent's
|
||||
and root's task files; the predecessor was not in that set. Now
|
||||
``lineage_task_ids`` appends the predecessor's validated id from the ONE carrier a
|
||||
running ToolContext has: ``task_contract["predecessor_authority"]["source"]
|
||||
["task_id"]``, minted by ``validate_task_authority_sources`` at startup binding
|
||||
(``agent_startup_checks``) and carried into the contract by
|
||||
``build_task_contract``; ``task_metadata`` is read only when no contract rides
|
||||
(the agent never copies the envelope into metadata). One hop only: the
|
||||
envelope's ``previous_task_id`` is never read. READ only: a write into the
|
||||
predecessor's drive stays refused while the task's own drive stays writable.
|
||||
The read follows the envelope the actor carries: a delegated child inherits its
|
||||
parent's envelope through the contract spread (``subagent_work_order`` copies the
|
||||
parent contract whole) and reads that predecessor's files too, read-only, exactly
|
||||
as it reads its parent's and root's; a child without the envelope keeps parent/root.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from ouroboros.artifacts import task_artifact_dir_path
|
||||
from ouroboros.contracts.task_constraint import TaskConstraint
|
||||
from ouroboros.tool_access import (
|
||||
_resolve_target_in_selected_base,
|
||||
lineage_read_base,
|
||||
lineage_task_ids,
|
||||
)
|
||||
from ouroboros.tools.registry import ToolContext, ToolRegistry
|
||||
|
||||
SUCCESSOR = "s2f0a1b2c3d4e5f60"
|
||||
PRED = "pred-1"
|
||||
GRANDPRED = "pred-0"
|
||||
OTHER = "other-task"
|
||||
PARENT = "p07499dc017c01f83"
|
||||
ROOT = "r5173b7c3c15d4c0b"
|
||||
CHILD = "c11ae4fd0aa111111"
|
||||
|
||||
|
||||
def host_pointer(task_id):
|
||||
"""The exact host-issued actor pointer ``valid_task_result_authority_source`` accepts."""
|
||||
return {
|
||||
"kind": "task_result",
|
||||
"task_id": task_id,
|
||||
"human_label": f"task {task_id}",
|
||||
"tool": "get_task_result",
|
||||
"arguments": {"task_id": task_id, "include_authority": True},
|
||||
}
|
||||
|
||||
|
||||
def predecessor_envelope(task_id, *, previous=""):
|
||||
"""The shape startup binding mints: a bounded envelope whose chain cursor names
|
||||
the hop BEFORE the predecessor and whose pull pointer rides LAST under ``source``."""
|
||||
return {
|
||||
"kind": "bounded_continuation_envelope",
|
||||
"status": "done",
|
||||
"previous_task_id": previous,
|
||||
"source": host_pointer(task_id),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def geometry(tmp_path, monkeypatch):
|
||||
"""The predecessor's task files live on the CANONICAL data root; the
|
||||
continuation runs on its own forked (headless) drive; the owner home is a
|
||||
fake tmp home. The predecessor's own predecessor and a stranger have files
|
||||
too, so the one-hop and no-walking rules have something to refuse."""
|
||||
home = tmp_path / "home"
|
||||
repo = tmp_path / "repo"
|
||||
canonical = tmp_path / "data"
|
||||
headless = tmp_path / "state" / "headless_tasks" / SUCCESSOR / "data"
|
||||
for path in (home, repo, canonical, headless):
|
||||
path.mkdir(parents=True)
|
||||
monkeypatch.setattr(pathlib.Path, "home", lambda: home)
|
||||
monkeypatch.setenv("OUROBOROS_USER_FILES_ROOT", str(home))
|
||||
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
|
||||
monkeypatch.setenv("OUROBOROS_SAFETY_MODE", "off")
|
||||
(repo / "README.md").write_text("repo readme\n", encoding="utf-8")
|
||||
pred_drive = canonical / "task_drives" / PRED
|
||||
pred_drive.mkdir(parents=True)
|
||||
(pred_drive / "notes.md").write_text("PRED_DRIVE_BYTES\n", encoding="utf-8")
|
||||
pred_artifacts = task_artifact_dir_path(canonical, PRED, create=True)
|
||||
(pred_artifacts / "report.pdf.txt").write_text("PRED_ARTIFACT_BYTES\n", encoding="utf-8")
|
||||
grand_drive = canonical / "task_drives" / GRANDPRED
|
||||
grand_drive.mkdir(parents=True)
|
||||
(grand_drive / "notes.md").write_text("GRANDPRED_BYTES\n", encoding="utf-8")
|
||||
other_drive = canonical / "task_drives" / OTHER
|
||||
other_drive.mkdir(parents=True)
|
||||
(other_drive / "notes.md").write_text("OTHER_BYTES\n", encoding="utf-8")
|
||||
other_artifacts = task_artifact_dir_path(canonical, OTHER, create=True)
|
||||
(other_artifacts / "out.txt").write_text("OTHER_ARTIFACT_BYTES\n", encoding="utf-8")
|
||||
return SimpleNamespace(
|
||||
home=home, repo=repo, canonical=canonical, headless=headless,
|
||||
pred_drive=pred_drive, pred_artifacts=pred_artifacts, grand_drive=grand_drive,
|
||||
other_drive=other_drive, other_artifacts=other_artifacts,
|
||||
)
|
||||
|
||||
|
||||
def continuation_registry(geo, *, predecessor=PRED):
|
||||
"""A top-level continuation of ``predecessor`` admitted with ``memory_mode=forked``:
|
||||
it executes on its headless drive while the canonical root stays its budget root.
|
||||
``predecessor=None`` gives the same task without any predecessor authority."""
|
||||
ctx = ToolContext(
|
||||
repo_dir=geo.repo, drive_root=geo.headless, task_id=SUCCESSOR, memory_mode="forked",
|
||||
budget_drive_root=str(geo.canonical),
|
||||
)
|
||||
ctx.task_metadata = {"memory_mode": "forked", "budget_drive_root": str(geo.canonical)}
|
||||
ctx.task_contract = {"predecessor_authority": predecessor_envelope(
|
||||
predecessor, previous=GRANDPRED)} if predecessor else {}
|
||||
registry = ToolRegistry(repo_dir=geo.repo, drive_root=ctx.drive_root)
|
||||
registry.set_context(ctx)
|
||||
return registry, ctx
|
||||
|
||||
|
||||
def child_registry(geo):
|
||||
"""A delegated read-only child of PARENT under ROOT whose contract carries the
|
||||
parent's predecessor envelope, exactly as the parent contract spread delivers it."""
|
||||
ctx = ToolContext(repo_dir=geo.repo, drive_root=geo.headless, task_id=CHILD)
|
||||
ctx.budget_drive_root = str(geo.canonical)
|
||||
ctx.task_metadata = {
|
||||
"delegation_role": "subagent",
|
||||
"parent_task_id": PARENT,
|
||||
"root_task_id": ROOT,
|
||||
"budget_drive_root": str(geo.canonical),
|
||||
}
|
||||
ctx.task_contract = {"predecessor_authority": predecessor_envelope(PRED)}
|
||||
ctx.task_constraint = TaskConstraint(mode="local_readonly_subagent", allow_enable=False)
|
||||
registry = ToolRegistry(repo_dir=geo.repo, drive_root=ctx.drive_root)
|
||||
registry.set_context(ctx)
|
||||
return registry, ctx
|
||||
|
||||
|
||||
# --- (a) the continuation reads its predecessor's task files from a forked drive ---
|
||||
|
||||
def test_continuation_reads_its_predecessors_task_drive_from_a_forked_drive(geometry):
|
||||
registry, ctx = continuation_registry(geometry)
|
||||
target = geometry.pred_drive / "notes.md"
|
||||
|
||||
out = registry.execute("read_file", {"root": "task_drive", "path": str(target)})
|
||||
|
||||
assert "PRED_DRIVE_BYTES" in out, out
|
||||
assert out.startswith("# task_drive:"), out
|
||||
assert ctx.last_read_view["opened_root"] == "task_drive"
|
||||
assert ctx.last_read_view["target"] == str(target.resolve())
|
||||
|
||||
|
||||
def test_continuation_reads_its_predecessors_registered_artifact(geometry):
|
||||
"""The live shape: the artifact path ``get_task_result(include_authority=True)``
|
||||
registered, read with the artifact root named and with no root at all."""
|
||||
registry, ctx = continuation_registry(geometry)
|
||||
target = geometry.pred_artifacts / "report.pdf.txt"
|
||||
|
||||
named = registry.execute("read_file", {"root": "artifact_store", "path": str(target)})
|
||||
bare = registry.execute("read_file", {"path": str(target)})
|
||||
|
||||
assert "PRED_ARTIFACT_BYTES" in named and named.startswith("# artifact_store:"), named
|
||||
assert "PRED_ARTIFACT_BYTES" in bare, bare
|
||||
assert ctx.last_read_view["opened_root"] == "artifact_store"
|
||||
assert lineage_read_base(ctx, "artifact_store", target) == geometry.pred_artifacts.resolve()
|
||||
assert lineage_read_base(ctx, "task_drive", geometry.pred_drive / "notes.md") == geometry.pred_drive.resolve()
|
||||
|
||||
|
||||
# --- (b) a stranger's files and the hop before the predecessor stay refused -------
|
||||
|
||||
def test_a_strangers_drive_and_the_predecessors_own_predecessor_stay_refused(geometry):
|
||||
registry, ctx = continuation_registry(geometry)
|
||||
|
||||
other = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.other_drive / "notes.md")})
|
||||
other_artifact = registry.execute(
|
||||
"read_file", {"root": "artifact_store", "path": str(geometry.other_artifacts / "out.txt")})
|
||||
grand = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.grand_drive / "notes.md")})
|
||||
|
||||
assert "OTHER_BYTES" not in other and "outside selected root=task_drive" in other, other
|
||||
assert "OTHER_ARTIFACT_BYTES" not in other_artifact, other_artifact
|
||||
assert "outside selected root=artifact_store" in other_artifact, other_artifact
|
||||
# ONE hop: the envelope's ``previous_task_id`` (GRANDPRED) is never a lineage member.
|
||||
assert "GRANDPRED_BYTES" not in grand and "outside selected root=task_drive" in grand, grand
|
||||
assert GRANDPRED not in lineage_task_ids(ctx) and OTHER not in lineage_task_ids(ctx)
|
||||
|
||||
|
||||
# --- (c) the predecessor grant is a READ grant ------------------------------------
|
||||
|
||||
def test_a_write_into_the_predecessors_drive_is_refused_while_the_own_drive_stays_writable(geometry):
|
||||
registry, ctx = continuation_registry(geometry)
|
||||
target = geometry.pred_drive / "notes.md"
|
||||
before = target.read_text(encoding="utf-8")
|
||||
|
||||
refused = registry.execute("write_file", {"root": "task_drive", "path": str(target), "content": "x"})
|
||||
own = registry.execute("write_file", {"root": "task_drive", "path": "scratch.txt", "content": "mine"})
|
||||
|
||||
assert refused.startswith("⚠️") and "outside selected root=task_drive" in refused, refused
|
||||
assert target.read_text(encoding="utf-8") == before
|
||||
assert not own.startswith("⚠️"), own
|
||||
assert (geometry.headless / "task_drives" / SUCCESSOR / "scratch.txt").read_text(encoding="utf-8") == "mine"
|
||||
own_base = geometry.headless / "task_drives" / SUCCESSOR
|
||||
assert _resolve_target_in_selected_base(
|
||||
ctx, root="task_drive", base_path=own_base, path=str(target), operation="read") == target.resolve()
|
||||
for operation in ("write", "edit"):
|
||||
with pytest.raises(ValueError):
|
||||
_resolve_target_in_selected_base(
|
||||
ctx, root="task_drive", base_path=own_base, path=str(target), operation=operation)
|
||||
|
||||
|
||||
# --- (d) without predecessor authority nothing changes ----------------------------
|
||||
|
||||
def test_a_task_without_predecessor_authority_keeps_todays_lineage(geometry):
|
||||
registry, ctx = continuation_registry(geometry, predecessor=None)
|
||||
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
out = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.pred_drive / "notes.md")})
|
||||
assert "PRED_DRIVE_BYTES" not in out and "outside selected root=task_drive" in out, out
|
||||
|
||||
ctx.task_metadata.update({"parent_task_id": PARENT, "root_task_id": ROOT})
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, ROOT)
|
||||
ctx.task_metadata["root_task_id"] = PARENT # parent IS the root: no duplicate
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT)
|
||||
ctx.task_metadata["parent_task_id"] = "../escape" # malformed ids are dropped, not guessed
|
||||
ctx.task_metadata["root_task_id"] = ""
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
top = ToolContext(repo_dir=geometry.repo, drive_root=geometry.canonical, task_id=ROOT)
|
||||
assert lineage_task_ids(top) == (ROOT,)
|
||||
|
||||
|
||||
def test_lineage_task_ids_names_the_predecessor_once_after_parent_and_root(geometry):
|
||||
_registry, ctx = continuation_registry(geometry)
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PRED)
|
||||
|
||||
ctx.task_metadata.update({"parent_task_id": PARENT, "root_task_id": ROOT})
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, ROOT, PRED)
|
||||
|
||||
ctx.task_metadata["root_task_id"] = PRED # the predecessor IS the root: no duplicate
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PARENT, PRED)
|
||||
|
||||
# A malformed or missing pointer id is dropped, never guessed from the envelope.
|
||||
ctx.task_metadata = {}
|
||||
ctx.task_contract["predecessor_authority"]["source"]["task_id"] = "../escape"
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
ctx.task_contract["predecessor_authority"]["source"] = "not-a-pointer"
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
ctx.task_contract["predecessor_authority"] = "not-an-envelope"
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
|
||||
|
||||
def test_metadata_carries_the_envelope_only_when_no_contract_rides(geometry):
|
||||
"""The contract is the carrier on a running ToolContext (the agent never copies
|
||||
the envelope into metadata); a context with no contract at all is read from
|
||||
metadata, and a present contract is never second-guessed by metadata."""
|
||||
_registry, ctx = continuation_registry(geometry, predecessor=None)
|
||||
ctx.task_metadata["predecessor_authority"] = predecessor_envelope(PRED)
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, PRED)
|
||||
|
||||
ctx.task_contract = {"predecessor_authority": predecessor_envelope(OTHER)}
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR, OTHER)
|
||||
ctx.task_contract = {"lineage": {"delegation_role": "root"}}
|
||||
assert lineage_task_ids(ctx) == (SUCCESSOR,)
|
||||
|
||||
|
||||
# --- (e) a delegated child keeps parent/root even with an inherited envelope ------
|
||||
|
||||
def test_a_delegated_child_inherits_the_continuations_predecessor_read(geometry):
|
||||
"""A child CAN carry ``predecessor_authority``: ``subagent_work_order`` copies the
|
||||
parent's contract whole into the child task and ``build_task_contract`` preserves
|
||||
the envelope. The read follows the envelope the actor carries - the child's lineage
|
||||
grows by the predecessor and its files are readable, read-only, exactly as the
|
||||
parent's and root's are; a child without the envelope keeps parent/root only."""
|
||||
registry, ctx = child_registry(geometry)
|
||||
|
||||
assert lineage_task_ids(ctx) == (CHILD, PARENT, ROOT, PRED)
|
||||
out = registry.execute("read_file", {"root": "task_drive", "path": str(geometry.pred_drive / "notes.md")})
|
||||
assert "PRED_DRIVE_BYTES" in out, out
|
||||
assert lineage_read_base(ctx, "task_drive", geometry.pred_drive / "notes.md") is not None
|
||||
|
||||
ctx.task_contract.pop("predecessor_authority", None)
|
||||
ctx.task_metadata.pop("predecessor_authority", None)
|
||||
assert lineage_task_ids(ctx) == (CHILD, PARENT, ROOT)
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
"""I7: what a project room may continue, and what stamps its last-result pointer.
|
||||
"""What a routing verb may continue, and what stamps a project's last-result pointer.
|
||||
|
||||
On 19.09 a room offered exactly ONE predecessor candidate (the project's
|
||||
last-result pointer, which a CHILD had overwritten), the model named the
|
||||
interrupted root itself and was refused `AUTHORITY_SOURCE_UNAVAILABLE`, and the
|
||||
retry without a predecessor minted a duplicate root. The list is a HINT; the
|
||||
door is a predicate: same project, a root, a readable result, not live.
|
||||
A room once offered exactly ONE predecessor candidate (the project's last-result
|
||||
pointer, which a CHILD had overwritten), the model named the interrupted root
|
||||
itself and was refused `AUTHORITY_SOURCE_UNAVAILABLE`, and the retry without a
|
||||
predecessor minted a duplicate root. Later a coordinator task named the settled
|
||||
roots of five other projects, each to be continued inside its own project, and
|
||||
was refused seven times because the door compared the predecessor's project with
|
||||
the CALLER's room. The list is a HINT; the door is a predicate on the result
|
||||
itself: settled and readable - never where the caller sits, where the work lands
|
||||
or whether it is a root's or a helper's; those facts are disclosed in the receipt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -50,6 +54,30 @@ def _door(ctx, task_id, evt=None):
|
|||
return _attach_predecessor_authority_from_metadata(ctx, evt if evt is not None else {}, task_id)
|
||||
|
||||
|
||||
def _confirm(monkeypatch, effective_project_id: str = ""):
|
||||
"""The admission receipt names where the task actually landed."""
|
||||
monkeypatch.setattr(
|
||||
"ouroboros.tools.control_events._wait_for_promotion_admission",
|
||||
lambda *_a, **_k: {"status": "scheduled", "effective_project_id": effective_project_id},
|
||||
)
|
||||
|
||||
|
||||
_TOWER_POINTER = {
|
||||
"kind": "task_result", "task_id": "tower-root", "human_label": "another room's work",
|
||||
"tool": "get_task_result", "arguments": {"task_id": "tower-root", "include_authority": True},
|
||||
}
|
||||
|
||||
|
||||
def _tower(tmp_path):
|
||||
"""A second project with one settled root, exactly the shape the coordinator named."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.task_results import write_task_result
|
||||
|
||||
create_project(tmp_path, "tower", name="Tower")
|
||||
write_task_result(tmp_path, "tower-root", "completed", project_id="tower",
|
||||
objective="another room's work", ts="2026-08-10T00:00:01Z")
|
||||
|
||||
|
||||
def test_a_root_the_room_manifest_lists_is_still_addressable(tmp_path):
|
||||
"""I29 positive path (owner batch 3, answer 6b=A): the narrowing removed
|
||||
CHILDREN from the window, and a listed owner root stays promotable."""
|
||||
|
|
@ -133,13 +161,15 @@ def test_a_room_root_older_than_the_list_is_addressable_all_the_same(tmp_path, m
|
|||
}
|
||||
|
||||
|
||||
def test_a_child_result_is_never_the_continuation_and_the_host_stops_offering_it(tmp_path):
|
||||
"""A pointer stamped by a child before this release still names a child: the
|
||||
host offers the ROOT instead, and the door refuses the child if the model names
|
||||
it all the same, saying where the work is reachable (I29 stays closed)."""
|
||||
def test_a_helpers_result_is_continued_with_its_root_named_and_never_offered(tmp_path, monkeypatch):
|
||||
"""A pointer stamped by a child before only roots stamped it still names a child:
|
||||
the host offers the ROOT (the hint stays roots-only, owner decision 6b=A) and heals
|
||||
the pointer, while a helper's result the model names on purpose is continued -
|
||||
the receipt says whose helper it was and where its root is."""
|
||||
import server
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.task_results import write_task_result
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task
|
||||
from ouroboros.tools.project_journal import record_project_last_result
|
||||
|
||||
project = create_project(tmp_path, "racer", name="Racer")
|
||||
|
|
@ -159,21 +189,74 @@ def test_a_child_result_is_never_the_continuation_and_the_host_stops_offering_it
|
|||
metadata["project_routing_manifest"]["final_results"]] == ["racer-root"]
|
||||
|
||||
evt: dict = {}
|
||||
refusal = _door(_room_ctx(tmp_path, metadata), "racer-child", evt)
|
||||
assert "delegated child result" in refusal and "root" in refusal
|
||||
assert evt == {}
|
||||
assert _door(_room_ctx(tmp_path, metadata), "racer-root") == ""
|
||||
assert _door(_room_ctx(tmp_path, metadata), "racer-child", evt) == ""
|
||||
assert evt["predecessor_task_id"] == "racer-child"
|
||||
assert evt["predecessor_facts"] == {"project_id": "racer", "helper": True,
|
||||
"root_task_id": "racer-root", "parent_task_id": "racer-root"}
|
||||
root_evt: dict = {}
|
||||
assert _door(_room_ctx(tmp_path, metadata), "racer-root", root_evt) == ""
|
||||
assert root_evt["predecessor_facts"]["helper"] is False # a root is nobody's helper
|
||||
|
||||
_confirm(monkeypatch, effective_project_id="racer")
|
||||
ctx = _room_ctx(tmp_path, metadata)
|
||||
out = _promote_chat_to_task(ctx, "Continue the helper's work", workspace="none",
|
||||
predecessor_task_id="racer-child")
|
||||
assert out.startswith("OK: task"), out
|
||||
assert "Note: predecessor racer-child is a delegated helper's result; its root is racer-root." in out
|
||||
assert "belongs to" not in out # same project: nothing else to disclose
|
||||
|
||||
|
||||
def test_another_projects_root_is_not_this_rooms_continuation(tmp_path):
|
||||
import server
|
||||
def test_a_helper_predecessor_is_named_on_the_route_verb_and_without_a_root_id(tmp_path, monkeypatch):
|
||||
"""The helper note rides both verbs and names what the helper's row knows: its root,
|
||||
else its parent, else that the root is unknown - it never goes silent on a helper
|
||||
whose row carries only the subagent role."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.task_results import write_task_result
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
|
||||
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
write_task_result(tmp_path, "racer-child", "completed", project_id="racer", objective="helper work",
|
||||
parent_task_id="racer-root", root_task_id="racer-root", delegation_role="subagent")
|
||||
write_task_result(tmp_path, "racer-nested", "completed", project_id="racer", objective="nested helper",
|
||||
parent_task_id="racer-child", delegation_role="subagent")
|
||||
write_task_result(tmp_path, "racer-orphan", "completed", project_id="racer", objective="role only",
|
||||
delegation_role="subagent")
|
||||
_confirm(monkeypatch, effective_project_id="racer")
|
||||
|
||||
routed = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _route_to_project(routed, "racer", "continue the helper's work", predecessor_task_id="racer-child")
|
||||
assert out.startswith("✉️ Routed to project 'Racer' (racer)"), out
|
||||
assert "Note: predecessor racer-child is a delegated helper's result; its root is racer-root." in out
|
||||
|
||||
nested = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _promote_chat_to_task(nested, "Continue the nested helper", workspace="none", predecessor_task_id="racer-nested")
|
||||
assert "Note: predecessor racer-nested is a delegated helper's result; its parent is racer-child." in out
|
||||
|
||||
orphan = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _promote_chat_to_task(orphan, "Continue the role-only helper", workspace="none", predecessor_task_id="racer-orphan")
|
||||
assert "Note: predecessor racer-orphan is a delegated helper's result; its root is unknown." in out
|
||||
|
||||
|
||||
def test_a_failed_root_is_a_settled_predecessor(tmp_path):
|
||||
"""Settled means completed, failed or cancelled: the coordinator's first refused
|
||||
predecessor had failed at its absolute ceiling and was still the work to continue."""
|
||||
from ouroboros.task_results import write_task_result
|
||||
|
||||
write_task_result(tmp_path, "tower-failed", "failed", project_id="tower",
|
||||
objective="ran out of ceiling", reason_code="absolute_ceiling")
|
||||
evt: dict = {}
|
||||
assert _door(_room_ctx(tmp_path, {}, project_id="racer"), "tower-failed", evt) == ""
|
||||
assert evt["predecessor_authority_source"]["arguments"] == {"task_id": "tower-failed", "include_authority": True}
|
||||
|
||||
def test_another_projects_root_is_continued_from_this_room_and_the_hint_stays_room_local(tmp_path):
|
||||
"""The room's manifest lists only its own roots - a hint - while the door judges the
|
||||
root itself: another project's settled root is continued from here with a pointer
|
||||
rebuilt from the durable result, never from a shown row."""
|
||||
import server
|
||||
from ouroboros.projects_registry import create_project
|
||||
|
||||
project = create_project(tmp_path, "racer", name="Racer")
|
||||
create_project(tmp_path, "tower", name="Tower")
|
||||
write_task_result(tmp_path, "tower-root", "completed", project_id="tower",
|
||||
objective="another room's work", ts="2026-08-10T00:00:01Z")
|
||||
_tower(tmp_path)
|
||||
|
||||
metadata = server._decision_turn_metadata(
|
||||
_host_ctx(tmp_path), int(project["chat_id"]), "room-4", {"project_id": "racer"},
|
||||
|
|
@ -181,9 +264,112 @@ def test_another_projects_root_is_not_this_rooms_continuation(tmp_path):
|
|||
assert metadata["project_routing_manifest"]["final_results"] == []
|
||||
|
||||
evt: dict = {}
|
||||
refusal = _door(_room_ctx(tmp_path, metadata), "tower-root", evt)
|
||||
assert "not an addressable result in the host routing manifest" in refusal
|
||||
assert evt == {}
|
||||
assert _door(_room_ctx(tmp_path, metadata), "tower-root", evt) == ""
|
||||
assert evt["predecessor_task_id"] == "tower-root"
|
||||
assert evt["predecessor_authority_source"] == _TOWER_POINTER
|
||||
assert evt["predecessor_facts"] == {"project_id": "tower", "helper": False, "root_task_id": "", "parent_task_id": ""}
|
||||
|
||||
|
||||
def test_a_pooled_task_continues_another_rooms_root_into_that_room_in_one_hop(tmp_path, monkeypatch):
|
||||
"""The coordinator shape: a pooled task carries no host manifest at all (its metadata is
|
||||
the client surface and its own contract), sits in one room, and names a settled root
|
||||
of another project as the predecessor of work it sends INTO that project. Both verbs
|
||||
schedule it in one hop, the event carries the rebuilt pointer and nothing else new,
|
||||
and a landing in the predecessor's own project has nothing to disclose."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
|
||||
|
||||
create_project(tmp_path, "coord", name="Coordination")
|
||||
_tower(tmp_path)
|
||||
metadata = {"client_surface": {"channel": "web"}, "task_contract": {"objective": "coordinate"}}
|
||||
_confirm(monkeypatch, effective_project_id="tower")
|
||||
|
||||
promoted = _room_ctx(tmp_path, metadata, project_id="coord")
|
||||
out = _promote_chat_to_task(promoted, "Continue the tower work", project_id="tower",
|
||||
workspace="none", predecessor_task_id="tower-root")
|
||||
assert out.startswith("OK: task"), out
|
||||
assert "belongs to" not in out
|
||||
[evt] = promoted.pending_events
|
||||
assert evt["project_id"] == "tower"
|
||||
assert evt["predecessor_task_id"] == "tower-root"
|
||||
assert evt["predecessor_authority_source"] == _TOWER_POINTER
|
||||
assert "predecessor_facts" not in evt
|
||||
|
||||
routed = _room_ctx(tmp_path, metadata, project_id="coord")
|
||||
out = _route_to_project(routed, "tower", "continue the tower work", predecessor_task_id="tower-root")
|
||||
assert out.startswith("✉️ Routed to project 'Tower' (tower)"), out
|
||||
assert "belongs to" not in out
|
||||
[evt] = routed.pending_events
|
||||
assert evt["predecessor_task_id"] == "tower-root"
|
||||
assert evt["predecessor_authority_source"] == _TOWER_POINTER
|
||||
assert "predecessor_facts" not in evt
|
||||
|
||||
|
||||
def test_a_landing_outside_the_predecessors_project_is_disclosed_once(tmp_path, monkeypatch):
|
||||
"""A free choice, said in the receipt like the second-project note: a continuation
|
||||
landing in another project names the predecessor's own project, on both verbs; one
|
||||
landing at home carries no such sentence, so the note cannot fire unconditionally."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task, _route_to_project
|
||||
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
_tower(tmp_path)
|
||||
note = "Note: predecessor tower-root belongs to project 'tower'; this continuation runs in project 'racer' (your choice)."
|
||||
|
||||
_confirm(monkeypatch, effective_project_id="racer")
|
||||
ctx = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _promote_chat_to_task(ctx, "Continue elsewhere", project_id="racer", workspace="none",
|
||||
predecessor_task_id="tower-root")
|
||||
assert out.startswith("OK: task") and note in out, out
|
||||
|
||||
routed = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _route_to_project(routed, "racer", "continue elsewhere", predecessor_task_id="tower-root")
|
||||
assert out.startswith("✉️ Routed to project 'Racer' (racer)") and note in out, out
|
||||
|
||||
_confirm(monkeypatch, effective_project_id="tower")
|
||||
home = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _promote_chat_to_task(home, "Continue at home", project_id="tower", workspace="none",
|
||||
predecessor_task_id="tower-root")
|
||||
assert out.startswith("OK: task") and "belongs to" not in out, out
|
||||
|
||||
|
||||
def test_a_main_root_is_continued_from_a_room_and_its_home_is_named(tmp_path, monkeypatch):
|
||||
"""A project-less (Main) root was reachable only through the Main lane's list; the door
|
||||
judges the root, so a room names it too, and the receipt says it comes from Main."""
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.task_results import write_task_result
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task
|
||||
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
write_task_result(tmp_path, "main-root", "completed", project_id="", objective="main work")
|
||||
|
||||
evt: dict = {}
|
||||
assert _door(_room_ctx(tmp_path, {}, project_id="racer"), "main-root", evt) == ""
|
||||
assert evt["predecessor_task_id"] == "main-root" and evt["predecessor_facts"]["project_id"] == ""
|
||||
|
||||
_confirm(monkeypatch, effective_project_id="racer")
|
||||
ctx = _room_ctx(tmp_path, {}, project_id="racer")
|
||||
out = _promote_chat_to_task(ctx, "Continue the main work here", project_id="racer",
|
||||
workspace="none", predecessor_task_id="main-root")
|
||||
assert "predecessor main-root belongs to the main chat; this continuation runs in project 'racer'" in out
|
||||
|
||||
|
||||
def test_a_public_conversation_continues_a_settled_root_and_still_lands_without_a_project(tmp_path, monkeypatch):
|
||||
"""Presence holds the routing verb under its own ceiling: the door judges the root,
|
||||
while the promote still strips project, workspace and source - the ceiling is the
|
||||
caller's, never the predecessor's - and the receipt names where the predecessor lives."""
|
||||
from ouroboros.tools.control_routing import _promote_chat_to_task
|
||||
|
||||
_tower(tmp_path)
|
||||
_confirm(monkeypatch, effective_project_id="")
|
||||
ctx = _room_ctx(tmp_path, {"presence": {"binding_id": "b" * 32}}, project_id="")
|
||||
out = _promote_chat_to_task(ctx, "Continue the tower work", project_id="tower",
|
||||
workspace="none", predecessor_task_id="tower-root")
|
||||
assert out.startswith("OK: task"), out
|
||||
[evt] = ctx.pending_events
|
||||
assert evt["project_id"] == "" and evt["presence"] == {"binding_id": "b" * 32}
|
||||
assert evt["predecessor_task_id"] == "tower-root"
|
||||
assert "predecessor tower-root belongs to project 'tower'; this continuation runs in the main chat" in out
|
||||
|
||||
|
||||
def test_a_live_root_is_steer_territory_not_a_predecessor(tmp_path):
|
||||
|
|
@ -246,9 +432,11 @@ def test_an_unreadable_predecessor_still_answers_authority_source_unavailable(tm
|
|||
assert not (tmp_path / "task_results" / "racer-gone.json").exists()
|
||||
|
||||
|
||||
def test_outside_a_room_the_host_list_still_decides(tmp_path):
|
||||
"""The quiet direction of the same guard: with no room project there is no
|
||||
`same project` to evaluate, so an unlisted id stays unaddressable."""
|
||||
def test_outside_a_room_an_unlisted_settled_root_is_continued_too(tmp_path):
|
||||
"""With no room there is no project to compare against, and none is needed: a Main
|
||||
turn names any settled root, listed (the host's own pointer) or not (a pointer
|
||||
rebuilt from the durable result); a missing id stays the one case no predicate
|
||||
can rescue, and it still emits nothing."""
|
||||
import server
|
||||
from ouroboros.projects_registry import create_project
|
||||
from ouroboros.task_results import write_task_result
|
||||
|
|
@ -259,12 +447,18 @@ def test_outside_a_room_the_host_list_still_decides(tmp_path):
|
|||
|
||||
metadata = server._decision_turn_metadata(_host_ctx(tmp_path), 1, "main-1", {})
|
||||
main_ctx = _room_ctx(tmp_path, {"client_message_id": "main-1"}, project_id="")
|
||||
refusal = _door(main_ctx, "racer-root")
|
||||
assert "not an addressable result in the host routing manifest" in refusal
|
||||
evt: dict = {}
|
||||
assert _door(main_ctx, "racer-root", evt) == ""
|
||||
assert evt["predecessor_task_id"] == "racer-root"
|
||||
assert evt["predecessor_authority_source"]["arguments"] == {"task_id": "racer-root", "include_authority": True}
|
||||
|
||||
listed_ctx = _room_ctx(tmp_path, metadata, project_id="")
|
||||
assert _door(listed_ctx, "racer-root") == ""
|
||||
|
||||
gone: dict = {}
|
||||
assert _door(main_ctx, "never-existed", gone) == "the selected predecessor task result is missing or unreadable"
|
||||
assert gone == {}
|
||||
|
||||
|
||||
def test_only_a_root_finalization_moves_the_projects_pointer(tmp_path):
|
||||
"""The pointer answers "continue from here" for the ROOM; a child that
|
||||
|
|
@ -297,7 +491,7 @@ def test_only_a_root_finalization_moves_the_projects_pointer(tmp_path):
|
|||
def test_the_self_heal_scan_never_offers_or_stamps_a_child(tmp_path):
|
||||
"""The lookup's fallback scan is the pointer's SECOND writer: with no pointer
|
||||
yet and a child as the project's newest result, it answers with the newest ROOT
|
||||
and stamps that, never the child the door would refuse."""
|
||||
and stamps that, never the child the hint does not offer."""
|
||||
import os
|
||||
|
||||
import server
|
||||
|
|
|
|||
|
|
@ -117,7 +117,13 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# private-snapshot paragraph now states the worktree ops lock's scope (shared
|
||||
# metadata only, row-then-ref order, batched binary verdict, typed busy refusal)
|
||||
# — rationale-layer text BIBLE P6 requires.
|
||||
"docs/architecture/06-agent-core.md": 306000,
|
||||
# 306000 -> 306800: the predecessor door is a predicate on the result, never on the
|
||||
# caller's room, the landing project or the root/helper distinction (the disclosed
|
||||
# notes replace the one-clause pointer to §10); the registry read of the routing
|
||||
# verbs on a forked execution drive and the predecessor's task files as a lineage
|
||||
# read are new facts of the paragraphs they extend. The merged base sat 147 bytes
|
||||
# under the previous budget.
|
||||
"docs/architecture/06-agent-core.md": 306800,
|
||||
"docs/architecture/07-configuration.md": 36991,
|
||||
# 18947 -> 19287: CI failure collection now documents diagnostic desktop builds while release remains gated.
|
||||
"docs/architecture/08-git-branching-ci-and-build.md": 19287,
|
||||
|
|
@ -135,7 +141,11 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# residual sentence of the off-thread invariant; the rule itself has no older text.
|
||||
# +200 (2026-09-22): invariant 10 names the process-local fingerprint memos
|
||||
# and their fallback rule; the base sat 23 bytes under the previous budget.
|
||||
"docs/architecture/10-key-invariants.md": 21300,
|
||||
# 21300 -> 21700: invariant 27 states the door as a predicate on the root (any actor
|
||||
# holding a routing verb, any project, a disclosed landing) with the reason the
|
||||
# room comparison protected nothing; the earlier one-clause form is replaced, and
|
||||
# the base sat 34 bytes under the previous budget.
|
||||
"docs/architecture/10-key-invariants.md": 21700,
|
||||
"docs/architecture/11-frozen-contracts-v1.md": 24194,
|
||||
# +400 (#1213): Presence turns are named as actors without cross-focus catalogue or focus authority.
|
||||
"docs/architecture/12-host-service-companions-and-chat-ids.md": 11400,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,38 @@ def _ctx(tmp_path, events=None, *, task_metadata=None, **overrides):
|
|||
return types.SimpleNamespace(**values)
|
||||
|
||||
|
||||
def test_a_task_on_a_forked_drive_reads_the_registry_from_the_canonical_root(tmp_path, monkeypatch):
|
||||
"""A promoted task with a workspace runs on a forked execution drive that never
|
||||
carries ``state/projects.json``; the routing verbs read the registry through the
|
||||
canonical data root, so such a task lists, routes into and names the owner's
|
||||
projects. A context without a canonical root still reads its own drive."""
|
||||
from ouroboros.tools.control_routing import _effective_scope_note, _promote_chat_to_task
|
||||
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
child = tmp_path / "state" / "headless_tasks" / "fork-1" / "data"
|
||||
child.mkdir(parents=True)
|
||||
forked = _ctx(child, task_metadata={"budget_drive_root": str(tmp_path)}, budget_drive_root=str(tmp_path))
|
||||
|
||||
assert "racer — Racer" in _list_projects(forked)
|
||||
out = _route_to_project(forked, "racer", "continue the engine tuning", predecessor_task_id="")
|
||||
assert out.startswith("⚠️ ROUTE_UNCONFIRMED:"), out
|
||||
assert forked.pending_events[0]["project_id"] == "racer"
|
||||
assert _effective_scope_note(forked, "racer") == " in project 'Racer' (racer)"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"ouroboros.tools.control_events._wait_for_promotion_admission",
|
||||
lambda *_a, **_k: {"status": "scheduled", "effective_project_id": "racer"},
|
||||
)
|
||||
promoted = _ctx(child, task_metadata={"budget_drive_root": str(tmp_path)}, budget_drive_root=str(tmp_path))
|
||||
out = _promote_chat_to_task(promoted, "tune the engine", project_id="racer", workspace="none", predecessor_task_id="")
|
||||
assert out.startswith("OK: task") and "in project 'Racer' (racer)" in out, out
|
||||
|
||||
# The quiet direction: no canonical root at all means the task's own drive is the registry.
|
||||
own_drive = _ctx(child)
|
||||
assert _list_projects(own_drive).startswith("No projects yet")
|
||||
assert "target_not_found" in _route_to_project(own_drive, "racer", "msg", predecessor_task_id="")
|
||||
|
||||
|
||||
def test_route_to_existing_project_emits_event_and_receipt(tmp_path):
|
||||
create_project(tmp_path, "racer", name="Racer")
|
||||
# The origin identity is captured at INGRESS and rides task_metadata by
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue