# Delegated-task timeline ## Read-only boundary A task row opens a phone sheet or wide-screen dialog above the mounted parent chat. Close/back returns to the same composer, scroll controller and parent state. Parent text, reasoning, tools and completion continue to accumulate, but all automatic scroll application is fenced while inspecting, including pre-open queued layout callbacks and older-history adjustments. Dismissal does not replay suppressed jumps or change sticky stream-follow intent. The viewer has Refresh and explicit Load older controls, but no composer, task-control actions or nested `ChatScreen`. `DashboardTaskHistory` uses `DesktopGatewayClient.normalizedGatewayBaseUrl` and `DashboardClient` authentication: password cookie, trusted proxy, or a local SPA token. It never opens a socket, watches/resumes a child, replaces the parent subscription, or closes/finalises a server runtime. Disposal closes only the viewer's HTTP client and timer; a closing wrapper blocks follow-up GETs after a late login. Each read uses: ```text GET /api/sessions/{Uri.encodeComponent(childStoredId)} [?profile=] GET /api/sessions/{Uri.encodeComponent(childStoredId)}/messages ?limit=50&offset=N&order=latest&inline_images=false&include_compacted=true [&profile=] ``` The messages response must contain `session_id`, `messages` and matching pagination (`limit`, `offset`, `order`, `returned`); an explicitly selected profile must match on **every** detail and messages response. `include_compacted=true` is necessary even without rotation: stock Dashboard defaults it to false, hiding archived tool/reasoning rows after in-place compaction. It is a display read, not model replay, and does not reactivate archived rows. Both stock detail and messages routes accept unambiguous prefixes. Require every detail response's bare `id` to match its requested ID exactly, including advertised newer references. Keep the first durable child reference as the task-lineage root and the latest live `child_session_id` separately as an **untrusted candidate**. The parent progress relay follows the child's current `session_id` after rotation; prove that candidate before requesting its transcript. A previously verified segment takes precedence over a late ancestor reference. Reject competing siblings rather than selecting by recency or mixing branches. Proof walks `parent_session_id` backwards to the task root, bounded to 32 nodes per path and 100 detail reads per fetch with a per-fetch cache. Require exact IDs/profile ownership, a compression-ended parent, finite coherent timestamps, equal persisted sources and parseable/null model configs. Rotation copies `source` (including `tool`) and `_session_init_model_config`; inherited `_delegate_from`, `_branched_from` and `_reset_from` values are valid when unchanged from the parent and pointing to an earlier grandparent. Reject direct-parent markers, changed inherited markers, missing fields, cycles, non-compression edges and cross-profile/source changes. Do not require child creation after the parent's `ended_at`: actual `publish_compression_child` inserts the child before stamping parent closure in the same transaction. Require creation no earlier than the parent's creation and coherent parent closure instead. A different messages canonical ID is also only accepted after proof on the same branch, before publishing rows. ### Explicit backend discovery limitation The real `_publish_rotated_compaction` path at fixture commit `d923a5eb48` preserves inherited delegation markers and `tool`/`subagent` source. On that backend, original-child `/messages` and `/latest-descendant` both return the **original** ID and omit final continuation rows. Actual `GET /api/sessions?limit=100&offset=0&sources=subagent,tool&full=true&archived=include&min_messages=0` also returns no delegated rows in the disposable fixture: the route still excludes children. The list GET additionally invokes auto-archive maintenance and can back-fill pins beyond the requested limit. The viewer never calls it, scans the store, or treats an empty list as proof of no continuation. When the newest verified segment has `end_reason == 'compression'` with no provable newer reference, display known rows with a localised **Partial history** warning: final output may be missing, automatic refresh is paused and manual Refresh remains available. A new parent reference wakes the viewer and resolves this missing-newer-reference warning only after successful proof/read. Reopened older roots without a newer reference remain explicitly limited. Complete automatic reopen requires a backend read-only, task-scoped continuation contract that understands inherited markers/tool sources and detects ambiguous forks, or a persisted latest-child reference. This change does not invent that endpoint or silently present original history as complete. Historical coverage is a separate limitation: the actual `tool`-source tip `/messages` route omits compression ancestors, unlike the `subagent` fixture. A verified tool continuation therefore carries the typed `partialAncestors` limitation and a localised warning that **earlier task segments may be missing**, even when its current page is short, `hasOlder` is false, and the final reply is visible. This includes cold opening with only the tip reference: one additional exact-ID/profile-checked parent metadata read proves a compression edge using the same source, timestamp and inherited-marker checks, without extending the task's identity anchor or reading ancestor messages. Direct delegation/branch/reset markers do not inherit a parent chat's compression warning. Retain all known same-branch rows and keep Refresh available; retained rows alone do not establish complete ancestor coverage. This historical warning does not pause polling of a running current tip or change completion/final-read retry policy. The unresolved-newer-segment pause remains separate. No ancestor aggregation, global scan or new backend endpoint is introduced. There is no API-server `data` fallback: that allowlist loses reasoning. Missing/rejected Dashboard authentication, incompatible responses and unprovable identity are typed unavailable states. Unresolved genuine compression is a separate partial-history limitation retaining known content. Refresh/Retry remains available. A 404 remains retryable: it cannot distinguish an unflushed child from an unsupported route. Ordinary pages use exact detail, messages and a final fresh detail read to detect compression completed during the request; continuations use additional bounded detail reads. Only HTTP GET inspection is used, never child runtime RPC/watch/control or global session listing. ## Identity and reopen `GatewaySubagentActivity` retains `child_session_id`, `delegation_id`, `parent_id` and terminal status separately from `subagent_id`. Opaque IDs are accepted unchanged only when non-empty, at most 512 UTF-16 code units, free of control characters/surrounding whitespace, and not `.` or `..`. Invalid IDs are rejected, never repaired or shortened. Id-only progress preserves the known goal; terminal failure/interruption/completion is sticky under late progress. Reopen recovery examines only `delegate_task` spawn calls (omitted action means spawn) and their known top-level result/`results` entries. Call arguments and result schemas both exclude `steer`, `stop`, `list` and other non-spawn actions; orphan `queued`, `interrupt_requested` and `subagents` control/list acknowledgements are ignored too. A request to stop is not evidence that the child stopped. Only recognised terminal statuses (`completed`, `failed`, `error`, `interrupted`, `cancelled`) recover terminal state; missing or unfamiliar statuses remain non-terminal. Real background `dispatched` payloads expand their parallel `goals`/`subagent_ids` without fabricating stored references. Recovery accepts a real structured `child_session_id`, never an arbitrary nested `session_id`, task number, goal or live-log path. Without a retained reference, the row opens an explicit unavailable explanation. In particular, stock background dispatch may preserve runtime IDs and log paths but no durable child ID. Failure, interruption and cancellation have distinct accessible status/icons, and a mixed terminal group says “finished”, not that all tasks succeeded. ## Snapshot and pagination ownership `DelegatedTaskController` owns a single request flight, generation fences and a two-second foreground polling timer. Lifecycle changes, completion and disposal invalidate pending reads; a newer generation queues behind an older flight. Backgrounded viewers do not poll. Completion triggers a final read; transient final-read failures receive up to three automatic retries. Successful completed snapshots stop polling, but Refresh remains available for delayed database flushes. Errors retain displayed rows; an empty successful response is not an unavailable state. Cold open reads one page. Durable numeric row IDs determine deduplication/order, never content. Latest refreshes retain older loaded rows. A disjoint latest page resets paging coverage so Load older can fill the gap. Pagination belongs to a canonical segment: on verified advancement, atomically commit identity/generation, reset coverage and fetch OFFSET 0. A canonical change during Load older restarts the HTTP read at OFFSET 0 instead of applying the old segment's offset. Retain durable rows only from the verified same branch; held old-generation responses cannot mutate the new window. Paging coverage counts unique fetched IDs separately from retained display rows. Raw older-page lengths plus subsequent head growth can double-count an append-induced overlap and skip the old tail; a deterministic regression covers that interleaving. The existing parent `ChatHistoryWindow` is not changed. ## Rendering and verification Recorded user/assistant/agent/tool/system rows render chronologically; `agent` uses assistant presentation and server-hidden rows are omitted. Markdown prose is selectable. Stock `display_commentary` is public assistant prose, even on an empty-content row; do not recover it from raw `codex_message_items` when server policy suppresses it. `display_reasoning` is authoritative whenever present, including explicitly empty/null values, and suppresses duplicate raw reasoning/sidecars. `display_content` is also authoritative when empty/null. Projected text that happens to look like JSON remains literal prose. Without projected reasoning, collapsed reasoning reads exposed text/summary fields from `reasoning`, `reasoning_content`, `reasoning_details` and `codex_reasoning_items`, supporting JSON-string/native-list sidecars without displaying encrypted data. Tool cards retain actual call IDs, arguments and results; missing arguments are labelled, not invented. Expansion keys use durable rows and sidecar/call identities. Markdown images use alt text rather than automatic remote fetches. All new interface copy uses AppLocalizations; new English ARB keys have explicit English fallbacks in every shipped locale. Services/controllers expose typed failure reasons instead of user-facing English or raw exception strings. Parent events update the open controller through a task-specific, disposed-safe bridge. Exposed live previews are separately labelled, never mixed into durable rows or mirrored as synthetic tool calls. `test/fixtures/delegated_task_dashboard.json` is captured from real stock route functions with a disposable SessionDB. Rotation fixtures call actual `_publish_rotated_compaction` for delegated `subagent` and `tool` sources, retaining actual inherited model config; the old hand-created null-config continuation is removed. They capture original/tip messages, latest-descendant and source-scoped listing (fixture-only maintenance disabled/read-only connection), alongside archived rows, prefix resolution, commentary projections and control ACKs. A permanent completed-tool-tip widget regression asserts that the missing ancestor is disclosed visibly rather than silently treated as complete; another verifies known-row retention and live-tip polling independently of the ancestor warning. The fixture records its backend commit. Regenerate with `python tools/capture_task_history_fixture.py /path/to/hermes-agent` using an interpreter containing backend dependencies. Capture establishes a temporary Hermes home before imports and disables launcher/update maintenance; it writes only its scratch home and the repository fixture, never existing profiles or a live database/server. All four independent specification findings were reproduced as failing regressions before fixes. Focused tests additionally use mocked HTTP, controlled futures and fake-clock lifecycle transitions. They cover exact/prefix/foreign/fork identity, authentication/query shape, projected commentary rendering, spawn recovery, running child → persisted control ACK → real ChatScreen hydration → late progress → continued polling → actual completion, all locale labels, terminal icons, pagination overlap/gaps, stale-response disposal, retry, stable expansion, modal states and parent-chat dismissal on phone/wide layouts. This remains snapshot polling, not token streaming; missing historical child references cannot be reconstructed. Independent quality review, device checks and signed APK validation belong to the parent release workflow.