Sub-session Workflow and Team Assignment Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Add a workflow action that safely creates a depth-one sub-session and support team selection during creation and directly from the parent session card.
Architecture: The workflow action owns its minimal insert path and repeats the existing parent-lock invariant instead of extracting a new shared abstraction. The existing agent-initiated creation path gains one optional validated team field. Inline card changes reuse the existing team badge, route, and assignment service, with an explicit callback to refresh the parent card’s child query.
Tech Stack: Fastify, Kysely, Zod, @opencx/workflows, React, SWR, OpenCX SDK, Vitest with real Postgres, and Playwright against the real dashboard/backend.
Spec: docs/superpowers/specs/2026-08-23-sub-session-workflow-team-assignment-design.md
Global Constraints
- Keep the implementation surgical; do not create a shared helper or migrate neighboring code.
- Use “session” and “sub-session” in all new customer-facing copy.
- Preserve the one-level hierarchy with the existing parent-row locking convention.
- Every organization-owned lookup must include the organization ID.
- No
any, unsafe casts, non-null assertions, mocks, spies, stubs, or fakes. - New functions in migrated chat-session repo/service areas require their own file and colocated spec; this plan adds no new chat-session service function.
- Use focused tests, backend
pnpm tsgo, dashboardpnpm type-check, and touched-file lint before commits. - Stage explicit paths only. Link commits and the PR to IMP-1119. Never merge the PR.
Task 1: Register and execute Create Sub-session
Files:- Create:
backend/src/workflow/definitions/actions/create-sub-session.action.ts - Create:
backend/src/workflow/definitions/actions/__tests__/create-sub-session.action.spec.ts - Create:
backend/src/workflow/definitions/actions/__tests__/create-sub-session.action.runner-e2e.spec.ts - Modify:
backend/src/workflow/enums/workflow-action.enum.ts - Modify:
backend/src/workflow/definitions/actions/index.ts - Modify:
backend/src/workflow/definitions/actions/lazy-loader.ts
-
Input:
{ parentSessionNumber: number; team?: string }. -
Output:
{ sessionId: string; sessionNumber: number }. -
Production export:
createSubSessionAction. - Step 1: Write the failing direct action matrix
createSubSessionAction.run and assert literal observable outcomes:
- A top-level parent creates one open child, inheriting
contact_id,channel,email, andsource_email_address; output identifiers equal the stored row. - An omitted team stores
inbox_id = null. - A valid support team stores
inbox_idand leavesassignee_id = null. - Parent timeline contains exactly one
SUB_SESSION_CREATEDrow whose JSON names both child identifiers and whose workflow/run IDs equal the action context. - A missing or foreign parent number returns
success: falseand creates no row in either organization. - A parent that is already a child returns
success: falseand creates no grandchild. - An unsupported ticketing system returns
success: falseand creates no child. - Missing, foreign, deleted, and non-support team IDs each return
success: falseand create no child. - Concurrently linking the parent under another session and creating its child leaves a valid depth-one graph regardless of which transaction wins.
- Step 2: Run the direct action spec and verify RED
cd backend && pnpm test create-sub-session.action.spec.ts.
Expected: failure because create-sub-session.action.ts and its enum member do not exist.
- Step 3: Implement the minimal action
CREATE_SUB_SESSION = 'create-sub-session' in the session action section. Define the action with Field.Number for the parent session number and the existing optionalSupportGroupField for team. Resolve and validate the integration, parent, and optional group inside the organization. In a Kysely transaction, call chatSession_repo_lockSessionsForLink, reject a locked parent with parent_session_id, and call chatSession_repo_insert with:
SUB_SESSION_CREATED on the parent with workflow_id, workflow_run_id, and a JSON payload containing child_session_id and child_ticket_number; emit session:updated for the parent. Return actionRunError.runtimeError for every thrown error. Register the action in the eager registry and lazy loader.
- Step 4: Run the direct action spec and verify GREEN
cd backend && pnpm test create-sub-session.action.spec.ts.
Expected: every matrix row passes with real Postgres state.
- Step 5: Write and run the failing workflow-runner test
createSubSessionAction.$stepValueType. Trigger it synchronously with workflowTrigger_service_triggerByWorkflowId. Assert the workflow and step complete, the step output contains the stored child’s ID/number, and the parent timeline row carries that workflow UUID and run UUID.
Run cd backend && pnpm test create-sub-session.action.runner-e2e.spec.ts before registering the action to prove the runner cannot load it, then after registration to prove the full path passes.
- Step 6: Verify Task 1
backend/src/workflow/definitions/actions/lazy-loader.spec.ts, then run backend typecheck and lint on the six touched files.
- Step 7: Commit Task 1
feat(backend, workflow): add create sub-session action with Linear: IMP-1119 in the body.
Task 2: Assign a team while creating a sub-session
Files:- Modify:
backend/src/chat-session/dtos/create-agent-session.dto.ts - Modify:
backend/src/chat-session/service/create-agent-initiated-session.ts - Create:
backend/src/chat-session/service/create-agent-initiated-session.team.spec.ts - Modify:
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(inbox)/inbox/_parts/components/NewSessionDialog.tsx - Regenerate:
dashboard/packages/sdk/src/schema.ts
-
CreateAgentSessionDtogainsgroup_id?: string. -
The existing creation service stores a validated
group_idas initialinbox_id. - Step 1: Write the failing backend matrix
- A support-enabled same-org team is stored on the created child.
- Omitting
group_idpreservesinbox_id = null. - Foreign, deleted, nonexistent, and non-support group IDs reject before insertion; the parent’s child count remains unchanged.
- An explicitly selected assignee remains the assignee even if initial team is also supplied.
- Step 2: Run the backend spec and verify RED
cd backend && pnpm test create-agent-initiated-session.team.spec.ts.
Expected: the schema rejects group_id or the child remains teamless.
- Step 3: Implement backend validation and persistence
group_id: z.string().uuid().optional() to the DTO. In chatSession_service_createAgentInitiatedSession, resolve the group with GroupRepo.getGroup({ orgId, groupId }) before contact mutation or outbound preflight; reject missing/foreign/deleted teams and is_support_enabled === false. Set inbox_id: group_id in sessionInsert. Do not invoke automatic distribution during initial creation.
- Step 4: Run the backend spec and verify GREEN
- Step 5: Add the dashboard field
NewSessionDialog, fetch groups only while a sub-session dialog is open, filter is_support_enabled, and maintain the optional team ID. When parentSessionId exists, render a Team Select next to Assignee with No Team plus the support-team options. Add group_id to both existing-contact and new-contact request bodies. Reset the selection on close. Standalone sessions render no Team field.
- Step 6: Regenerate the SDK and verify Task 2
CreateAgentSessionDto.group_id plus the new workflow action metadata generated from the backend.
- Step 7: Commit Task 2
feat(backend, dashboard): assign team on sub session creation with Linear: IMP-1119.
Task 3: Change a child team inline from the parent card
Files:- Modify:
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(inbox)/inbox/_parts/components/common/SessionBadges.tsx - Modify:
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(inbox)/inbox/_parts/components/common/SessionIdentity.tsx - Modify:
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(inbox)/inbox/_parts/components/session-card/session-card.tsx - Modify:
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(inbox)/inbox/_parts/components/SessionContentMain/ChatAside/ChatAside__SubSessions.tsx - Create:
dashboard/apps/dashboard/e2e/sub-sessions-team-assignment.e2e.ts
-
SessionBadge__Teamgains optionalonChanged?: () => void | Promise<void>. -
SessionIdentityandSessionCardgain optionalhideTeamBadge?: boolean. - Step 1: Write the full-stack browser test
- The global standalone New Session dialog contains no Team field.
- The parent card’s Create sub-session dialog contains Team and creates a child initially owned by Team A.
- The parent Sub-sessions card shows Team A without navigating away.
- Selecting Team B from the inline team control updates the real backend child row and the visible card to Team B.
- Reloading the parent preserves Team B.
- Step 2: Run Playwright and verify RED
E2E_WITH_BACKEND=1 pnpm test:e2e -- sub-sessions-team-assignment.e2e.ts from dashboard/apps/dashboard.
Expected: the Team field and inline control are absent.
- Step 3: Implement the inline control
onChanged after the existing team mutation and inbox/session refetches in SessionBadge__Team. Pass hideTeamBadge through SessionCard to SessionIdentity. In ChatAside__SubSessions, calculate canChangeTeam = hasPermission('sessions:write'); hide the embedded static badge and render SessionBadge__Team outside the child Link only for that permission. Pass refreshSubSessions as onChanged. Keep unlink visibility keyed to sessions-sub-sessions:write.
- Step 4: Run Playwright and verify GREEN
- Step 5: Verify Task 3
- Step 6: Commit Task 3
feat(dashboard, inbox): assign child teams inline with Linear: IMP-1119.
Task 4: Final verification, review, PR, and Greptile
Files: All files changed by Tasks 1–3 plus this spec and plan.- Step 1: Run the complete focused verification matrix
pnpm tsgo, dashboard pnpm type-check, touched-file lint/format checks, generated SDK diff validation, and the real Playwright scenario.
- Step 2: Apply the simplify pass
- Step 3: Perform the OpenCX reviewer pass
- Step 4: Commit planning artifacts or final fixes
- Step 5: Push and create the PR
aziz-hasan/feature/create-sub-session-workflow to origin and create a PR targeting main. The PR body must link IMP-1119, summarize the workflow action and both team-assignment surfaces, list the test matrix and exact commands, and note that no migration or new endpoint was added.
- Step 6: Babysit automated review