Retain the focus source as a native task_source ref readable through get_task_result; bound it, refuse unsettled task sources

This commit is contained in:
Ouroboros 2026-09-22 19:12:51 +03:00
parent 8f8c5bdf4e
commit c693c0fad3
12 changed files with 143 additions and 49 deletions

View file

@ -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; the handler answers it at authoring time through that reader under the caller's authority and retains the exact answer write-once as `focus.source_handle` (a closed `runtime_data` pointer the roster renders as `retained_source`) — a refusing reader or a changed snapshot refuses the focus (`FOCUS_SOURCE_UNRESOLVED`), since a pointer at a page rewritten once the author is dormant identifies nothing; the roster drops the focus of a root whose durable result already settled. 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 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.
`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.

View file

@ -16,8 +16,8 @@ from ouroboros.utils import utc_now_iso
_MAX_TEXT = 280
_MAX_SOURCE = 512
_MAX_HANDLE = 640
_HANDLE_PATH_PREFIX = "task_results/artifacts/"
_HANDLE_KEYS = {"kind", "root", "path", "size", "sha256", "task_id"}
_HANDLE_PATH_PREFIX = "source_handles/context_checkpoints/"
_HANDLE_KEYS = {"kind", "root", "path", "size", "sha256"}
# A focus source is a pointer to one of the existing bounded readers. Keep
# this contract deliberately small: source references are metadata, not a new
@ -90,23 +90,27 @@ def _safe_handle(value: Any) -> Optional[Dict[str, Any]]:
"""Validate the retained-source pointer a focus carries.
A source_ref names a reader; the handle proves what that reader ANSWERED at
authoring time: exact bytes stored write-once under the author task's
source-handle store, addressed relative to the canonical data root so any
actor that may read runtime data can resolve them after the task is dormant.
The shape is closed: no free path language, no foreign root.
authoring time: exact bytes stored write-once in the author task's
source-handle store on the CANONICAL data root, in the native ``task_source``
shape ``artifacts.read_actor_source_bytes`` verifies (size + sha256, no
symlink, no escape). Peers read it through
``get_task_result(task_id=<author>, include_focus_source=True)`` — the one
cross-task reader — so the pointer keeps identifying its evidence after the
author is dormant, from any drive. The shape is closed: no free path
language, no foreign root.
"""
if not isinstance(value, Mapping):
return None
handle = dict(value)
if set(handle) != _HANDLE_KEYS:
return None
if handle.get("kind") != "task_source" or handle.get("root") != "runtime_data":
if handle.get("kind") != "task_source" or handle.get("root") != "artifact_store":
return None
path = handle.get("path")
if not isinstance(path, str) or not path.startswith(_HANDLE_PATH_PREFIX):
return None
parts = path.split("/")
if any(part in ("", ".", "..") for part in parts) or "\\" in path or "\x00" in path:
if len(parts) != 3 or any(part in ("", ".", "..") for part in parts) or "\\" in path or "\x00" in path:
return None
size = handle.get("size")
if isinstance(size, bool) or not isinstance(size, int) or size < 0:
@ -114,17 +118,6 @@ def _safe_handle(value: Any) -> Optional[Dict[str, Any]]:
digest = handle.get("sha256")
if not isinstance(digest, str) or len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
return None
task_id = handle.get("task_id")
if not isinstance(task_id, str) or not task_id.strip():
return None
try:
from ouroboros.task_results import validate_task_id
validate_task_id(task_id)
except (ImportError, TypeError, ValueError):
return None
if parts[2] != task_id:
return None
encoded = json.dumps(handle, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
if len(encoded.encode("utf-8")) > _MAX_HANDLE:
return None
@ -151,8 +144,8 @@ def normalize_focus(text: Any, source_ref: Any, *, task_id: str = "", authored_a
handle = _safe_handle(source_handle)
if handle is None:
raise ValueError("source_handle must be a retained task_source pointer")
if handle["task_id"] != focus["author_task_id"]:
raise ValueError("source_handle must belong to the authoring task")
if not focus["author_task_id"]:
raise ValueError("source_handle requires an authoring task")
focus["source_handle"] = handle
return focus

View file

@ -206,8 +206,9 @@ def render_roster_note(roster: Dict[str, Any], *, exclude: str = "") -> str:
handle = focus.get("source_handle")
if handle:
# The retained bytes the reader answered at authoring time: what
# the source_ref still identifies once the author is dormant.
line += (f" · retained_source=read_file(root='runtime_data', path={json.dumps(handle['path'])})"
# 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']}")
lines.append(line)
if not shown:

View file

@ -442,6 +442,31 @@ def completion_source_projection(
return {**payload, **({"reason": reason} if reason else {})}
def focus_source_projection(
drive_root: Any, task_id: str, result: Dict[str, Any], start_char: Any = None, end_char: Any = None,
) -> 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
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
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)
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"
return {**unavailable, "reason": reason}
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 {})}
def build_sealed_final_package(result_row: Any, final_text: str) -> Dict[str, Any]:
"""Host-attested final outcome: delivered text + artifact-store manifest.

View file

@ -397,12 +397,12 @@ def get_tools() -> List[ToolEntry]:
"parameters": {"type": "object", "required": ["task_id"], "properties": {
"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_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_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."},
"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."},
}},

View file

@ -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 = "",
known_result_sha256: str = "", include_focus_source: bool = False,
) -> 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 {}
@ -217,7 +217,7 @@ def _get_task_result(
"this one is lost is yours to judge from the two times above."
),
))
if bool(include_authority) or bool(include_work_order_source) or bool(include_completion_source):
if bool(include_authority) or bool(include_work_order_source) or bool(include_completion_source) or bool(include_focus_source):
from ouroboros.agent_startup_checks import task_result_authority_projection
authority = task_result_authority_projection(data, drive_root=status_drive_root)
@ -259,9 +259,15 @@ def _get_task_result(
payload["completion_source"] = completion_source_projection(
status_drive_root, str(task_id), data, source_start_char, source_end_char,
)
if bool(include_focus_source):
from ouroboros.task_finalization import focus_source_projection
payload["focus_source"] = focus_source_projection(
status_drive_root, str(task_id), data, source_start_char, source_end_char,
)
text = json.dumps(payload, ensure_ascii=False, sort_keys=True)
if any(isinstance(view, dict) and view.get("reason") == "source_range_invalid"
for view in (payload.get("work_order_source"), payload.get("completion_source"))):
for view in (payload.get("work_order_source"), payload.get("completion_source"), payload.get("focus_source"))):
# The requested text was NOT returned: same JSON (it names complete_chars and
# the range received), recorded as the argument fault it is, never as `ok`.
return _publish_tool_result(ctx, ToolResult(status="error", code="TOOL_ARG_ERROR", text=text))

View file

@ -497,6 +497,10 @@ def _workpad_write(ctx: ToolContext, content: str, project_id: str = "") -> str:
_FOCUS_SOURCE_REFUSAL_PREFIXES = (
"⚠️", "JOURNAL_READ_SNAPSHOT_CHANGED", "CHAT_HISTORY_SNAPSHOT_CHANGED",
)
# A focus points at ONE bounded page of an existing reader; a retained answer is
# a cognitive artifact, not a mirror of the store. Larger answers are refused
# with the repair (narrow the page), never clipped.
_FOCUS_SOURCE_MAX_BYTES = 256 * 1024
def _read_focus_source(ctx: ToolContext, source: Dict[str, Any]) -> Tuple[Optional[str], str]:
@ -523,7 +527,15 @@ def _read_focus_source(ctx: ToolContext, source: Dict[str, Any]) -> Tuple[Option
from ouroboros.tools.recent_tasks import _handle_live_roots
text = _handle_live_roots(ctx, offset=int(args.get("offset") or 0), snapshot=str(args.get("snapshot") or ""))
elif reader == "get_task_result":
from ouroboros.task_status import load_effective_task_result
from ouroboros.routing_wait import is_emitted_admission_stub
from ouroboros.tool_access import canonical_data_root
from ouroboros.tools.control_task_results import _get_task_result
row = load_effective_task_result(canonical_data_root(ctx), str(args.get("task_id") or ""))
if not row or is_emitted_admission_stub(row):
# The reader's unavailable/pending answers are prose without a
# typed marker; a task with no settled result is not a source.
return "", f"{reader} refused: task {args.get('task_id')} unknown or admission pending"
text = _get_task_result(ctx, task_id=str(args.get("task_id") or ""))
elif reader == "chat_history":
from ouroboros.tools.control_runtime import _chat_history
@ -548,25 +560,26 @@ def _read_focus_source(ctx: ToolContext, source: Dict[str, Any]) -> Tuple[Option
error = payload.get("error")
code = error.get("code") if isinstance(error, dict) else payload.get("host_code")
return "", f"{reader} refused: {code or 'typed error'}"
if len(body.encode("utf-8")) > _FOCUS_SOURCE_MAX_BYTES:
return "", (f"{reader} answered {len(body.encode('utf-8'))} bytes, above the {_FOCUS_SOURCE_MAX_BYTES}-byte "
"focus source bound; point the focus at a narrower page (limit/offset/snapshot)")
return body, ""
def _retain_focus_source(canonical: pathlib.Path, task_id: str, source: Dict[str, Any], body: str) -> Dict[str, Any]:
"""Store the reader's exact answer write-once and return the focus handle."""
from ouroboros.artifacts import store_actor_source_bytes, task_artifact_dir_path
"""Store the reader's exact answer write-once on the canonical root; return the handle.
The handle is the native ``task_source`` ref minus its ``read`` block: peers
resolve it through ``get_task_result(include_focus_source=True)`` against the
same canonical root the durable task result lives on, not through the
author's own (possibly forked) ``artifact_store``.
"""
from ouroboros.artifacts import store_actor_source_bytes
ref = store_actor_source_bytes(canonical, task_id, category="context_checkpoints",
source_id=f"focus_source_{source.get('reader')}",
data=body.encode("utf-8"), extension="md")
path = task_artifact_dir_path(canonical, task_id, create=False) / ref["path"]
return {
"kind": "task_source",
"root": "runtime_data",
"path": path.relative_to(canonical).as_posix(),
"size": int(ref["size"]),
"sha256": str(ref["sha256"]),
"task_id": task_id,
}
return {key: ref[key] for key in ("kind", "root", "path", "size", "sha256")}
def _update_focus(ctx: ToolContext, text: str, source_ref: Any) -> str:

View file

@ -735,7 +735,7 @@ _EXACT_IDENTIFIER_CODES = MappingProxyType(
# nothing durably); cross-focus refusals split availability from policy.
"SCOPE_REJECTED": "TOOL_REPORTED_FAILURE", "SCOPE_UNCONFIRMED": "TOOL_REPORTED_FAILURE",
"FOCUS_PROJECTION_UNAVAILABLE": "LEGACY_UNAVAILABLE", "FOCUS_TASK_NOT_LIVE": "LEGACY_UNAVAILABLE",
"FOCUS_STALE": "LEGACY_BLOCKED", "TOOL_FORBIDDEN": "LEGACY_BLOCKED",
"FOCUS_STALE": "LEGACY_BLOCKED", "TOOL_FORBIDDEN": "LEGACY_BLOCKED", "FOCUS_SOURCE_UNRESOLVED": "LEGACY_UNAVAILABLE", "FOCUS_SOURCE_UNRETAINED": "LEGACY_UNAVAILABLE",
"TOOL_ERROR": "TOOL_ERROR",
"TOOL_INTERNAL_ERROR": "TOOL_INTERNAL_ERROR",
"EXECUTOR_UNAVAILABLE": "LEGACY_UNAVAILABLE",

View file

@ -939,6 +939,22 @@
"is_error": true,
"status": "error"
},
"ident:FOCUS_SOURCE_UNRESOLVED:named": {
"is_error": false,
"status": "ok"
},
"ident:FOCUS_SOURCE_UNRESOLVED:plain": {
"is_error": false,
"status": "ok"
},
"ident:FOCUS_SOURCE_UNRETAINED:named": {
"is_error": false,
"status": "ok"
},
"ident:FOCUS_SOURCE_UNRETAINED:plain": {
"is_error": false,
"status": "ok"
},
"ident:FOCUS_STALE:named": {
"is_error": false,
"status": "ok"

View file

@ -47,20 +47,33 @@ def test_root_focus_persists_and_terminal_race_refuses(tmp_path, monkeypatch):
# The reader's exact answer is RETAINED beside the focus: the pointer keeps
# identifying its evidence after the author is dormant and the journal grows.
handle = stored["focus"]["source_handle"]
assert handle["root"] == "runtime_data" and handle["task_id"] == "root"
retained = (tmp_path / handle["path"]).read_bytes()
assert set(handle) == {"kind", "root", "path", "size", "sha256"} and handle["root"] == "artifact_store"
retained = (tmp_path / "task_results" / "artifacts" / "root" / handle["path"]).read_bytes()
assert hashlib.sha256(retained).hexdigest() == handle["sha256"] and len(retained) == handle["size"]
assert b"seam exact evidence" in retained
from ouroboros.focus import compact_focus, focus_fingerprint
assert compact_focus(stored["focus"]) == stored["focus"]
assert handle["sha256"] in focus_fingerprint(stored["focus"])
forged = {**stored["focus"], "source_handle": {**handle, "path": "../secret"}}
assert compact_focus(forged) is None
for forged_path in ("../secret", "source_handles/context_checkpoints/../../x.md",
"task_results/artifacts/other/source_handles/context_checkpoints/a.md"):
assert compact_focus({**stored["focus"], "source_handle": {**handle, "path": forged_path}}) is None
from ouroboros.peer_roster import render_roster_note
_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 f"retained_source=read_file(root='runtime_data', path={json.dumps(handle['path'])})" in note
assert "retained_source=get_task_result(task_id=\"root\", include_focus_source=True)" 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
peer = types.SimpleNamespace(task_id="peer", drive_root=tmp_path / "fork", task_metadata={"budget_drive_root": str(tmp_path)})
view = json.loads(_get_task_result(peer, "root", include_focus_source=True))["focus_source"]
assert view["reason"] == "source_range_required" and view["complete_sha256"] == handle["sha256"]
assert view["complete_chars"] == len(retained.decode("utf-8")) and "status" not in view
assert view["source_ref"] == {"reader": "journal_read", "project_id": "alpha"}
ranged = json.loads(_get_task_result(peer, "root", include_focus_source=True, source_start_char=0, source_end_char=20))["focus_source"]
assert ranged["text"] == retained.decode("utf-8")[:20]
(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"
write_task_result(tmp_path, "root", STATUS_COMPLETED, result="done")
refused = _update_focus(ctx, "Too late", {"reader": "journal_read"})
@ -256,3 +269,25 @@ def test_settled_root_carries_no_live_focus_when_the_queue_snapshot_lags(tmp_pat
rows = {row["task_id"]: row for row in independent_roots(tmp_path)["roots"]}
assert "focus" not in rows["root"]
assert rows["live"]["focus"]["author_task_id"] == "live"
def test_focus_source_refuses_unsettled_task_results_and_oversized_answers(tmp_path, monkeypatch):
"""A get_task_result source with no settled row is prose, not evidence; a
reader answer above the focus bound is refused with the repair, never clipped."""
from ouroboros.tools import project_journal as pj
from ouroboros.utils import append_jsonl
monkeypatch.setattr("ouroboros.config.DATA_DIR", tmp_path)
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,
task_metadata={"root_task_id": "root", "budget_drive_root": str(tmp_path)}, event_queue=None,
)
refused = pj._update_focus(ctx, "Pointing at a ghost", {"reader": "get_task_result", "task_id": "missing1"})
assert "FOCUS_SOURCE_UNRESOLVED" in refused and "unknown or admission pending" in refused
write_task_result(tmp_path, "done1", STATUS_COMPLETED, result="settled answer")
ok = pj._update_focus(ctx, "Pointing at a settled result", {"reader": "get_task_result", "task_id": "done1"})
assert ok.startswith("OK: focus[root]")
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

View file

@ -279,6 +279,8 @@ APPROVED_DELTAS: Mapping[str, Delta] = MappingProxyType({
"FOCUS_PROJECTION_UNAVAILABLE": Delta(True, "error", True, "unavailable", "A.25", "a direct focus projection the host cannot accept is unavailable, not a generic execution error"),
"FOCUS_TASK_NOT_LIVE": Delta(False, "ok", True, "unavailable", "A.25", "a focus update for a settled task has no live publication target"),
"FOCUS_STALE": Delta(False, "ok", True, "blocked", "A.25", "a newer focus wins the CAS and blocks the stale publication"),
"FOCUS_SOURCE_UNRESOLVED": Delta(False, "ok", True, "unavailable", "A.25", "a focus source the named reader refused or cannot answer is unavailable evidence, not a published focus"),
"FOCUS_SOURCE_UNRETAINED": Delta(False, "ok", True, "unavailable", "A.25", "a focus whose source answer could not be stored has no retained evidence to publish"),
"TOOL_FORBIDDEN": Delta(True, "error", True, "blocked", "A.25", "an unauthorized project/focus operation is a policy denial, not a generic tool failure"),
# Owner's recovered transport WORK-ORDER B7 / #744: these producers now
# publish existing codes for known refusals. No text-adapter policy changed.

View file

@ -877,7 +877,9 @@ def test_era_compression_preserves_gap_markers(tmp_path):
chat, blocks, meta = _chat_layout(tmp_path)
old_blocks = [
{"ts": "2026-07-01T00:00:00Z", "type": "summary", "range": "r",
"message_count": 100, "content": "old block A"},
"message_count": 100, "content": "old block A1 " + "detail " * 40},
{"ts": "2026-07-01T06:00:00Z", "type": "summary", "range": "r",
"message_count": 100, "content": "old block A2 " + "detail " * 40},
{"ts": "2026-07-01T12:00:00Z", "type": "summary", "range": "unknown",
"message_count": 0, "gap_id": "gap:test", "content": "[MEMORY GAP] test"},
{"ts": "2026-07-02T00:00:00Z", "type": "summary", "range": "r",
@ -900,12 +902,13 @@ def test_era_compression_preserves_gap_markers(tmp_path):
blocks_after = json.loads(blocks.read_text(encoding="utf-8"))
gap_positions = [i for i, b in enumerate(blocks_after) if b.get("gap_id") == "gap:test"]
assert len(gap_positions) == 1
# The era compressed ONLY the pre-gap run ("old block A"); the gap keeps its
# The era compressed ONLY the pre-gap run (A1+A2); the gap keeps its
# chronological slot right after it, and post-gap blocks B/C stay intact.
assert gap_positions[0] == 1
texts = [b.get("content", "") for b in blocks_after]
assert "old block B" in texts and "old block C" in texts
assert "old block A" not in texts # compressed into the era
assert not any(t.startswith("old block A") for t in texts) # compressed into the era
assert "Era or block summary." in texts[0] # the era is shorter than the run it replaced
def test_consolidator_rotation_between_resolve_and_first_capture(tmp_path, monkeypatch):