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_inferredevent. - 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
decomposeStandard Action).
Group (parent_relationship='group')¶
- Umbrella container; no action of its own. Lands in
MONITORINGimmediately on spawn. - Children carry their items as
context_items(a tuple of ContextItem rows). Items move between sibling group-parents at item granularity viathreads.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
MONITORINGimmediately. - 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_datadetectsparent_relationship == 'singular'and surfaces each child's actions inline on the parent'sactionsarray, augmented withhost_thread_id,state(derived from the child'sfsm_state), andsettled(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_parentsettingall_dismissed_singularonTRIG_EXECUTION_DONE;engine._default_branch_resolverroutes thedone_when_all_subthreads_terminalbranch toDISMISSEDwhen that flag is set. - Sub-LLM context items:
pipelines/singular.py:_build_subcall_context_itemsattaches the deadline-extract and project-picker SubCall outputs to every spawned thread as ContextItems withsource='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_actionwith{feedback}records aKIND_ACTION_REDIRECTEDevent and transitions the childAWAITING_CONFIRMATION → AWAITING_INFERENCEwithdata={'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_blocksurfaces unresolved redirect feedback onto the LLM prompt; resolved feedback (a neweraction_inferredalready landed) is skipped.render._latestpicks the newestaction_inferredas 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 withparent_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.py—group_thread,move_item,delete_group_subthread,cascade_approve_umbrella.work_buddy/threads/decompose.py—cascade_terminal_to_parent(used by all three patterns).work_buddy/threads/render.py—build_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.py—spawn_thread_for_matterper-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).