mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Focus source: read under the reader admission the contract grants, resolve against the physical author, select historical bytes by digest; drop the SYSTEM.md restatement
This commit is contained in:
parent
d42e887f09
commit
ac18ebd4e9
9 changed files with 94 additions and 18 deletions
|
|
@ -60,7 +60,7 @@ A workspace task's completion compares against the captured preflight base — t
|
|||
|
||||
`promote_chat_to_task`, `route_to_project`, `steer_task` and `ensure_project_scope` ride one receipt rail: an act succeeds only after its token-matched supervisor facts are durable in the existing 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 rather than 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 — a direct turn the owner typed or a turn with a host-stamped ingress `client_message_id` — or a TASK speaking for itself (including a root without owner ingress relaying an owner message it just drained; a consciousness wake-up runs on the direct lane, but `metadata.initiator == "consciousness"` means nobody typed it, so its 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 that supersedes a reviewed answer, the room veto from the registry lane of the issuing chat (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: they go through the one task-message writer `forward_to_worker` also uses, as `independent_task` provenance, to any host-listed active independent root (hidden roots included, no room veto), render as `[Message from independent task <id>]`, enter no owner corpus (`owner_source_sha256` and the acceptance premises stay the owner's), carry no attachments, and are confirmed WRITTEN or refused with the host's reason. A relay keys and publishes its acknowledgement on the owner message it drained; other task acts use their own synthetic receipt id without a chat acknowledgement. Neither receipt 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 a `[INDEPENDENT_ROOTS]` TAIL note (`peer_roster.py`, `ROSTER_NOTE_CAP` = 40 rows shown, the cut disclosed), appended only when the roster changed and never merged into a row already sent. Rows identify live direct conversations without claiming an owner initiator; whether to message one stays the model's judgment. A root may publish one bounded `update_focus(text, source_ref)` record; the same projections expose it in grouped notes and paginated `live_roots`, while `recent_tasks` retains it on dormant results. A `source_ref` names a reader; `update_focus` answers it through that reader under the caller's authority 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)` (the roster's `retained_source`); a refusing reader or changed snapshot refuses the focus (`FOCUS_SOURCE_UNRESOLVED`) (a pointer at a page rewritten once the author is dormant identifies nothing), and the roster drops a settled root's focus. Focus has authored time separate from host observation and carries no TTL or owner authority. Explicit authorized project journal/workpad reads return exactly the requested source; foreign scoped writes refuse, and children/Presence retain their existing capability ceiling.
|
||||
WHO is speaking is ONE host-minted fact on the event (`control_routing._routing_issuer`; the model has no argument): an OWNER TURN — a direct turn the owner typed or a turn with a host-stamped ingress `client_message_id` — or a TASK speaking for itself (including a root without owner ingress relaying an owner message it just drained; a consciousness wake-up runs on the direct lane, but `metadata.initiator == "consciousness"` means nobody typed it, so its 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 that supersedes a reviewed answer, the room veto from the registry lane of the issuing chat (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: they go through the one task-message writer `forward_to_worker` also uses, as `independent_task` provenance, to any host-listed active independent root (hidden roots included, no room veto), render as `[Message from independent task <id>]`, enter no owner corpus (`owner_source_sha256` and the acceptance premises stay the owner's), carry no attachments, and are confirmed WRITTEN or refused with the host's reason. A relay keys and publishes its acknowledgement on the owner message it drained; other task acts use their own synthetic receipt id without a chat acknowledgement. Neither receipt 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 a `[INDEPENDENT_ROOTS]` TAIL note (`peer_roster.py`, `ROSTER_NOTE_CAP` = 40 rows shown, the cut disclosed), appended only when the roster changed and never merged into a row already sent. Rows identify live direct conversations without claiming an owner initiator; whether to message one stays the model's judgment. A root may publish one bounded `update_focus(text, source_ref)` record; the same projections expose it in grouped notes and paginated `live_roots`, while `recent_tasks` retains it on dormant results. A `source_ref` names a reader; `update_focus` answers it through that reader 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 a later focus or a retry cannot substitute its evidence (the roster's `retained_source`); a refusing reader or changed snapshot refuses the focus (`FOCUS_SOURCE_UNRESOLVED`) (a pointer at a page rewritten once the author is dormant identifies nothing), and the roster drops a settled root's focus. Focus has authored time separate from host observation and carries no TTL or owner authority. Explicit authorized project journal/workpad reads return exactly the requested source; foreign scoped writes refuse, and children/Presence retain their existing capability ceiling.
|
||||
|
||||
`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 actually 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 and its receipt is keyed on the message actually relayed — the drained delivery's own 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, while no chat row carries the id and the owner message's own receipt (what a later decision turn reads) is left standing. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -208,8 +208,8 @@ def render_roster_note(roster: Dict[str, Any], *, exclude: str = "") -> str:
|
|||
# The retained bytes the reader answered at authoring time: what
|
||||
# the source_ref still identifies once the author is dormant,
|
||||
# readable from any drive through the one cross-task reader.
|
||||
line += (f" · retained_source=get_task_result(task_id={json.dumps(focus['author_task_id'])}, include_focus_source=True)"
|
||||
f" sha256={handle['sha256'][:12]}… size={handle['size']}")
|
||||
line += (f" · retained_source=get_task_result(task_id={json.dumps(focus['author_task_id'])}, include_focus_source=True,"
|
||||
f" focus_source_sha256={json.dumps(handle['sha256'])}) size={handle['size']}")
|
||||
lines.append(line)
|
||||
if not shown:
|
||||
lines.append("- (none)")
|
||||
|
|
|
|||
|
|
@ -444,18 +444,40 @@ def completion_source_projection(
|
|||
|
||||
def focus_source_projection(
|
||||
drive_root: Any, task_id: str, result: Dict[str, Any], start_char: Any = None, end_char: Any = None,
|
||||
sha256: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
"""Read the bytes a task's focus source_ref answered at authoring time (focus.source_handle)."""
|
||||
from ouroboros.artifacts import read_actor_source_bytes, text_source_range_projection
|
||||
"""Read the bytes a task's focus source_ref answered at authoring time (focus.source_handle).
|
||||
|
||||
``sha256`` selects a HISTORICAL retained source by digest: a roster row quotes
|
||||
the handle it saw, and a later focus of the same author must not substitute
|
||||
its own evidence for that row's. The store is write-once and digest-named,
|
||||
so the selector resolves to exactly one immutable file or to nothing.
|
||||
"""
|
||||
from ouroboros.artifacts import read_actor_source_bytes, task_artifact_dir_path, text_source_range_projection
|
||||
from ouroboros.focus import compact_focus
|
||||
|
||||
unavailable = {"schema": 1, "kind": "task_focus_source", "status": "unavailable"}
|
||||
focus = compact_focus(result.get("focus"))
|
||||
handle = focus.get("source_handle") if focus else None
|
||||
wanted = str(sha256 or "").strip().lower()
|
||||
if wanted and (not isinstance(handle, dict) or str(handle.get("sha256") or "") != wanted):
|
||||
if len(wanted) != 64 or any(c not in "0123456789abcdef" for c in wanted):
|
||||
return {**unavailable, "reason": "source_ref_invalid"}
|
||||
try:
|
||||
store = task_artifact_dir_path(drive_root, str(task_id), create=False) / "source_handles" / "context_checkpoints"
|
||||
matches = sorted(p for p in store.glob(f"focus_source_*-{wanted}.md") if not p.is_symlink())
|
||||
except (OSError, ValueError):
|
||||
matches = []
|
||||
if len(matches) != 1:
|
||||
return {**unavailable, "reason": "source_unavailable", "requested_sha256": wanted}
|
||||
handle = {"kind": "task_source", "root": "artifact_store",
|
||||
"path": f"source_handles/context_checkpoints/{matches[0].name}",
|
||||
"size": matches[0].stat().st_size, "sha256": wanted}
|
||||
focus = None # a historical selector carries no current source_ref/authored_at claim
|
||||
if not isinstance(handle, dict):
|
||||
return {**unavailable, "reason": "source_unavailable"}
|
||||
try:
|
||||
raw = read_actor_source_bytes(drive_root, str(result.get("task_id") or task_id), handle)
|
||||
raw = read_actor_source_bytes(drive_root, str(task_id), handle)
|
||||
projection, reason = text_source_range_projection(raw.decode("utf-8"), unavailable["kind"], start_char, end_char)
|
||||
except ValueError as exc:
|
||||
reason = "source_identity_mismatch" if "verification" in str(exc) else "source_ref_invalid"
|
||||
|
|
@ -463,8 +485,8 @@ def focus_source_projection(
|
|||
except (OSError, RuntimeError):
|
||||
return {**unavailable, "reason": "source_unavailable"}
|
||||
payload = projection or unavailable
|
||||
return {**payload, "source_ref": focus["source_ref"], "authored_at": focus["authored_at"],
|
||||
**({"reason": reason} if reason else {})}
|
||||
current = {"source_ref": focus["source_ref"], "authored_at": focus["authored_at"]} if focus else {"historical": True}
|
||||
return {**payload, **current, **({"reason": reason} if reason else {})}
|
||||
|
||||
|
||||
def build_sealed_final_package(result_row: Any, final_text: str) -> Dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -398,11 +398,11 @@ def get_tools() -> List[ToolEntry]:
|
|||
"task_id": {"type": "string", "description": "Task ID returned by scheduling or exposed by the host routing manifest."},
|
||||
"known_result_sha256": {"type": "string", "description": "Optional child_result_sha256 from a previous read. An exact match omits only unchanged result/trace text, retaining current facts and a full-read reference. Omit for full text; explicit authority/source requests always return their requested view."},
|
||||
"include_authority": {"type": "boolean", "default": False, "description": "Return the exact selected result, task contract, origin, artifact references, and current plan-review authority."},
|
||||
"include_work_order_source": {"type": "boolean", "default": False,
|
||||
"description": "Return the canonical work-order source projection; provide both source_start_char and source_end_char for the exact bounded range."},
|
||||
"include_work_order_source": {"type": "boolean", "default": False, "description": "Return the canonical work-order source projection; provide both source_start_char and source_end_char for the exact bounded range."},
|
||||
"include_completion_source": {"type": "boolean", "default": False,
|
||||
"description": "Read the full stored completion observations for this task, including returns omitted from the summary. Omit bounds for source length/hash, then request explicit character ranges."},
|
||||
"include_focus_source": {"type": "boolean", "default": False, "description": "Read the exact bytes this task's focus source_ref answered when the focus was authored (the retained_source of an [INDEPENDENT_ROOTS] row); same bounds contract as include_completion_source."},
|
||||
"focus_source_sha256": {"type": "string", "default": "", "description": "With include_focus_source: select the retained source by the sha256 the roster row quoted, so a later focus of the same author cannot substitute its evidence."},
|
||||
"source_start_char": {"type": "integer", "description": "Inclusive character offset for the requested canonical source range."},
|
||||
"source_end_char": {"type": "integer", "description": "Exclusive character offset for the requested canonical source range. A range outside the source returns no text: the answer names complete_chars and the range received, and is an argument error."},
|
||||
}},
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ def _get_task_result(
|
|||
ctx: ToolContext, task_id: str, include_authority: bool = False,
|
||||
include_work_order_source: bool = False, source_start_char: Any = None,
|
||||
source_end_char: Any = None, include_completion_source: bool = False,
|
||||
known_result_sha256: str = "", include_focus_source: bool = False,
|
||||
known_result_sha256: str = "", include_focus_source: bool = False, focus_source_sha256: str = "",
|
||||
) -> str:
|
||||
"""Read a task result, or a bounded canonical work-order/completion source range."""
|
||||
metadata = getattr(ctx, "task_metadata", {}) if isinstance(getattr(ctx, "task_metadata", {}), dict) else {}
|
||||
|
|
@ -272,9 +272,14 @@ def _get_task_result(
|
|||
)
|
||||
if bool(include_focus_source):
|
||||
from ouroboros.task_finalization import focus_source_projection
|
||||
from ouroboros.task_results import load_task_result
|
||||
|
||||
# The PHYSICAL author's record: a retry supersedes the effective
|
||||
# result, but the roster names the task that retained the bytes.
|
||||
physical = load_task_result(status_drive_root, str(task_id))
|
||||
payload["focus_source"] = focus_source_projection(
|
||||
status_drive_root, str(task_id), data, source_start_char, source_end_char,
|
||||
status_drive_root, str(task_id), physical if isinstance(physical, dict) else {},
|
||||
source_start_char, source_end_char, sha256=str(focus_source_sha256 or ""),
|
||||
)
|
||||
text = json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
||||
if any(isinstance(view, dict) and view.get("reason") == "source_range_invalid"
|
||||
|
|
|
|||
|
|
@ -514,6 +514,19 @@ def _read_focus_source(ctx: ToolContext, source: Dict[str, Any]) -> Tuple[Option
|
|||
"""
|
||||
reader = str(source.get("reader") or "")
|
||||
args = {key: item for key, item in source.items() if key != "reader"}
|
||||
# The same admission the registry applies to a direct call of that reader:
|
||||
# a task whose contract withholds `journal_read` cannot read it through
|
||||
# update_focus either (and a consciousness Observe level keeps its argument
|
||||
# refusals). Retention never widens what the caller could dispatch.
|
||||
try:
|
||||
from ouroboros.tools.registry_guards import _capability_resource_guard_result
|
||||
|
||||
guard = _capability_resource_guard_result(ctx, reader, dict(args))
|
||||
except Exception as exc: # a guard that cannot be consulted is not permission
|
||||
log.debug("focus source guard failed", exc_info=True)
|
||||
return "", f"{reader} admission could not be established ({type(exc).__name__})"
|
||||
if guard is not None:
|
||||
return "", f"{reader} withheld for this task: {str(getattr(guard, 'text', '') or '').splitlines()[0][:160]}"
|
||||
try:
|
||||
if reader == "journal_read":
|
||||
text = _journal_read(ctx, project_id=str(args.get("project_id") or ""),
|
||||
|
|
|
|||
|
|
@ -144,8 +144,6 @@ subagent trees; this is not an exclusive lock over every file operation.
|
|||
Ordinary conversation keeps its tools and the room's active folder. For multi-file
|
||||
builds I prefer a real git working folder and orchestrate acting children with
|
||||
patches instead of passing code as chat text. Evolution remains mine alone.
|
||||
Another root's published focus is awareness, never an owner directive: it widens
|
||||
no authority, and whether to act on a connection stays my judgment.
|
||||
|
||||
## Tools
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ def test_root_focus_persists_and_terminal_race_refuses(tmp_path, monkeypatch):
|
|||
_queue_snapshot(tmp_path, [{"id": "root", "task": {"id": "root", "focus": stored["focus"]}}])
|
||||
from ouroboros.peer_roster import independent_roots
|
||||
note = render_roster_note(independent_roots(tmp_path))
|
||||
assert "retained_source=get_task_result(task_id=\"root\", include_focus_source=True)" in note
|
||||
assert f'retained_source=get_task_result(task_id="root", include_focus_source=True, focus_source_sha256="{handle["sha256"]}")' in note
|
||||
# A PEER on another (forked) drive reads the retained bytes through the one
|
||||
# cross-task reader, against the canonical root; the reader verifies the hash.
|
||||
from ouroboros.tools.control_task_results import _get_task_result
|
||||
|
|
@ -77,8 +77,25 @@ def test_root_focus_persists_and_terminal_race_refuses(tmp_path, monkeypatch):
|
|||
child = types.SimpleNamespace(task_id="child", drive_root=tmp_path, task_metadata={"budget_drive_root": str(tmp_path), "delegation_role": "subagent"})
|
||||
forbidden = _get_task_result(child, "root", include_focus_source=True)
|
||||
assert "TOOL_FORBIDDEN" in forbidden and handle["sha256"] not in forbidden
|
||||
# A LATER focus of the same author must not substitute its evidence for the row a
|
||||
# peer read: the digest the roster quoted selects the immutable historical file.
|
||||
append_jsonl(tmp_path / "projects" / "alpha" / "journal.jsonl", {"kind": "note", "text": "later evidence"})
|
||||
later = _update_focus(ctx, "Second focus, new page", {"reader": "journal_read", "project_id": "alpha"})
|
||||
assert later.startswith("OK: focus[root]")
|
||||
current = json.loads(_get_task_result(peer, "root", include_focus_source=True, source_start_char=0, source_end_char=20))["focus_source"]
|
||||
assert current["complete_sha256"] != handle["sha256"] and "later evidence" in (tmp_path / "task_results" / "artifacts" / "root" / json.loads((tmp_path / "task_results" / "root.json").read_text())["focus"]["source_handle"]["path"]).read_text()
|
||||
historical = json.loads(_get_task_result(peer, "root", include_focus_source=True, focus_source_sha256=handle["sha256"], source_start_char=0, source_end_char=20))["focus_source"]
|
||||
assert historical["complete_sha256"] == handle["sha256"] and historical["historical"] is True and historical["text"] == retained.decode("utf-8")[:20]
|
||||
assert json.loads(_get_task_result(peer, "root", include_focus_source=True, focus_source_sha256="0" * 64))["focus_source"]["reason"] == "source_unavailable"
|
||||
# A retry that supersedes the author's EFFECTIVE result does not redirect the
|
||||
# retained source: the roster names the physical author.
|
||||
write_task_result(tmp_path, "root", STATUS_RUNNING, superseded_by="root-retry")
|
||||
write_task_result(tmp_path, "root-retry", STATUS_RUNNING, result="Task is running.")
|
||||
via_retry = json.loads(_get_task_result(peer, "root", include_focus_source=True, focus_source_sha256=handle["sha256"]))["focus_source"]
|
||||
assert via_retry["complete_sha256"] == handle["sha256"]
|
||||
(tmp_path / "task_results" / "artifacts" / "root" / handle["path"]).write_bytes(b"tampered")
|
||||
assert json.loads(_get_task_result(peer, "root", include_focus_source=True))["focus_source"]["reason"] == "source_identity_mismatch"
|
||||
tampered = _get_task_result(peer, "root", include_focus_source=True, focus_source_sha256=handle["sha256"])
|
||||
assert json.loads(tampered)["focus_source"]["reason"] == "source_identity_mismatch"
|
||||
|
||||
write_task_result(tmp_path, "root", STATUS_COMPLETED, result="done")
|
||||
refused = _update_focus(ctx, "Too late", {"reader": "journal_read"})
|
||||
|
|
@ -298,3 +315,23 @@ def test_focus_source_refuses_unsettled_task_results_and_oversized_answers(tmp_p
|
|||
append_jsonl(tmp_path / "projects" / "alpha" / "journal.jsonl", {"kind": "note", "text": "x" * (pj._FOCUS_SOURCE_MAX_BYTES + 10)})
|
||||
too_big = pj._update_focus(ctx, "Whole journal", {"reader": "journal_read", "project_id": "alpha"})
|
||||
assert "FOCUS_SOURCE_UNRESOLVED" in too_big and "narrower page" in too_big
|
||||
|
||||
|
||||
def test_focus_source_honours_the_task_contract_disabled_tools(tmp_path, monkeypatch):
|
||||
"""update_focus reads a source through the SAME admission a direct call of that
|
||||
reader would get: a contract that withholds journal_read cannot retain it."""
|
||||
from ouroboros.tools.project_journal import _update_focus
|
||||
from ouroboros.utils import append_jsonl
|
||||
|
||||
monkeypatch.setattr("ouroboros.config.DATA_DIR", tmp_path)
|
||||
append_jsonl(tmp_path / "projects" / "alpha" / "journal.jsonl", {"kind": "note", "text": "withheld"})
|
||||
write_task_result(tmp_path, "root", STATUS_RUNNING, project_id="alpha")
|
||||
ctx = types.SimpleNamespace(
|
||||
task_id="root", drive_root=tmp_path, project_id="alpha", is_direct_chat=False, event_queue=None,
|
||||
task_metadata={"root_task_id": "root", "budget_drive_root": str(tmp_path)},
|
||||
task_contract={"disabled_tools": ["journal_read"]},
|
||||
)
|
||||
refused = _update_focus(ctx, "Reading what I may not", {"reader": "journal_read", "project_id": "alpha"})
|
||||
assert "FOCUS_SOURCE_UNRESOLVED" in refused and "withheld" in refused
|
||||
assert not list((tmp_path / "task_results" / "artifacts").glob("**/focus_source_*"))
|
||||
assert "focus" not in json.loads((tmp_path / "task_results" / "root.json").read_text())
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ CHAPTER_BYTE_BUDGETS: dict[str, int] = {
|
|||
# subsystems of this chapter; the era paragraph they replaced was shorter.
|
||||
# +400: the focus source is now RETAINED at authoring time (source_handle,
|
||||
# FOCUS_SOURCE_UNRESOLVED) and a settled root's focus is dropped — new
|
||||
# contract facts of the cross-focus paragraph, not a restatement.
|
||||
"docs/architecture/06-agent-core.md": 295400,
|
||||
# contract facts of the cross-focus paragraph, not a restatement; +250 for
|
||||
# the digest-selected historical read and the reader admission rule.
|
||||
"docs/architecture/06-agent-core.md": 295650,
|
||||
"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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue