Skip to content

Threads — parent-child relationship patterns (decompose / group / singular)

Three parent-child relationship patterns. Decompose: parent has an action, children FSM-execute, cascade-on-terminal advances parent. Group: umbrella holds N cluster sub-threads with item-level drag-drop reorganization (Chrome / journal / email scans). Singular: umbrella holds N children whose actions render hoisted onto the parent's card so the user sees one thread with N proposals (inline-capture multi-record path).

Details

Three parent-child relationship patterns coexist. The discriminator is Thread.parent_relationship (free-string column on the threads table).

Decompose (parent_relationship='decompose', the default)

  • Parent thread has its own action_inferred event.
  • Children each carry one ContextItem and one own action_inferred; FSM-execute independently.
  • Cascade-on-terminal: when every child is terminal, parent advances to DONE via cascade_terminal_to_parent (work_buddy/threads/decompose.py).
  • Used when an agent decides 'this work needs to be broken down' (the decompose Standard Action).

Group (parent_relationship='group')

  • Umbrella container; no action of its own. Lands in MONITORING immediately on spawn.
  • Children carry their items as context_items (a tuple of ContextItem rows). Items move between sibling group-parents at item granularity via threads.group.move_item.
  • 'Sibling' = group-parents sharing the same originating_scrape_id.
  • Cascade-on-terminal still fires.
  • Empty group children do NOT auto-DISMISS; manual X-button delete via threads.group.delete_group_subthread.
  • Frontend: custom multi-column drag-drop view (window.renderGroupSubThreads).
  • Used by source-pipeline scrapes: chrome triage, journal backlog, email scan.

Singular (parent_relationship='singular')

  • Umbrella container with no action of its own. Lands in MONITORING immediately.
  • Each child carries one ContextItem (the captured selection) and one action_inferred — same shape as a single record's spawn.
  • Items do NOT move between siblings. There's no reorganization to do.
  • Render-time hoist: work_buddy/threads/render.py:build_render_data detects parent_relationship == 'singular' and surfaces each child's actions inline on the parent's actions array, augmented with host_thread_id, state (derived from the child's fsm_state), and settled (true when state is done/rejected/failed). Settled actions render gray + status badge on the umbrella card; pending first, settled last.
  • The frontend's standard render path renders the parent's umbrella card with the hoisted Actions section. The Sub-threads (N) section is suppressed.
  • Cascade-on-terminal: when every child is terminal, the parent advances based on the children's terminal mix. All children DISMISSED → parent DISMISSED. Any child DONE or HANDED_OFF → parent DONE. Decompose / group umbrellas keep the simple all-terminal → DONE rule. Implemented via decompose.cascade_terminal_to_parent setting all_dismissed_singular on TRIG_EXECUTION_DONE; engine._default_branch_resolver routes the done_when_all_subthreads_terminal branch to DISMISSED when that flag is set.
  • Sub-LLM context items: pipelines/singular.py:_build_subcall_context_items attaches the deadline-extract and project-picker SubCall outputs to every spawned thread as ContextItems with source='subcall', type=<subcall_name>. Children of a singular umbrella inherit the same audit ContextItems alongside the captured selection, so the dashboard and downstream agents can inspect what the sub-LLMs saw without re-running them. Generic across the four spawn shapes (flat / singular umbrella / refusal / dismissed).
  • Per-action redirect: hoisted action chips on a singular umbrella's card carry a Redirect button. POST /api/threads/<host>/redirect_action with {feedback} records a KIND_ACTION_REDIRECTED event and transitions the child AWAITING_CONFIRMATION → AWAITING_INFERENCE with data={'target': 'action'}, so the inference worker enqueues only action-layer inference (no walk back through intent / context). The bootstrap inference runner's _build_redirect_feedback_block surfaces unresolved redirect feedback onto the LLM prompt; resolved feedback (a newer action_inferred already landed) is skipped. render._latest picks the newest action_inferred as the active proposal; the prior one stays in event history for audit.
  • Used by the inline-capture multi-record path (work_buddy/pipelines/inline.py:_spawn_inline_umbrella): when a right-click selection's verdict produces 2+ actionable records, the umbrella is spawned with parent_relationship='singular'. The user sees ONE thread with N actions on the dashboard.
  • Future consumers: per-message email triage (when built) reuses the same pattern.

Choosing the pattern

  • One matter, the agent self-decides to fan out → decompose.
  • Many items, organized into clusters with item-level drag-drop → group.
  • One matter, multiple proposed actions on it → singular (render hoist makes it look like one thread).
  • Multiple separate matters → N flat threads (no umbrella). The text-segmenter SubCall (clarify/text-segmenter) is the upstream filter that detects multi-matter captures so they don't conflate into one singular umbrella.

Backend module map

  • work_buddy/threads/group.pygroup_thread, move_item, delete_group_subthread, cascade_approve_umbrella.
  • work_buddy/threads/decompose.pycascade_terminal_to_parent (used by all three patterns).
  • work_buddy/threads/render.pybuild_render_data; the singular-hoist branch is here.
  • work_buddy/threads/execution_runner.py — EXECUTING state-entry handler.
  • work_buddy/pipelines/runner.py — source-pipeline driver (group umbrellas).
  • work_buddy/pipelines/inline.py — inline-capture pipeline (singular umbrellas).
  • work_buddy/pipelines/singular.pyspawn_thread_for_matter per-matter spawn primitive (source-agnostic).

Frontend

Group umbrellas: dispatcher in scripts/tabs/threads/main.py's renderThreadDetail checks thread.parent_relationship === 'group' and renders the multi-column group view (scripts/tabs/threads/group.py). Drag-and-drop columns, action chips with dropdowns, cascade_approve_umbrella.

Singular umbrellas: standard render layout applies, but _renderActionsSection displays the hoisted children's actions on the parent's card and _renderSubThreadsSection is suppressed. Whole-card click — the action card itself acts as a button (role='button', onclick → threadsPushPath(host_thread_id), onkeydown for Enter/Space) so the user can navigate to the child's full thread page where Approve / Edit / Redirect / Reject all available; inner buttons stop propagation so they don't double-fire. Settled actions render gray + with a status badge (done / rejected / failed / executing). Inline Redirect button per pending action invokes threadCardRedirectAction(hostThreadId).