Skip to main content

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, dashboard pnpm 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
Interfaces:
  • Input: { parentSessionNumber: number; team?: string }.
  • Output: { sessionId: string; sessionNumber: number }.
  • Production export: createSubSessionAction.
  • Step 1: Write the failing direct action matrix
Add real-database tests that call createSubSessionAction.run and assert literal observable outcomes:
  1. A top-level parent creates one open child, inheriting contact_id, channel, email, and source_email_address; output identifiers equal the stored row.
  2. An omitted team stores inbox_id = null.
  3. A valid support team stores inbox_id and leaves assignee_id = null.
  4. Parent timeline contains exactly one SUB_SESSION_CREATED row whose JSON names both child identifiers and whose workflow/run IDs equal the action context.
  5. A missing or foreign parent number returns success: false and creates no row in either organization.
  6. A parent that is already a child returns success: false and creates no grandchild.
  7. An unsupported ticketing system returns success: false and creates no child.
  8. Missing, foreign, deleted, and non-support team IDs each return success: false and create no child.
  9. 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
Run 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
Add 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:
After commit, insert 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
Run 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
Create a real manual workflow version whose only block is 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
Run both new specs plus 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
Stage the six Task 1 files explicitly and commit 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
Interfaces:
  • CreateAgentSessionDto gains group_id?: string.
  • The existing creation service stores a validated group_id as initial inbox_id.
  • Step 1: Write the failing backend matrix
Use real organizations, contacts, parents, and groups. Assert:
  1. A support-enabled same-org team is stored on the created child.
  2. Omitting group_id preserves inbox_id = null.
  3. Foreign, deleted, nonexistent, and non-support group IDs reject before insertion; the parent’s child count remains unchanged.
  4. An explicitly selected assignee remains the assignee even if initial team is also supplied.
  • Step 2: Run the backend spec and verify RED
Run 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
Add 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
Run the new team spec plus the existing agent-initiated service and create-controller permission specs.
  • Step 5: Add the dashboard field
In 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
Run the repository SDK generation command, focused backend specs, dashboard typecheck, and lint on touched backend/dashboard files. Inspect generated schema changes to confirm they are limited to CreateAgentSessionDto.group_id plus the new workflow action metadata generated from the backend.
  • Step 7: Commit Task 2
Stage the five Task 2 files explicitly and commit 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
Interfaces:
  • SessionBadge__Team gains optional onChanged?: () => void | Promise<void>.
  • SessionIdentity and SessionCard gain optional hideTeamBadge?: boolean.
  • Step 1: Write the full-stack browser test
Using Dev login and real backend requests, create an OpenCX-ticketing organization, two support teams, a contact, and a parent web session. Then assert:
  1. The global standalone New Session dialog contains no Team field.
  2. The parent card’s Create sub-session dialog contains Team and creates a child initially owned by Team A.
  3. The parent Sub-sessions card shows Team A without navigating away.
  4. Selecting Team B from the inline team control updates the real backend child row and the visible card to Team B.
  5. Reloading the parent preserves Team B.
  • Step 2: Run Playwright and verify RED
Start repository dependencies, backend, and dashboard using the documented root/backend/dashboard commands. Run 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
Call 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
Repeat the exact browser command and require all five observable assertions to pass against the real backend and database.
  • Step 5: Verify Task 3
Run dashboard typecheck, touched-file lint, existing Sub-sessions card tests for regression detection, and the new Playwright test. The existing card gating suite may keep its unrelated component boundary stub; all new feature behavior is proven through the real full-stack test.
  • Step 6: Commit Task 3
Stage the five Task 3 files explicitly and commit 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
Run all new backend specs together, relevant existing sub-session/link/depth/controller/lazy-loader specs, backend 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
Inspect only this task’s diff for duplicated validation, speculative abstractions, unnecessary files, broad formatting, unsafe types, and copy terminology. Remove only complexity introduced by this work, then rerun the affected checks.
  • Step 3: Perform the OpenCX reviewer pass
Read every changed file completely; trace every touched symbol’s callers; enumerate production branches against the test matrix; check organization scoping, row-lock concurrency, permission boundaries, public copy, one-function migration rules, and zero test doubles. Fix every Critical or Important finding and rerun its proving test.
  • Step 4: Commit planning artifacts or final fixes
Stage explicit remaining paths and create the smallest Conventional Commit needed, linked to IMP-1119. Confirm the branch is clean.
  • Step 5: Push and create the PR
Push 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
Poll PR checks and reviews. For every Greptile, Copilot, or Cursor Bugbot finding: reproduce or validate it, fix surgically when valid, run the focused regression test, commit and push, then reply to the review thread with evidence. Continue until Greptile reports 5/5 and required checks are green. Do not merge.