Repository navigation
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…b.com/OpenHCSDev/agent-comms into work/claude-nra--P872
ReadNotifications carried a caller-choosable viewer field so the MCP outcomes tool could reuse it under the caller's authority. That made the reader's authority an argument any caller could set, and the UI and MCP sides each restated who reads. Now a BusRead member owns its arguments and its read once, as read_as(comms, reader); the boundary supplies the BusReader. UiRead derives the UI's read() as the operator, and the MCP tool reads under its own ToolCaller, which is now a BusReader. OperatorOutcomes moves to bus_read beside the reader it serves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Handoff: exact state of #3538 at 3c23ca2 (P872 item 4)Landed on the branch (step 1, c57a747, +77/-29 over 5 files):
Nit to fold in: Remaining migration, checked against origin/main just now:
Done list: (A) input delivery is one declaration and the MCP side reports live scope; (B) every #3537 pair is migrated; (C)
|
|
debt-src at 80cff6a, every finding: |
) ContentCache stored mypy counts under a key of committed content and checker versions, but mypy also reads the installed packages, which no key names. Hosted #3538 restored debt-ratchet-src-71453f18 (saved by main cache job 40654054315 under a Core-only install, Pydantic 2.14.1) and compared that stored base count with a head count measured under the full stack (Pydantic 2.13.5). - CheckedRevision.findings runs the checker. TypeErrors inherits it and keeps mypy's own --cache-dir, which tracks dependency changes itself. - LintFindings.findings keeps the content-keyed memo unchanged: Ruff reads only the tree and the head's configuration. - Unformatted keeps its per-file blob cache unchanged. - ClientTypeErrors.findings calls checked directly. The client commit and src tree it computed only for the key are gone. Regression: one parameterized real-mypy test switches an installed PEP 561 dependency between two sys.path environments, with committed source, client and configuration unchanged and the same count-cache directory. The count must follow the dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
GoalHistory (tools.py) and GoalHistoryReply (runtime_requests.py) were
empty subclasses of RecordedGoalHistory, and recorded_history took the
result class as an argument so each caller picked its own empty wrapper.
Neither added a field or a behavior.
- RecordedGoalHistory now extends ToolResult (itself a DeclaredReply), so
the MCP tool and the owner socket return the same record.
- GoalHistory and GoalHistoryReply are deleted.
- GoalManagement.recorded_history(name, scope) returns RecordedGoalHistory;
the reply argument, its HistoryReply TypeVar and the TypeVar import are gone.
- CommsGoalHistoryTool and GoalHistoryRuntimeRequest return it directly;
tool name, declared runtime name, subject resolution, scope decoding and
the worker-thread execution are unchanged.
- tests/test_goal_execution.py compares against RecordedGoalHistory.empty().
The encoded record is unchanged: one populated history/failure payload
decodes and re-encodes byte-identically through GoalHistory,
GoalHistoryReply and RecordedGoalHistory before this commit and through
RecordedGoalHistory after it; empty() encodes as
{"failures": [], "history": []} on both sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…the owner (#3538) CommsInputDeliveryTool read goals.input_delivery in the connector's own process under UNOBSERVED_DELIVERY_SCOPE: it could not see the owner's live queue, yet returned an ordinary-looking delivery view. The UI and the owner socket each restated the same read again (ReadDeliveryOverview, InputDispositionsRuntimeRequest, with their own include_history). - BusRead is now an abstract capability of the existing ToolDeclaration family (no DeclaredFamily root of its own); UiRead still derives from it. - BusReader owns subject/own_thread/refuse_thread once; the copies on ToolCaller and NoThreadCaller are gone, refusal text and type unchanged. - OwnerReading: one exchange with one held owner incarnation; admit() defaults to any owner, OwnerLiveRead admits only native owners, so an external bridge (live PID, no socket) is refused instead of retried forever. - OwnerReplyConnection.held/answer: the one connection factory (exact incarnation, root, process; 3 s reply timeout, close in finally, no retry after dispatch) for OwnerRead and OwnerLiveRead. - OwnerLiveRead: MCP apply/read_as resolves caller.subject(thread) and asks the owner via asyncio.run on the tool's synchronous boundary; the owner answers in owned(ctx) with ctx.name and its own session. - InputDeliveryRead (comms_input_delivery, thread, include_history=True): the one declaration, in the ToolRequest/native catalogs by capability. owned() takes scope only from the owner's delivery_scope. - OwnerReadRuntimeRequest (owner_read) and ReadOwnerOperation: one generic runtime envelope and one generic UI envelope carrying the operation. - Deleted: CommsInputDeliveryTool, ReadDeliveryOverview, InputDispositionsRuntimeRequest, the input_dispositions request. - The connector encodes Refused, ConnectionError and TimeoutError from an owner read as a failed tool call instead of dropping the HTTP call. - tests/test_command_families.py: owner_read round trips for both history settings; a non-owner-live operation and a bad include_history refuse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
… a live owner (#3538) - test_observer_death: unavailable/stale owner reads are parametrized from ReadOwnerGoal plus ReadOwnerOperation over BusRead.members_with(OwnerLiveRead), not OwnerRead.__subclasses__(); refusal and observer-alive assertions kept. - test_input_delivery_history: the CLI test that asserted a declared owner with no process answers delivery state is deleted (that answer was the defect). The replacement starts the canonical owner socket, queues a prompt, records an off-queue notice, and asserts MCP invoke and the UI envelope both equal the owner's own projection for include_history False and True, that the off-queue notice is current only under the old unobserved projection, and that a stopped owner refuses the MCP read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…re handed (#3538) - OwnerReading.admit is the shared native-transport admission (owner.execution.require_native()). OwnerLiveRead.admit and ReadOwnerOperation.admit were identical restatements and are deleted. ReadOwnerGoal now reaches it too: an external bridge has a live PID and no RuntimeServer socket, and _connect_current waited for that socket forever before dispatch. It is refused before the wait. - OwnerReplyConnection.held compared the registry's incarnation with the supplied one, then OwnerConnection.__init__ read the registry again into _identity, so a replacement incarnation could be recaptured unchecked. held now constructs the connection and OwnerConnection.require_identity compares the captured _identity with the supplied witness before returning; the earlier separate comparison is deleted. No second identity field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…ation, death, external controls (#3538) tests/owner_socket_fixture.py (registered in pytest_plugins) is the one fixture owner: runtime_processes (moved from test_runtime.py; it now yields the spawned children so a test can stop one through its own owner), declare_owner (native or external registration) and silent_socket (the real Unix-socket peer from test_dispatched_request_without_a_reply..., which receives one request and closes without replying). The existing reply-loss test is refactored onto it. New controls, nothing of ours patched, no fabricated reply: - MCP InputDeliveryRead without a reply, for include_history False and True: OwnerReplyLostError, exactly one received request decoding to OwnerReadRuntimeRequest(thread, InputDeliveryRead(thread, include_history)), durable inputs unchanged. The peer runs no domain body. - Root replaced under a held connection (rename to a sibling, new directory at the old path): OwnerIdentityChangedError, zero requests; root restored. - Same-name recreation (unregister, begin_delete, remove, declare): the held read and a new held() with the old witness both refuse; incarnations differ. - Held process death (runtime_processes child stopped through stop_sync): the read refuses with ConnectionError before any request; inputs unchanged. - External owner with a live child and no socket: MCP delivery refuses with RelationViolationError; through the observer, ReadOwnerGoal and the input-delivery envelope (derived from OwnerLiveRead membership) both refuse and the observer stays alive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…ps importing its transport (#3538) OwnerLiveRead extracted OwnerReadRuntimeRequest.declared_name and reached runtime/runtime_requests through two function-local imports to escape the cycle that extraction created, and OwnerReadRuntimeRequest.result returned Any because OwnerLiveRead had no Result default (hosted debt-src: FunctionLocalImport +2, FamilyFlattened +1, TypeErrors runtime_requests +1, BannedVocabulary +3). - OwnerLiveRead[Result: ToolResult = ToolResult]; operation fields stay the bare family class. OwnerLiveRead no longer inherits OwnerReading and has no socket read: owned/decode_reply are its answer and decoder. read_as builds OwnerReadRuntimeRequest(thread=owner.name, operation=self), sends it through OwnerReplyConnection.answer and decodes the reply. asyncio.run stays at the synchronous tool boundary. - OwnerReadRuntimeRequest moves from runtime_requests.py to bus_read.py as OwnerReading[object] + ResultRuntimeRequest; same owner_read action and operation field. Its read(owner) is the one socket exchange: owner.request(self.declared_name, operation=self.operation). - OwnerReading and OwnerReplyConnection move into owner_connection.py beside OwnerConnection. OwnerReading.read takes OwnerConnection. OwnerReplyConnection derives OwnerConnection directly (owner_read and goal_snapshot use the base proxy_payload; only PromptRuntimeRequest needs the ACP controller token). REPLY_TIMEOUT, held/answer/close and the require_identity check are unchanged. - The native transport check is OwnerReading.require_native_owner, called from held; RuntimeRequest.require_owner(entry) is untouched. - ReadOwnerOperation.read builds the envelope with owner.session_id and decodes through the operation; ReadOwnerGoal.read takes OwnerConnection; OwnerRead.ask loses its local import. All imports are module scope. - Old definitions deleted, no aliases. tests/test_runtime.py imports the moved names from bus_read/owner_connection, and the held root/recreation/death controls hand held() the real OwnerReadRuntimeRequest and await its read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…on lifecycle (#3538) Root's selection on 5c26262 ran 60 cases: 49 passed, 11 failed. One same-name control used remove() after begin_delete, which correctly refuses Deleting status; ten observer fixtures used the removed synchronous observer surface (`with`, readable()/results()/fileno(), sync close, NotificationWindow == {}). - test_runtime same-name control: deletion completes through edit.document.remove_originals((old,)) in registry.store.editing(), the path production uses (thread_management.py:537); the captured connection, old witness, zero-dispatch peer and both OwnerIdentityChangedError refusals stay. - test_observer_death: every observer is `async with`; empty-window checks compare .outcomes. The killed-observer test uses the real receiver: one read proves startup, SIGSTOP is confirmed with waitpid(WUNTRACED), the next read is shown pending, then SIGKILL; ObserverDied carries the child pid, -SIGKILL and signal text, and StopSidebar and a later read raise that same object. A stopped child is killed and a pending task cancelled in finally. The failed-observer test asks one real read on the not-a-wire route and requires exit status 1 and the original traceback. The readable() helper is deleted. - test_busy_history_loading: three `async with` sites, the close-once test's `async with process` and awaited repeated close, .outcomes comparisons; the Connection.close endpoint observation is unchanged. - test_deleted_thread_observation and test_unreadable_history_observation (the two readable() importers) start the receiver once with start_receiving and wait on tests/observed_queue.py, a test-local queue of delivered results that raises a receiver failure as its original exception. No manual pumping. No production change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…ked for (#3538) Root's rerun on 9bb8b2e: 22 cases in 70.37 s, 20 passed, 2 failed, both the other_root=False stale-identity cases (goal and input delivery). held looked up the selected thread's process before any identity comparison, so a stale incarnation was refused as "no owner process" (RelationViolationError) instead of OwnerIdentityChangedError. a4702b1 had dropped the initial comparison when it added the constructor check. - OwnerReplyConnection.held: registry.require(owner.name), then thread.incarnation != owner -> OwnerIdentityChangedError, then reading.require_native_owner(thread), then require_process/socket path and construction, then connection.require_identity(owner). The two comparisons check two different registry reads (the selected thread and the constructor's capture); no identity field or cache is added. - ObservedQueue.until holds one 30 s bound over the whole match; next() keeps its own bound for direct callers. Before, each unrelated result restarted the bound, so a steady stream could keep a wait running indefinitely. - test_deleted_thread_observation restores one 30 s bound around waiting for both the missing-thread result and the retirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…one (#3538) The notification read lived on the UI request: ReadNotifications held root and request_id beside its references, ReadRecordedNotifications subclassed it for the archive, and comms_message_outcomes built that UI envelope with str(comms.root) only to reach read_as. - notification_reads.py (new): NotificationRead(UiRead[NotificationWindow]) with changed_since; frozen LiveNotificationRead(references) and RecordedNotificationRead(references, source). Two members: archive identity and the change rule differ (live activity/evidence vs history revision). The module sits outside notification_read.py because presentation imports that module and bus_read imports presentation. - NotificationsRead and UnreadableNotificationsRead move unchanged into notification_read.py beside CapturedNotificationWindow. - ReadNotifications(ReadRequest) keeps root and request_id and carries operation: NotificationRead; it is no longer a UiRead. read() delegates to the operation (OperatorReader). ReadRecordedNotifications is deleted. NotificationReading asks request.operation.changed_since; read states stay keyed by the whole request. - CommsMessageOutcomesTool reads LiveNotificationRead((reference,)) under its caller; refusal, visibility, paging and its public schema are unchanged. - Callers migrated: observer fixtures carry LiveNotificationRead(()), test_thread_read_results reads the operation instead of a fake UI root, read-state tests call the operations. New controls: family/codec/pickle and request identity, archive change drops state, root validation per envelope, capability discovery, a foreign operation refused; a 125-message live batch across two window chunks; the recorded operation reads the archive (not live evidence) and a detached key refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…e says what it returns (#3538) NotificationsRead.activity spelled "this participant has no projection" twice: absence from the window's watched names, and a None value. observed_activity already omits registered humans and archived rows and answers DeletedThread for a deleted name, so the None was a second encoding of absence. - activity is Mapping[str, ObservedActivity | DeletedThread], keeping only the window's names that the projection actually returned. - live_changed re-projects the window's full activity_names() when ActivityView moved and compares the sparse maps, so omitted->observed, observed->omitted and observed->DeletedThread still invalidate. With no ActivityView change it does not project at all. - HistorySource.notification_references annotates references as Sequence[MessageReference] and returns NotificationWindow, the type MessageNotification.read_references already returns. That closes the Any that reached RecordedNotificationRead.read_as. - The read-state fixture that built {absent-participant: None} now settles a real #team read whose participants are an agent, a human, an archived agent and a deleted agent. New controls cover: omitted humans and archived agents, an archived agent declared again, a deleted participant, and zero or one projection by revision kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
NotificationWindow.activity_names() comes from native-recipient outcomes, so the
#team fixture watches worker, archived and gone, never the agent sender or the
human. Two assertions expected sender and human, and failed in the root run
(29 passed, 2 failed). The window-names test now requires exactly
{worker, archived, gone} and excludes sender and human; only archived is omitted
from the sparse activity. The projection-count test requires archived as the one
watched-but-omitted name. No production change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…e no acquired evidence (#3538) A recipient-scoped thread read selected every claim of each broadcast by wire_seq alone, decoded them, and filtered recipients only afterwards: the saved six-tab desktop held 17,681 claims to show 15 rows. The same acquired graph (frozen audiences, decisions, WakeAssignment and activity rows) then travelled inside ThreadPresentation to the UI, twice over, through notification_window and through each notification's message, which was the backend record itself. Viewer captured once: - NotificationAssignmentRead carries its required OutcomeViewer. read, capture, missing_store and invalid_store take it, and the public missing store returns an empty read with that viewer; the private one still refuses. - capture builds the visible (recipient_lookup, wire_seq, message_id) keys through the viewer before any SQL. Claims are selected through the recipient/seq index by those keys, using TypedTable.requested_keys (factored out of select_keys, which now consumes it). HarnessDelivery.for_recipients gets the same keys. Rows keep the original wire_seq DESC, recipient order across batches. - delivery_window, project_delivery_window and for_sources read the captured viewer instead of taking a second one. for_deliveries skips invisible recipients before allocating an UnrecordedNotificationSource, and recorded_delivery_window allocates no placeholder activity for them. - NotificationAssignment.changed groups retained reads by their own viewer and re-reads each group's sources as that viewer. - SelectedParticipant captures its sent read as OwnerOutcomes of the owner, so the sender still sees every outcome. Public envelopes: - ThreadPresentation.with_sources and MessageNotification.for_sources attach source.envelope, the declared public Message, instead of the Delivered record. Acquired versus published: - ThreadPresentation keeps only the UI fields: status, binding, notifications and read_identity. - AcquiredThreadPresentation adds incoming_references, sent_references and notification_window. It is built by with_sources, supplies revision inputs and sent_summaries (replacing SentMessageSummary.recent), and published() derives the base from the declared fields. Pickling it raises a TypeError naming published(). - ThreadReadResult.published() defaults to itself and ThreadPresented projects its presentation. ThreadReading keeps the acquired results for its next change check and publishes only the changed results' projections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…ations (#3538) - test_outbound_transcript_source: - each recipient's own read decodes exactly one claim row (profiled AssignmentActivityEvidence._decode_row), equal to that recipient's rows and attempts in the operator read; the sender read equals the operator read; - invisible recipients allocate no UnrecordedNotificationSource, and the sender keeps all of them; - one claim transition refreshes only the retained reads whose viewer sees it: the recipient, sender and operator reads, not the other recipient's. - test_recipient_activity_index: - a viewer follows frozen lookups through rename and never into a rebirth; - a public root without a coordinator returns an empty read with the same viewer and projects only visible recipients; - a private root without one still refuses. - test_thread_read_results: - ThreadReading retains the AcquiredThreadPresentation and publishes its ThreadPresentation projection, with the same status, read_identity and notifications, whose messages are public envelopes; - the pickled observation carries no acquired evidence type, and sending the acquired result is refused; - after a second change, the UI's current and last-published rows reach no acquired evidence. - test_thread_presentation: absence publishes itself and summarises no sent message; its notification_window assertion read a field absence no longer has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…ch and null-key controls (#3538) presentation_fields() returned dict[str, object], so expanding it into ThreadPresentation(...) and AcquiredThreadPresentation(...) erased every field's type: 10 new arg-type errors at thread_presentation.py:287/311. It is deleted. - with_sources builds shown = replace(self, notifications=..., read_identity=...) once and constructs AcquiredThreadPresentation from {declared.name: getattr(shown, declared.name) for declared in fields(ThreadPresentation)} plus its three acquisition fields. - AcquiredThreadPresentation.published constructs ThreadPresentation from the same inline mapping over itself, the ThreadStatusDisplay.status_display shape. There is no cast, Any, ignore or copied field roster. - test_outbound_transcript_source::test_a_forced_key_batch_captures_the_same_evidence_through_the_recipient_index: - setup: a second real #team original in the private _root fixture gives at least 4 distinct claim keys, plus a repeated original; - batching: on the fixture's own coordination connection, SQLITE_LIMIT_VARIABLE_NUMBER=6 (restored in finally) forces several key batches, with deduplicated bindings equal to the unlimited batch; - result: the whole capture equals the unlimited read in rows, attempts, evidence, order and viewer, with no duplicated assignment; - index: the traced claim statements' EXPLAIN QUERY PLAN SEARCHes wake_claims through the unique recipient_lookup/wire_seq index found by PRAGMA, never a SCAN of it. - round2 test_declared_table_family: select_keys selects the weight=None child through a None key once, even when the key is repeated, and stops selecting it once the weight is 3.25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…n allows (#3538) The fixed limit of 6 refused NativeRuntimeTable's schema catalog read inside capture before any claim query ran (root: 1 passed, 1 failed in 3.30 s), so it proved nothing about claim batching. - The limit is the larger of 6 and each schema capture validates (privacy.coordinator_schema() and NativeRuntimeTable), counted as declared object names plus table names, the bindings of objects_of's catalog read. It must still be below the connection's own limit. - Enough real #team originals are published and accepted in the same private fixture for more than limit / 3 distinct claim keys. The count is derived from the recipient count and must fit NotificationWindow.window_limit, and the repeated original stays as the duplicate control. - The batching, equality, ordering, trace and EXPLAIN index assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…s its type (#3538) The GC stage made two new source-gate regressions: - NotificationAssignment.changed built an untyped `changed = []`. - AcquiredThreadPresentation.sent_summaries imported SentMessageSummary and MessageNotification.summary back from presentation inside the method, a hidden runtime cycle. Now: - `changed: list[NotificationAssignmentRead] = []`. - ThreadPresentation/AcquiredThreadPresentation.sent_summaries are deleted. sent_notifications() returns (reference, outcomes) pairs: () on the public presentation, the captured window's outcomes per sent reference on the acquired one. - SentMessageSummary.for_sources aggregates those pairs with the same-module MessageNotification.summary. ThreadCaller.listing calls it. - thread_presentation keeps only TYPE_CHECKING imports for these types; function-local imports 5 -> 4 (the four remaining predate this stage). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…s class header (#3538) OwnerLiveRead carried the only `apply(comms, caller) -> Result` forwarding to read_as; every other BusRead would have needed a copy just to repeat a return annotation for tests/test_tool_replies.py. The method moves to BusRead and the copy is deleted. The test's result_type now resolves a BusRead's result from the first concrete specialisation of a BusRead capability along its MRO (types.get_original_bases, the CliCommand.result_type pattern). Family headers still carrying their type parameter declare nothing; none or two different answers are refused. Other tools keep the apply-annotation path. Controls: RecordedNotificationRead inherits UiRead[NotificationWindow]; bare BusRead/UiRead/OwnerLiveRead refuse; an ordinary tool keeps its annotation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…3538) ReadCommanderUnit restated the commander-detail read as a UI-only request: the domain witnesses (commander, unit, orders) and the business body lived on the root-bearing envelope, so MCP had no declaration to serve. - CommanderUnitRead (order_tools.py) is UiRead[CommanderUnitDetail] + ReadTool + ToolRequest, declared_name="comms_commander_unit", with only the held commander/unit incarnations and the card's orders. read_as is the old body verbatim: both incarnations checked against one registry snapshot, the current published frame, the exact card, card.detail with the exact orders, every ToolRefusedError kept. - CommanderUnitDetail is a ToolResult directly; fields, codec schema and pickle bytes are identical to ed6ff38 (probe: pickle 4fc68ac7, schema 74234e98). - ReadOperation(ReadRequest) is the one generic UI envelope: root, request id, the operation as a bare UiRead decoded through its family, and the old ToolRefusedError/WorkBranchError refusals. deferred_reads stays (CoordinationReadUnavailable,). - ReadCommanderUnit is deleted. The real detail test compares the direct operation, the root-bound envelope and invoke_tool on the same card, and its stale-selection controls now vary the operation's witnesses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
The goal snapshot had two owner paths: the runtime command goal_snapshot (a GoalSnapshotResultRuntimeRequest whose change() did nothing) that UI ReadOwnerGoal sent by string, and a second copy of the same snapshot/turn projection inside every goal write. MCP had neither. - OwnerGoalRead (order_tools.py) is OwnerLiveRead[GoalSnapshotReply] + ThreadReadTool, declared_name="comms_owner_goal", with only the inherited thread selector. owned(ctx) is the old projection verbatim: goals.goal_snapshot(ctx.name), then turns.turn_state(ctx.session_id), then GoalSnapshotReply.from_snapshot(snapshot, UpdateMetadata.of(TurnChangedUpdate(turn))). - GoalSnapshotReply is a ToolResult directly (it was a DeclaredReply); pickle and wire bytes are unchanged (probe: 92ead7d5 / 648ee867). - GoalSnapshotResultRuntimeRequest.result awaits change(ctx) and then returns OwnerGoalRead().owned(ctx): the writes keep one copy of the read. - Deleted: GoalSnapshotRuntimeRequest and UI ReadOwnerGoal. Decoding action=goal_snapshot, with or without an operation, is now MalformedRecord. - Tests: the observer-death owner reads derive solely from members_with(OwnerLiveRead); the missing-owner busy test and the runtime family roundtrip use owner_read(comms_owner_goal); a canonical-owner test compares the owner's projection, MCP invoke and the UI envelope for an empty and a set goal, and refuses after the owner stops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…#3538) TranscriptRequest restated five domain arguments (thread, before, after, through, historical_source) on every UI envelope, and RefreshTranscript built a whole UI request just to reach the page read. MCP had no page tool. - TranscriptPageRead (order_tools.py) is UiRead[TranscriptSnapshotUpdate] + ThreadReadTool, declared_name="comms_transcript_page": the inherited thread selector plus before/after/through cursors and historical_source with their old defaults. capture_as(comms, reader) resolves reader.subject(thread) and calls comms.transcripts.capture_page_read once; read_as returns TranscriptSnapshotUpdate.from_read of it. No new renderer, window, cap or wrapper. - TranscriptSnapshotUpdate is AgentCommsUpdate + ToolResult. The update family stays first: same 23 update names, transcript_snapshot discriminator and page/identity/draft fields as ed6ff38. ACP encode/decode_updates unchanged. - TranscriptRequest holds only `operation: TranscriptPageRead`; page_read is operation.capture_as(comms, OperatorReader()). ReadTranscript, SubscribeTranscript (deferred_reads=(StaleRevision,)) and ReadTranscriptUpdate (previous publication state) stay UI lifecycle boundaries, unexposed. No forwarding properties. - RefreshTranscript keeps its original identity, bind_page_read and None for current content; on change it builds TranscriptPageRead from the identity's requested_name, cursors and historical_source and reads it through the UI's UnreadableHistory conversion. No UI request is constructed. - Callers migrated: transcript_updates replay/subscribe and the busy-history tests. New tests: one operation pages 45 records to the beginning and back, then an empty transcript; each page is identical through the operation, the UI envelope and MCP invoke. The attached-recorded witness test does the same with historical_source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…d-read declaration controls (#3538) comms_commander_unit can raise WorkBranchError when an order's original Git cannot be read. The UI envelope already lists it as a refusal, but the connector's tools/call tuple did not, so over MCP it would escape as a crash instead of a failed tool result. It joins the existing tuple; the real detail test breaks the original .git and asserts invoke_tool raises exactly WorkBranchError there. No other class is added and nothing broadens to ValueError. Declaration controls beside the notification envelope controls: - commander-unit and transcript-page operations encode without root, request_id, previous, owner or identity, decode through bare UiRead, and their ReadOperation envelope round-trips codec, pickle, hash, set and dict; - the owner goal decodes through bare OwnerLiveRead and its owner envelope pickles; - root, request id, unit birth, orders, cursor and archive each make a distinct request; - one operation under two roots keeps the foreign-root refusal; - each public read is cataloged once in ToolRequest and NativeRequestTools, and its result, read off its class header, is exactly CommanderUnitDetail, GoalSnapshotReply or TranscriptSnapshotUpdate. UiRead tools are exactly {commander_unit, transcript_page}, OwnerLiveRead tools exactly {input_delivery, owner_goal}, and no UI lifecycle envelope is a tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…guments ToolsCallMessage.result caught CoordinationError but not MalformedRecord, so a newline-complete corrupt native entry escaped tools/call as an HTTP 500 with no id. MalformedRecord joins the existing failure tuple; ToolsCallResult.failed keeps the id and the nominal error. The shared-read controls sent FieldCodec.encode(operation) into invoke_tool, which carries the "tool" selector ToolRequest.invoke refuses; they now send FieldCodec.encode_fields(operation), the declared fields without the tag. Real HTTP regressions in test_connector: corrupt saved session -> failed call equal to the domain's MalformedRecord, then comms_channels still serves; stale cursor after rebinding the session -> failed call equal to TranscriptChanged. The deleted goal_snapshot command gets its own UnknownFamilyMember test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
The producer took an unannotated read and returned Any, so the typed TranscriptPageRead.read_as (order_tools.py:584) failed the hosted type gate through it. from_read now takes TranscriptRead and returns Self; the body, fields and wire bytes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…elds extracted into a held class move CleanChangedLines already decides which occurrences a change writes, but Comparison.failed also failed its raw per-file growth through Measure.blocks, so a moved occurrence that introduced nothing still failed on its destination's count. CleanChangedLines.blocks is now False; every introduced occurrence still fails through Comparison.failed, including one hidden by an unrelated deletion. Measure.grown is the growth predicate blocks and the site-locating step share, so raw counts, deltas and found sites are unchanged. FieldExtraction proves one more transfer from the before/head AST: an old class present at both revisions newly holds, in one typed field, one class absent from the base that no other old class holds, and each field it lost is declared once there with the same name, type (typing.Annotated metadata aside) and default. Only occurrences inside that field declaration are credited, each spending one departure. The holder is resolved through DecoratorBinding (now also DecoratorBinding.named / module_of), refused when the name is bound twice or the module star-imports. TranscriptRequest's four cursor/archive fields moved into TranscriptPageRead are that case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
|
debt-tools at 6fc1a17, every finding: |
…fails DecoratorBinding.bindings(tree, scope) reads ScopeNames' lexical scopes as the one binding owner, and declarations() proves a name only when its scope binds it exactly once, by an unconditional import, class, function or annotated assignment in that body, with no star import. named()/of() (boundary_decoder, dataclasses.field), held-class exports, field uniqueness, source disappearance and the typing.Annotated strip all read it, so a nested-only import, a rebound Annotated or export, a field reassigned after its annotation, or a same-name binding hidden in unpacking no longer earns transfer credit. DeclaredClass.bound_once and its whole-tree census are gone. Measure.missing names counts whose required evidence a comparison lacks; CleanChangedLines.missing is each positive head count with no introduced report. Comparison fails on any, reports them as "missing" and prints them; an explicit empty introduced list stays the measured zero-new-sites answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…nd to The field proof compared printed annotations, so the same spelling T imported from alpha.Cursor and beta.Cursor counted as one type. DecoratorBinding.identity rewrites every name and attribute reference in a field's type and default to its owner from the existing lexical declarations: an import's source module and export (relative imports and aliases via module_of), a declared class's own module, a builtin only when neither the module nor the class scope binds the name. Anything else - a class-scope binding, type variable, rebinding, star import or unresolved name - is UnprovedIdentity, equal to nothing; the proof and its key are one NominalProof result, and DeclaredClass.proved is gone. Occurrences pair node for node across the proved declarations, so an alias of the same owner carries its departure while a changed owner does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…cope DecoratorBinding.named takes the scope it is read from (module by default) and drops a spelling whose root that scope rebinds. DeclaredClass.annotated and DeclaredClass.holding pass the class's own scope, so a class body that rebinds Annotated or the held class's name proves neither the stripped inner type nor the held-class edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
…e itself The hosted tools gate counted the measurement repair's own AST case switches (DecoratorBinding.bound and the switch inside declarations: TypeSwitch +2, TypeSwitchArms +20) and its reconstructed coordinates (TypeErrors). ScopeNames is the one foreign-AST visitor: its visit_<kind> hooks record each node's bound names and the declaring nodes during the walk that assigns scopes, and DecoratorBinding.bindings/declarations group those facts; a statement owns a binding when it is that node or its direct child. SitePlace and FieldExtraction.place are gone: credit is a Counter keyed by the destination node the cached tree already owns. The edge list is typed and introduced is a classmethod reading cls.members_with(cls). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA
notification_assignment keeps this branch's requested-key batching through the recipient/seq index and takes main's SQL-derived queued presence (#3579) in place of transporting the current execution id; rows are already sorted by (-wire_seq, recipient) after the batches, so main's ORDER BY adds nothing. tool_caller and test_command_families keep the imports each merged file uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
Takes #3538's resolution of main; tool_caller drops the unused CallingThreadUnavailable import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnH6dR85kyFKcc2tQAJLhK
|
Batched: this PR's head is inside carrier #3566 (batch/integ-20261029-queue-core). Its own auto-merge is off and its own runs are cancelled. It lands when the carrier lands, so CI runs once on the batch, not per member. |
Closes #3537.
One declaration per bus read, so a UI read, an MCP tool and an owner-socket request can never restate the same operation or answer it differently. Owner: claude-nra; shape decided by codex-greentext at the review checkpoint (map: bus add32451fa15).
Summary in numbers (source phases built; root qualification, paired integration and installed GC/frame gates open)
cfad6769e, notificationsc57a7476b, ratchet3d3a357bb, goal history732605601, input delivery36611e2b6(+149/−62, 8 files), testsc0821f571(+65/−20, 2 files), native owner check and held identitya4702b1f7, lifetime controls08263a4c0, transport relocation5c262628f(+89/−97, 6 files), fixture corrections9bb8b2e38(tests only, +111/−128, 6 files), held ordering8d21db48c, notification reads21b07552a(+257/−96, 13 files), sparse activity and producer types7bcb1ca43(3 files, +77/−14), recipient-fixture correction5bcc78d9b(tests only, +8/−6), viewer-scoped evidence and published-only presentations7b5c78a0e+2e8db8229(14 files, +449/−143), typed construction and forced-batch/null-key controls5126bc6c6+ef1ae76d4, typed accumulator and summary ownered6ff38d2, final read surfaceffe8bd27c780b9957e1c8b7e8ebd92c06392d5b7376f8, plus main merges (latest: main3adf7912eintoed6ff38d2as7fff19b7b, no conflicts). Overallorigin/main...HEAD: 52 files, +1906/−628.OwnerReading.require_native_owner, called fromOwnerReplyConnection.held; the per-class copies are deleted). The goal read for an external owner no longer waits forever for a socket that never comes.declared_namereads: 1 → 0.Anyresults on the runtime envelope: 1 → 0.OwnerConnection.__init__recaptured the registry identity afterheldchecked it).InputDeliveryReadto the live owner.CommsInputDeliveryTool,ReadDeliveryOverview,InputDispositionsRuntimeRequest→InputDeliveryRead).OwnerRead.connectand the delivery path →OwnerReplyConnection.held/answer).subject/own_thread/refuse_threadcopies: 3 → 1 (ToolCaller,NoThreadCaller→BusReader).ReadNotifications.viewer).Mechanism:
BusRead[Result]BusReadis an abstract capability inside the existingToolDeclarationfamily, with noDeclaredFamilyroot of its own. Members own their arguments and one read,read_as(comms, reader: BusReader) -> Result. The boundary supplies the reader; it is never an argument.BusReader(ABC)declares:outcome_viewer(comms);subject(named), which isnamed or own_thread();own_thread(), which by default refuses;refuse_thread(), which raisesCallingThreadUnavailable("This tool requires a thread caller."). The text and type are unchanged.OperatorReader(OperatorOutcomes, BusReader): the owner's UI, which sees everything.ToolCaller(BusReader):ThreadCallerreads its own outcomes and thread.NoThreadCallerandDirectorCallerread as operator.UiRead[Result](BusRead[Result]): derives the UI'sread(comms). The envelope stays onReadRequest.BusRead.members_with(UiRead)= (ReadNotifications, ReadRecordedNotifications). The UI selector is nowtool(theToolDeclarationdiscriminator; it waskind). No persisted or wire producer exists, because observation requests travel by pickle; pickle and hash equality were checked.owner_connection.py, besideOwnerConnection,RuntimeRequestandResultRuntimeRequest:OwnerReading[Result](ABC)is one exchange with one held owner incarnation.read(owner: OwnerConnection)is abstract.require_native_owner(owner)isowner.execution.require_native();ThreadExecutionowns native versus external, with no isinstance or roster. An external bridge (live PID, noRuntimeServersocket) is refused before_connect_currentstarts its unsent missing-socket wait.RuntimeRequest.require_owner(entry)is a different authority and is untouched.OwnerReplyConnection(OwnerConnection):heldrequires the selected thread, refusesthread.incarnation != ownerwithOwnerIdentityChangedError, callsreading.require_native_owner(thread), then derives the socket fromrequire_process, constructs the connection and checksrequire_identity(owner);answerreads once and closes infinally.REPLY_TIMEOUTis 3 s. It derivesOwnerConnectiondirectly, becauseowner_readandgoal_snapshotuse the baseproxy_payload; onlyPromptRuntimeRequestneeds the ACP controller token, so wire payloads are byte-identical.OwnerLiveRead[Result: ToolResult = ToolResult](BusRead[Result])is the domain capability. It does no transport.owned(ctx)anddecode_reply(raw)are its only answer and decoder. MCPapply/read_asresolvescaller.subject(thread), buildsOwnerReadRuntimeRequest(thread=owner.name, operation=self), sends it withasyncio.run(OwnerReplyConnection.answer(...))at the synchronous tool boundary (connectorto_thread, sync CLI, nativeCoordinationCall), and decodes. Operation fields stay annotated with the bareOwnerLiveReadfamily class.OwnerReadRuntimeRequest(OwnerReading[object], ResultRuntimeRequest)inbus_read.py, next to the capability it carries: actionowner_read, fieldoperation: OwnerLiveRead.read(owner)is the single socket exchange,owner.request(self.declared_name, operation=self.operation).result(ctx) -> ToolResultrunsoperation.owned(ctx)inside the owner. Runtime membership is still derived from the declaration: importing onlyagent_comms.runtimeresolvesowner_readtobus_read.OwnerRead(OwnerReading[object], AwaitedRead)asks throughOwnerReplyConnection.answer.ReadOwnerGoal.readandReadOwnerOperation.readtakeOwnerConnection.ReadOwnerOperationbuilds the envelope withowner.session_idand decodes through its operation.Before / After (input delivery)
Deleted:
CommsInputDeliveryTool,ReadDeliveryOverview,InputDispositionsRuntimeRequest, theinput_dispositionsrequest action,OwnerRead.connect(), and theown_thread/subject/refuse_threadcopies onToolCallerandNoThreadCaller. Census at08263a4c0and Toad3772cf0, src/tests/tools: the three deleted names and the oldinput_dispositionssocket action have 0 callers in Core and in Toad. The same search finds the anchorInputDeliveryReadin 4 Core files and 1 Toad file, so the zero isn't a broken search. The remaininginput_dispositionshits are the SQLite filename (input_disposition.py:247and five test assertions), not socket callers. Toad usedReadDeliveryOverviewonly atsrc/toad/acp/agent_controller.py:26,434, which OpenHCSDev/toad#1302 migrates.Wire (fresh-process declaration check):
{"tool":"comms_input_delivery","thread":"","include_history":true};{"action":"owner_read","thread":"worker","operation":{"tool":"comms_input_delivery","thread":"worker","include_history":false}};OwnerLiveRead,ToolRequest,ToolDeclarationandRuntimeRequest;comms_goal_history) asoperationrefuses with "is not a member of OwnerLiveRead".InputDeliveryReadis in the ToolRequest and native catalogs by capability, and noUiReadis in them. The schema keepsthreadwith its description.Intentional public change: none to the MCP default.
InputDeliveryRead.include_historydefaults to True, ascomms_input_deliverydid before. Toad'sAgentController.input_deliverykeeps its own default, False, and always passes it explicitly. For equal explicit booleans, MCP and the UI return the same owner scope, current inputs and counts; onlyhistorical_inputschanges with the boolean. Behaviour change: a delivery read for a thread with no live native owner now refuses with the owner error (RelationViolationError,OwnerIdentityChangedError,StaleRevision) instead of returning an UNOBSERVED-looking view. The connector encodesRefused,ConnectionErrorandTimeoutErroras a failed tool call instead of dropping the HTTP call.tests/verify_connector_tools.py:244still callscomms_input_deliveryforworker, which is declaredStoppedThreadStatuswith no process, so it now gets that clean refusal.Held identity (commit a4702b1)
Before:
OwnerReplyConnection.heldcomparedregistry.require(name).incarnationwith the supplied witness. ThenOwnerConnection.__init__read the registry again into_identity, and nothing compared that second read with the witness. A replacement incarnation registered between the two reads was adopted silently.After (
8d21db48c):heldmakes two identity comparisons, one for each registry read:OwnerIdentityChangedError, not as "no owner process".OwnerConnection.require_identity(owner)compares the connection's captured_identitywith the witness, which closes the gap between the two reads.a4702b1f7had dropped the first comparison, and root's rerun caught the wrong refusal order._identityis still the connection's only identity field: there's no expected-owner cache and no overwrite. Later rechecks in_owner_entry(root stat, name, incarnation) are unchanged.Also found, not changed: two unrelated classes are named
BusReader(bus.py:146,bus_read.py:22).Goal history: three result classes → one (commit 7326056)
Before:
RecordedGoalHistory(DeclaredReply)heldhistoryandfailures.GoalHistory(ToolResult, RecordedGoalHistory)(tools.py) andGoalHistoryReply(RecordedGoalHistory)(runtime_requests.py) were empty subclasses, andGoalManagement.recorded_history(reply: type[HistoryReply], name, scope)built whichever one the caller passed in. Two boundaries, two empty names, one record, plus a TypeVar to keep them apart.After:
RecordedGoalHistory(ToolResult)(ToolResultis already aDeclaredReplyand adds no fields).recorded_history(name, scope) -> RecordedGoalHistory.CommsGoalHistoryTool.applyandGoalHistoryRuntimeRequest.resultboth return it. The tool name, declared runtime name, subject resolution, scope decoding and worker-thread execution are unchanged.Deleted:
GoalHistory,GoalHistoryReply, thereplyparameter, theHistoryReplyTypeVar and its import (+12/−35 across 5 files).Encoding is unchanged. One populated history/failure payload decodes and re-encodes byte-identically through all three old classes before, and through
RecordedGoalHistoryafter.empty()is{"failures": [], "history": []}on both sides. This is a source serialization check, not an application run.Checks (run by root on Core 7326056 + Toad caf4953):
comms_goal_history: identical populated and empty-scope history, 1 passed in 19.35 s.5fa29f3.7326056for this step only. The overhaul is not approved for merge; the remaining plan below is open.Census: 0 hits for the deleted names in Core src/tests/tools and Toad main src/tests/tools; the same search finds 10 Core and 18 Toad
RecordedGoalHistorysites.tests/test_commander_latency.py:34 recorded_historyis an unrelated helper that only shares the name.Ratchet repair: type counts are measured each comparison (commit 3d3a357)
The hosted debt-src failure on this PR wasn't a source difference. The ratchet stored mypy counts in
ContentCacheunder a key of committed content and checker versions, but mypy also reads the installed packages, and no key names them.CI evidence (Codex read it at the CI boundary):
debt-ratchet-src-71453f18249492f07957174bb0a13be6a90b7e9b.uv sync --locked --all-extras, with Pydantic 2.14.1.Local evidence (Codex's fresh comparison against Toad 2bbca26a7, using the ratchet's own
StackClient.importsselection): 287 client files on Core 71453f1 and on this PR's b97957a give 533 diagnostic lines on both sides, byte-identical. No Toad patch is justified.Source change (
tools/debt_ratchet.py):CheckedRevision.findings(inherited byTypeErrors)--cache-dirLintFindings.findingsUnformatted.findingsClientTypeErrors.findingssrctreecheckeddirectly; the commit/tree lookups computed only for the key are deletedNo new cache policy, dependency fingerprint, package roster or wrapper was added.
ClientTypeErrors.clientsdiscovery caching, import selection, checker command and counting are unchanged.Regression (
tests/guards/test_debt_ratchet.py):test_type_counts_follow_the_installed_dependency_not_a_stored_count, parameterized overTypeErrorsandClientTypeErrors. A PEP 561 packageextdepoutside every committed root is switched between an accepting (VALUE: int) and a rejecting (VALUE: str)sys.pathenvironment. The committed source, client checkout and configuration don't change, and the count-cache directory stays the same. The real findings path runs twice, and the count must go 0 → 1. Nothing of ours is mocked, and no mypy output is faked.Checks (run by root on 3d3a357):
Proof limits:
Input delivery tests (commit c0821f5)
tests/test_observer_death.py: unavailable and stale owner reads are parametrized fromReadOwnerGoalplusReadOwnerOperation(op())for eachopinBusRead.members_with(OwnerLiveRead)(idsgoal,comms_input_delivery). The hand-listedOwnerRead.__subclasses__()roster is gone. The refusal and observer-alive assertions are kept.tests/test_input_delivery_history.py: the CLI test asserting that a declared owner with no process answers delivery state is deleted, because that answer was the defect. The replacement usescanonical_delivery_owner, a real owner socket. It runs the owner, holds a turn, queues one prompt and records an off-queue notice (bus:91). Then:delivery_scopeisOwnerQueueInputDeliveryScope;bus:91as current;include_historyFalse and True, MCPInputDeliveryRead.invokeequalsFieldCodec.encodeof the owner's projection, and the UIReadOwnerOperation(...).answer(comms).valueequals it;bus:91is historical, and onlyhistorical_inputsdiffers between the two settings;NoProcess,StoppedThreadStatus) refuses the MCP read.Lifetime controls (commit 08263a4)
tests/owner_socket_fixture.py, registered inpytest_plugins, is the one fixture owner:runtime_processes, moved fromtest_runtime.py, now yields the spawned children so a test can stop one throughstop_sync. A second stop is a no-op:signal_groupskips gone members,Popen.waitreturns the saved code, andNO_CHILD_REGISTRATION.finisheddoes nothing.declare_ownerregisters a native or external owner.silent_socketis the real Unix-socket peer fromtest_dispatched_request_without_a_reply_remains_unknown_and_is_not_replayed: it receives one request and closes without replying. That test is refactored onto it.Nothing of ours is patched, and no reply is fabricated:
include_historyFalse and TrueOwnerReplyLostError; exactly one request, decoding toOwnerReadRuntimeRequest(thread, InputDeliveryRead(thread, include_history)); durable inputs unchanged. The peer runs no domain body.OwnerIdentityChangedErrorfrom the root-stat comparison; 0 requests; root restoredunregister,begin_delete,remove,declareheld()with the old witness both raiseOwnerIdentityChangedError; old and new incarnations recorded and different; 0 requestsruntime_processeschild, thenstop_syncConnectionErrorbefore any request; 0 requests; inputs unchangedExternalThreadExecutionwith a live child, no socketRelationViolationError, inputs unchanged. Observer path:ReadOwnerGoaland the delivery envelope (fromOwnerLiveReadmembership) both raiseRelationViolationError, and the observer stays aliveThe canonical native-owner journey in
test_input_delivery_history.pystays the domain proof (MCP == UI == the owner's projection). The silent socket peer only proves dispatch and then reply loss.tests/test_runtime.py,tests/test_observer_death.py,tests/test_input_delivery_history.py,tests/test_command_families.py, and the stopped-worker case intests/verify_connector_tools.py. That file is unchanged from main (last commit11ad0e348), and itsworker(StoppedThreadStatus, no process) now gets a refusal atrequire_processinstead of a live overview.Root qualification and fixture corrections (commit 9bb8b2e)
Root's selection on
5c262628fwas not a clean suite: 60 cases ran in 3.99 s, 49 passed and 11 failed.remove()afterbegin_deletecorrectly refuses Deleting status.with,readable()/results()/fileno(), sync close,NotificationWindow == {}).Root's rerun of these corrections on
9bb8b2e38: 22 cases in 70.37 s, 20 passed and 2 failed, bothother_root=Falsestale-identity cases (goal and input delivery) refused as no-process.8d21db48crestores the identity comparison before process lookup. Toad0be58d8(Toad's 5 checks, the new native acquisition test among them) passed in 67.48 s. The earlier 49 passing cases and these results are evidence for their exact heads, not a whole-suite pass.8d21db48calso boundsObservedQueue.untilby one 30 s timeout for the whole match, where before every unrelated result restarted it, and restores one 30 s bound around the deleted-thread wait for both observations.Root's final targeted selection on
8d21db48c: 19 passed in 36.68 s:held's ordering (unavailable owner, other root, external owner, same-name recreation, process death, reply loss, root replacement);Head verdict OK on
8d21db48cfor the owner-live input-delivery stage only. The PR stays a draft and isn't armed.Evidence, by exact head (separate selections, not one suite count):
5c262628f9bb8b2e388d21db48c0be58d8test_runtime.pysame-name control: deletion completes withedit.document.remove_originals((old,))insideregistry.store.editing(). That's the path production uses (thread_management.py:537), becauseRegistrationdoesn't forwardremove_originals. The captured connection, the old witness, the zero-dispatch peer and bothOwnerIdentityChangedErrorrefusals are unchanged. No production deletion change.test_observer_death.py: every observer isasync with, and empty-window checks compare.outcomes.waitpid(WUNTRACED)), the next read shown pending, then SIGKILL. TheObserverDiedcarries the child pid,-SIGKILLand the signal text, andStopSidebarand a later read raise the same object. Afinallykills a stopped child and cancels a pending task.readable()is deleted.test_busy_history_loading.py: threeasync withsites, plus the close-once test'sasync with processand its awaited repeated close, and the.outcomescomparisons. TheConnection.closeendpoint observation is unchanged.test_deleted_thread_observation.pyandtest_unreadable_history_observation.py(the tworeadable()importers) start the receiver once withstart_receivingand wait ontests/observed_queue.py. That's a test-local queue of delivered results that raises a receiver failure as its original exception; there's no manual pumping.Observer-caller census (AST over src/tests/tools; it tracks names bound from
ObservationProcess(...)/.awaited(...)and ignores multiprocessing handles):async withconstructions and 1async withon a bound observer.with, 0readableimports, 0results()/fileno()..close()calls on observers, both awaited (test_busy_history_loading.py:118andtest_transcript_observation_lifetime.py:108).Codex's check (
rg 'test_observer_death import readable|process\.(results|fileno)\('over the three files) gives 0.Found and left alone:
test_transcript_observation_lifetime.py's bare-process pending-read test never starts a receiver. Its pending read settles only becauserequest()hits the broken pipe andended()abandons the read. That's a separate pre-existing contract.comms_root.py:392, already usesstart_receiving(:244) and closes as a task (:260).Root rerun nodes:
tests/test_runtime.py::test_a_same_name_recreation_refuses_the_held_read_and_the_old_witnesstests/test_observer_death.py, all six functions with their parametrizations:test_a_killed_observer_is_reported_once_with_its_signal_and_fails_awaited_reads,test_an_invalid_notification_reference_does_not_kill_the_observer,test_unavailable_owner_reads_preserve_the_observer,test_stale_owner_reads_preserve_the_observer,test_external_owner_reads_refuse_before_waiting_for_a_socket,test_an_observer_that_failed_is_reported_with_its_own_traceback_and_exit_statustests/test_busy_history_loading.py:test_busy_source_keeps_original_waiter_and_other_reads_move,test_cancelled_busy_read_is_removed_without_resubmission,test_awaited_scope_preserves_exit_and_closes_once,test_awaited_notifications_then_immediate_close_retires_successfullytests/test_deleted_thread_observation.py::test_observer_process_survives_deleted_interest_and_keeps_servingtests/test_unreadable_history_observation.py::test_unknown_saved_member_does_not_kill_the_real_observation_processtests/native/test_native_goal_owner.py::test_input_delivery_is_read_from_the_live_native_owner_queue(new; not yet run by root)Notification reads: root-free operations, one UI envelope (commit 21b0755)
Before:
ReadNotifications(UiRead[NotificationWindow], ReadRequest)held the UI'srootandrequest_idbeside itsreferences.ReadRecordedNotificationssubclassed it to addsource.CommsMessageOutcomesTool.applybuilt that UI envelope withstr(comms.root)purely to callread_as. MCP was carrying a UI root it never validated, to reach a read that doesn't need one.After:
notification_reads.NotificationRead(UiRead[NotificationWindow])changed_since(comms, revision, previous)LiveNotificationRead(references)read_aswith the reader'soutcome_viewerviamessage_notifications_for_references; stale whenprevious.live_changed(activity and evidence)RecordedNotificationRead(references, source)read_asthroughHistorySource.require(sources, source).notification_references; stale only whenrevision.historychangesnotification_read.NotificationsRead/UnreadableNotificationsReadCapturedNotificationWindowui_model.observation.ReadNotifications(ReadRequest)root,request_id,operation: NotificationRead;readdelegates to the operation; no longer aUiReadnotification_reads.py, separate fromnotification_read.py, becausepresentationimportsnotification_readandbus_readimportspresentation.ReadRecordedNotifications, with no alias.NotificationReadingasksrequest.operation.changed_since. Read states stay keyed by the whole request, so a different root, request id, references or archive is a different key.MCP is unchanged in contract.
comms_message_outcomescallsLiveNotificationRead((reference,)).read_as(comms, caller). Its positive-limit refusal, exact reference lookup,outcome_viewer.seesrefusal before rows, recipient-cursor paging and remaining count all stay. Its public schema is byte-identical to8d21db48c(1317 bytes of sorted JSON). The 120-reference window chunk stays a per-acquisition chunk, not a cap, and MCP'slimit(default 100) is still a page size.Exposure follows capability.
BusRead.members_with(NotificationRead)is (Live, Recorded). Neither is in theToolRequestorNativeRequestToolscatalogs, andCommsMessageOutcomesToolis in both. The envelope decodesoperationas the bareNotificationReadfamily and refuses an input-delivery operation ("is not a member of NotificationRead").Toad (#1302
a9a330b):HistoryNotifications.notification_request(a class used as a root+references constructor) becomesnotification_operation, a root-free operation constructor:LiveNotificationReadby default,partial(RecordedNotificationRead, source=source)inRecordedContents.notification_readbinds theProjectPathOwnerroot:ReadNotifications(root, operation(refs)).comms_rootstill keys views and outcomes by the full request.Controls added (not run here):
test_toolsare unchanged.RecordedNotificationReadreturns the archive's "Sent" while the live read is "Consumed", and a detached key refuses withHistoricalSourceChanged.Fresh-process checks (executed):
comms_message_outcomesschema is byte-equal to 8d.Found, not changed:
ObserveNotificationsmulti-request FieldCodec encoding fails, because the frozenset serializer sorts unorderable requests. It's the same on 8d; the production path is pickle.tests/textual/test_observation_shutdown.pycallsprocess.fileno()andloop.remove_reader(process.fileno()).ObservationProcesshas nofileno()on Core main or on this branch, so that test is already broken on main, independently of this phase. Only its request construction changed here.Root selection still to run:
test_notification_read_state,test_message_notifications::test_a_live_batch_beyond_one_window_chunk_keeps_every_message,test_historical_notifications::test_live_cli_ack_does_not_become_archived_ack,test_toolsmessage-outcomes cases,test_thread_read_resultsattribution, and the observer fixtures touched by the envelope change.test_historical_notification_source, notification paint / hidden late result / markdown archived-empty,test_observation_shutdown.Notification correction: activity holds only projected participants (commit 7bcb1ca)
Why: hosted debt-src on
21b07552acounted NoneAbsence +1 atnotification_read.py:40and TypeErrors +1 atnotification_reads.py:57. The second is the return of an unannotatedHistorySource.notification_referencesreachingRecordedNotificationRead.read_as.Activity:
observed_activity(registry_document.py:1139) projects visible executors only. A registered human or an archived row is omitted, and a deleted name answersDeletedThread. The window'sactivity_names()already names every watched participant.NoneinNotificationsRead.activitywas therefore a second spelling of "absent". It is gone:activity: Mapping[str, ObservedActivity | DeletedThread]keeps only names the projection returned.live_changedre-projects the window's fullactivity_names()when ActivityView moved and compares the sparse maps. omitted→observed, observed→omitted and observed→DeletedThread still invalidate. With no ActivityView change it projects nothing.Types:
HistorySource.notification_references(sources, references: Sequence[MessageReference], viewer) -> NotificationWindow, the typeMessageNotification.read_referencesalready returns. The body is unchanged.Tests:
{absent-participant: None}fixture is replaced by a real#teamread. Its participants are an agent, a human, an archived agent and a deleted agent.Root run on
7bcb1ca43+ Toad54db5d1(seven heads): 31 collected, 29 passed and 2 failed, in 16.64 s. Both failures were in my fixture: it expected the agent sender and the human inactivity_names(), but the window names only native recipients (hereworker,archived,gone).5bcc78d9bcorrects those two assertions and changes nothing in production.Root rerun on
5bcc78d9b: the two corrected nodes, 2 passed in 1.64 s (exit 0). The matched Toad54db5d1shutdown, paint and archive selection: 7 passed in 76.19 s (exit 0). Source-stage verdict: OK on5bcc78d9b, for the notification stage only. The PR stays a draft and unarmed. Still open: the paired-client CI mismatch and the remaining plan phases.Remaining client counts: hosted ClientTypeErrors +1 on Toad-main
acp/agent_controller.py(importsReadDeliveryOverview, line 26),screens/historical_sessions.pyandtests/textual/test_historical_notification_source.py(importReadRecordedNotifications). Toad #1302 changes all three, so they clear only when the pair lands. Not run here.Notification evidence stays with its reader (commits 7b5c78a, 2e8db82)
Why: on the saved six-tab desktop, two windows of five incoming broadcasts (1767/1768 frozen recipients each) allocated 17,681 claims to show 15 rows.
NotificationAssignment.captureselected claims bywire_seqalone, and only after decoding did it letOwnerOutcomesdrop other recipients. The same acquired graph then reached the UI twice:ThreadPresentation.notification_window;message, which was theDeliveredrecord itself, carrying the frozen audience and decisions.Codex's offline replay of the saved values: 8.10 MB → 136,914 B and 375,706 → 2,638 graph nodes, with consumer parity. That is a payload result, not a frame-time result.
Viewer captured once:
NotificationAssignmentReadviewer: OutcomeViewer;empty()deleted (no callers)NotificationAssignment.capture/read/missing_store/invalid_storecapturebuilds visible(recipient_lookup, wire_seq, message_id)keys before SQL; claims are joined through the recipient/seq index, andHarnessDelivery.for_recipientsgets the same keysTypedTable.requested_keys→RequestedKeys(values, join, bindings)select_keys, which consumes it; one batched, null-safe, declaration-encoded key producerdelivery_window / project_delivery_window / for_sourcesassignment_read.viewer; no second viewer parameterfor_deliveries(…, viewer)UnrecordedNotificationSource;recorded_delivery_windowallocates no placeholder activity for itNotificationAssignment.changedSelectedParticipant.prepareOwnerOutcomesof the owner (sender sees all);capture_sent_outcomeslost its now-unused owner parameterRegistryPrivacy.missing_notification_assignments(sources, viewer)Public envelopes:
ThreadPresentation.with_sourcesandMessageNotification.for_sourcesattachsource.envelope, notsource.message. Census: 0 sites still attach a record as a notification's message.Acquired versus published:
ThreadPresentationis the UI value: status, binding, notifications and read_identity.AcquiredThreadPresentation(ThreadPresentation)addsincoming_references,sent_referencesandnotification_window.with_sources, supplies revision inputs, and ownssent_summaries()(SentMessageSummary.recentis deleted).published()derives the base from its declared fields.__reduce_ex__raisesTypeError, so pickling it is refused with a message namingpublished().ThreadReadResult.published()is self by default, andThreadPresentedprojects its presentation.ThreadReadingkeeps acquired results for its next change check and publishes only the changed results' projections.read_thread_presentationreturns.published()before leaving its worker (P325: return owner subscription busy refusals without waiting #1302cdd6e15), soConversationandObservedSourcePublicationnever hold the acquired value.Executed here (fresh processes):
p872-gc-probe.pyin 4 import orders, all identical:ThreadPresented), while published round-trips;recipient_lookup/wire_seq/message_id, with duplicate keys bound once.rg 'replace\(notification, message=source\.message\)' srcgives 0. Of 66 non-literal reflection sites, 7 sit in presentation, notification, observation or status files; these are listed inp872-gc-census.jsonas unknown dynamic limits.Not run (root):
test_outbound_transcript_source: per-viewer decode count and rows, no placeholder for an invisible recipient, refresh grouped by viewer;test_recipient_activity_index: rename and rebirth, missing public and private stores;test_thread_read_results: retained acquired versus published, pickled types, UI current and last-published rows;test_thread_presentation.test_thread_read_results::test_assignment_row_changes_attribute_only_their_view_without_projection;test_historical_notificationsrecorded-response cases (345, 424);test_toolsthreads and outcomes;test_recipient_activity_index::test_viewer_selects_the_outcomes_of_a_delivery_window;Root run on
2e8db8229+ Toadcdd6e15(selections): 60 Core cases passed in 17.01 s, and 9 Toad consumer cases passed in 87.56 s. Neither is the whole suite or a GC/frame result.Correction
5126bc6c6:thread_presentation.py:287/311.presentation_fields() -> dict[str, object]erased each field's type before the typed constructor. It is deleted.with_sourcesbuildsshown = replace(self, …)once and constructs the acquired value from the inline{declared.name: getattr(shown, declared.name) for declared in fields(ThreadPresentation)}plus its three acquisition fields.published()uses the same inline mapping over itself, thestatus_displayshape. No cast, Any, ignore or field roster.test_a_forced_key_batch_captures_the_same_evidence_through_the_recipient_index):#teamoriginal and a repeated original in the private_rootfixture;SQLITE_LIMIT_VARIABLE_NUMBER=6on the fixture's own connection forces several batches;captureequals the unlimited read;EXPLAIN QUERY PLANSEARCHeswake_claimsthrough the PRAGMA-derived recipient/seq unique index, never a SCAN.round2/test_declared_table_familychecks thatselect_keysselects theweight=Nonechild once through a repeated None key, and stops after the update.5126bc6c6:no-untyped-defdiagnostics: the 10 new arg-type errors are gone;NativeRuntimeTable's schema catalog read before any claim query ran;Correction
ef1ae76d4(test only):PrivateCoordinatorSchema163,NativeRuntimeTable21. It must be below the connection's own limit.limit // (3 × recipients) + 1real#teamoriginals (28 with 2 recipients; 29 sources including the repeat, within the window limit of 120), so the keys still force several batches while schema validation runs normally.Still open: this stage removes the captured payload only. Root still has to remeasure the whole-process GC tail; the earlier capture covered 17,681 of 38,895 assignments, and
Conversationand publication aliases were not captured. The 16 ms frame target, the p99.9 tail, 29-agent coverage and the remaining shared-read phases stay open.Final read surface: commander detail, owner goal, transcript page (commits
ffe8bd27c…d5b7376f8)Three reads were still declared only as UI requests. Their domain arguments were mixed into root-bearing envelopes, and MCP had nothing to serve.
Each one is now one root-free operation in
order_tools.py. MCP, native and the UI derive it.comms_commander_unitCommanderUnitReadUiRead+ReadTool+ToolRequestcommander,unit(held incarnations),ordersCommanderUnitDetailcomms_owner_goalOwnerGoalReadOwnerLiveRead+ThreadReadToolthreadselector onlyGoalSnapshotReplycomms_transcript_pageTranscriptPageReadUiRead+ThreadReadToolthread,before,after,through,historical_sourceTranscriptSnapshotUpdateBefore / After
read_aschecks both held incarnations against one registry snapshot, takesCommanderFrame.current, selects the exact card and callscard.detailwith the exact orders. EveryToolRefusedErrorand the post-read checks are unchanged.ReadOperationis the one generic UI envelope: it decodes a bareUiReadthrough its family and keepsToolRefusedError/WorkBranchErroras refusals, withdeferred_readsstill(CoordinationReadUnavailable,).ReadNotificationskeeps its own typedNotificationReadenvelope.owned(ctx)runsgoals.goal_snapshot(ctx.name), thenturns.turn_state(ctx.session_id), thenGoalSnapshotReply.from_snapshot(snapshot, UpdateMetadata.of(TurnChangedUpdate(turn))), in the same order. Writes delegate to it after their change.OwnerReadRuntimeRequest/ReadOwnerOperationstay the only owner-live envelopes.action=goal_snapshot, with or without an operation, now decodes asMalformedRecord.capture_asresolvesreader.subject(thread)and callscapture_page_readonce.ReadTranscript,SubscribeTranscript(deferred_reads=(StaleRevision,)),ReadTranscriptUpdate(previouspublication state) andRefreshTranscriptstay UI lifecycle boundaries and are never tools. There are no forwarding properties.TranscriptSnapshotUpdate.capture's owner-publish path is untouched, andacp_extensiondoes not importorder_tools.CommanderUnitDetail,GoalSnapshotReplyandTranscriptSnapshotUpdatebecameToolResultdirectly, with no added fields or wrappers.TranscriptSnapshotUpdatekeepsAgentCommsUpdatefirst in its MRO.tests/test_tool_replies.pyreads aBusRead's result off the first concrete specialisation of aBusReadcapability along its MRO (types.get_original_bases, theCliCommand.result_typepattern), and refuses none or two. Non-BusReadtools keep the apply-annotation path. No per-leafapplycopies, no result roster.WorkBranchErrorjoined the existingtools/callfailure tuple (connector.py:463), so an unreadable original Git is a failed call, not a crash.f675fbe01).MalformedRecordjoined the same tuple: a newline-complete corrupt native entry escapedtools/callas HTTP 500 with no id. Now it isisErrorwith the original id and the domain's ownMalformedRecord(JSONDecodeError cause), and the connector keeps serving. The shared-read controls sendFieldCodec.encode_fields(operation); the oldFieldCodec.encodecarried thetoolselector thatToolRequest.invokerefuses.goal_snapshothas its ownUnknownFamilyMembertest.701b58768).TranscriptSnapshotUpdate.from_read(read: TranscriptRead) -> Self; body and wire unchanged, soTranscriptPageRead.read_asno longer receivesAny.5ed8ac75a).Comparison.failedfailed a clean-lines measure twice: once for introduced occurrences, and again for raw per-file growth throughMeasure.blocks. The second gate failed a moved occurrence that wrote nothing.CleanChangedLines.blocksis nowFalse, and every introduced occurrence still fails, including one an unrelated deletion offsets.Measure.grownis the growth predicate shared byblocksand the site locator, so base/head/delta andfoundare unchanged.FieldExtractionproves one transfer from the AST: an old class present at both revisions newly holds, in one typed field, one class absent from the base that no other old class holds. Each field it lost is declared once in that class with the same name, type (typing.Annotatedmetadata aside) and default. Only occurrences inside that field are credited, one departure each.TranscriptRequest.before/after/through/historical_sourcemoved intoTranscriptPageRead(order_tools.py:567/568/569/571). Raw NoneAbsence still shows observation.py −4 and order_tools.py +4. These are moved declarations with the sameNonedefaults, not deleted debt and not new debt.Nonein a method.908db01d6).DecoratorBinding.bindings(tree, scope)reads ScopeNames' lexical scopes.declarations()proves a name only when its scope binds it exactly once, by an unconditional import, class, function or annotated assignment in that body, and nothing is star-imported.named()/of()(boundary_decoder, dataclasses.field), held-class exports, field uniqueness, source disappearance and thetyping.Annotatedstrip all read that one owner.DeclaredClass.bound_onceis deleted.Annotated;class Pagefollowed byPage = dict; a field reassigned after its annotation; a same-name binding hidden in unpacking.Measure.missing/CleanChangedLines.missing: a positive clean-lines head count with no introduced report now fails and is reported asmissing.introduced=[]stays the explicit measured answer of zero new sites.1372658f7).DecoratorBinding.identityrewrites each name and attribute in a field's type and default to its owner, using the same lexical declarations:module_of);UnprovedIdentity, which equals nothing, not even itself: class-scope bindings, type variables, rebinding, star imports and unresolved names.TranscriptCursorresolves toagent_comms.transcripts.TranscriptCursorin both the old observation.py and the new order_tools.py, andstrtobuiltins.str. The credit is unchanged at 567/568/569/571.Cursor as Markfrom the same module carries its departure, while the sameCursorspelled from another module does not.1ea8ab3b6).DecoratorBinding.named(..., scope)drops a spelling whose root the class rebinds. Thetyping.Annotatedstrip and the held-class edge now read in the class's own scope, soAnnotated = dictorPage = dictin a class body proves nothing.75773856d).1ea8ab3b6failed this measurement repair on its own code: TypeErrors +6, TypeSwitch +2, TypeSwitchArms +20. Root's 87 focused measurement controls passed on that head.ScopeNames'visit_<kind>hooks now record bound names and declaring nodes in the walk that assigns scopes.DecoratorBinding.boundand the switch indeclarationsare deleted. On the file itself, TypeSwitch is 12 → 10 and TypeSwitchArms 62 → 42, both equal to main.SitePlaceandFieldExtraction.placeare deleted.introducedis a classmethod overcls.members_with(cls).75773856d(recorded by root, not run by the author):75773856d. No installed, frame or heap outcome is claimed.Numbers
ReadCommanderUnit,ReadOwnerGoal,GoalSnapshotRuntimeRequestdeleted, with no aliases).comms_input_delivery, pluscomms_commander_unit,comms_owner_goal,comms_transcript_page), each cataloged once inToolRequest,NativeRequestTools.catalog()andConnectorCaller.exposed().applyforwarding copies: one, moved toBusRead.ed6ff38dvs new, in fresh processes:CommanderUnitDetailpickle4fc68ac7, schema74234e98, unchanged;GoalSnapshotReplypickle92ead7d5, wire648ee867, unchanged;transcript_snapshot, andpage/identity/draft. The full transcript schema hash can't be computed in either tree (recursive types), so it isn't claimed.root/request_id/previous/owner/identity.goal_snapshotremains only in the two must-fail decode cases;TranscriptPageRead;request()senders remain for other commands (cancel,edit_goal,goal_history, …), none a goal snapshot read;getattr/setattr/hasattrsites remain unresolved static limits, not zero consumers.Controls added (not run here)
test_commander_notifications::test_expanded_unit_reads_original_orders_and_bounded_activity_then_refuses_stale_selection:ReadOperationandinvoke_toolreturn identical records for the same card;.gitbroken, MCP raises exactlyWorkBranchError.test_input_delivery_history::test_mcp_and_the_ui_read_the_owner_goal_from_the_live_owner: on the canonical owner socket, the owner's projection equals MCPinvokeand the UIReadOwnerOperationfor an empty and a set goal, includingmeta.batch == (TurnChangedUpdate(...),). It refuses after the owner stops.test_transcript_pages::test_one_page_operation_pages_alike_for_the_ui_and_mcp: 45 records, to the beginning and back with no duplicates, then an empty transcript. Every page is identical through the operation, the UI envelope and MCP.test_historical_viewsdoes the same for an attached recorded source.test_notification_read_state:UiRead/OwnerLiveRead;test_observer_deathowner reads derive solely frommembers_with(OwnerLiveRead).test_command_familiesround-tripsowner_read(comms_owner_goal)and refuses both oldgoal_snapshotforms.Not claimed: root's focused selections, hosted checks, the paired #3538 + #1302 integration, whole-process GC, 16 ms / p99.9, 29-agent coverage and P872 completion. They wait on root's evidence.
Plan (ticked as commits land)
BusRead[Result]withBusReaderauthority; notifications and recorded notifications migrated end to end (c57a7476b).RecordedGoalHistoryrecord for every boundary (7326056; Toad Goal history is Core's one RecordedGoalHistory record toad#1301 merged as5fa29f3).OwnerLiveRead: MCP, the UI and the owner socket carry oneInputDeliveryRead; the 3 duplicate declarations are deleted (36611e2b6, testsc0821f571; Toad Input delivery reads through Core's one owner-live operation (agent-comms#3538) toad#1302).UiReadthroughReadOperation; connector and native catalogs derive from the existingToolRequestcapability (noMcpRead, no pairing table); each boundary keeps its own caller/root validation and publication; the root is never a tool argument (780b9957e,d5b7376f8).LiveNotificationRead/RecordedNotificationReadunderNotificationRead, oneReadNotifications(root, operation)UI envelope, MCP outcomes without a UI envelope (21b07552a; Toada9a330b). Head verdict pending.OwnerReading/OwnerReplyConnectioninowner_connection.py,OwnerReadRuntimeRequestinbus_read.py,Result = ToolResultdefault,require_native_owner; no cycle, no function-local imports (5c262628f).ReadOwnerGoalfor external owners), held-identity check, and real EOF/root/recreation/death/external controls (a4702b1f7,08263a4c0).CommanderUnitRead(comms_commander_unit) in the genericReadOperationenvelope;ReadCommanderUnitdeleted (780b9957e; Toad9b6c482e2).OwnerGoalRead(comms_owner_goal) owner-live, turn metadata kept, writes delegate;GoalSnapshotRuntimeRequestandReadOwnerGoaldeleted (1c8b7e8eb; Toad6f41bc6f9).TranscriptPageRead(comms_transcript_page);TranscriptRequestholds only the operation;RefreshTranscriptkeeps its identity (d92c06392; Toad9572b2f2c).acp/agent.get_goal_snapshot,session_scopesnapshot/update requests andhistorical_sessionsbuild root-free operations inside their UI envelopes;acp/transcript_reader'sRefreshTranscript(root, identity)is a retained lifecycle boundary. Root stays in transport identity.Toad impact so far
7326056): Goal history is Core's one RecordedGoalHistory record toad#1301 moved 7 consumers (16 sites) toRecordedGoalHistory; merged5fa29f3.36611e2b6): Input delivery reads through Core's one owner-live operation (agent-comms#3538) toad#1302 (3772cf0source, +7/−6;0be58d8adds the native acquisition test).input_deliverysendsReadOwnerOperation(root, thread, InputDeliveryRead(include_history=...))and wraps the owner's overview inReadDeliveryView. The no-coordinationDeliveryView(),require_ownerand thedismiss_historical_inputswrite are unchanged. It must land with or after this PR.0be58d8,tests/native/test_native_goal_owner.py): the real native session runscontroller.input_delivery()andinput_delivery(include_history=True). Each returns aReadDeliveryViewwhosecurrent_scope is OwnerQueueInputDeliveryScope, the scope only the live owner produces, with empty inputs and zero counts on the fresh private wire. Nothing is mocked. Not run here.08263a4also countedClientTypeErrors+1 in Toad'sagent_controller.py. That's Toad main still importing the deletedReadDeliveryOverview, which only P325: return owner subscription busy refusals without waiting #1302 removes, so it clears when the pair lands together, not from Core.c57a7476b): none.9b6c482e2(commander page →ReadOperation(root, CommanderUnitRead(...))),6f41bc6f9(get_goal_snapshot→ReadOwnerOperation(..., operation=OwnerGoalRead()), with turn token,require_ownerandmeta.batchdispatch unchanged),9572b2f2c(session_scope/historical_sessions→TranscriptPageReadwith every cursor and source key; a recorded-reader test asserts the operation's page arguments and the envelope's root/previous).Verification
5c262628f, fresh-process imports in forward and reverse order (owner_connection→ … →connectorand back), plusruntime,ui_model.observationandbus_readalone: all import, andRuntimeRequest.member_named('owner_read')resolves tobus_read.OwnerLiveRead.__type_params__[0].__default__isToolResult. The declaration check gives identical wire, schema, catalogs, pickle and hash; the only change is thatOwnerReadingleftInputDeliveryRead's MRO, as intended. The changed source contains 0 banned words.runtime_requests/runtimeof the moved names, 0OwnerReadRuntimeRequest.declared_namereads, 0admitin our code. Anchors:bus_read.py:108,owner_connection.py:260/263/272.owner_socket_fixture,test_runtime,test_observer_death,test_input_delivery_historyandtest_command_familiesagainst installed Core6c1887e59's interpreter: all import, and the 6 lifetime tests are collected by name.🤖 Generated with Claude Code
https://claude.ai/code/session_01RnH6dS2Kmg1WxNnAc78YHA