> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# 2026 08 23 sub session workflow team assignment

# 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:

```ts theme={"dark"}
{
  id: v4(),
  copilot_id: ctx.organization.id,
  channel: parent.channel,
  contact_id: parent.contact_id,
  email: parent.email,
  source_email_address: parent.source_email_address,
  verified: 1,
  ...SessionStatusUtils.pair(SessionStatus.OPEN),
  parent_session_id: parent.id,
  parent_ticket_number: parent.ticket_number,
  inbox_id: input.team,
  assignee_id: null,
  meta: { [SESSION_META_IS_OUTBOUND_KEY]: true },
}
```

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.
