Backs the schema added in 210d310 with SQLAlchemy 2.0 models.
- SessionFact: "What we know" facts with polymorphic source_ref pointing
at task-lane item UUIDs inside ai_sessions.pending_task_lane (not a FK
per Section 4.2).
- SessionSuggestedFix: AI-proposed resolutions with supersession tracking
and the full user_decision state machine.
- DraftTemplate: post-resolve templatization queue with promotion to
script_templates.
- AccountSettings: per-account JSONB preferences grab-bag with async
classmethod helpers — get_setting(db, account_id, key, default) reads
without creating, set_setting(db, account_id, key, value) upserts via
Postgres ON CONFLICT + jsonb `||` merge so existing keys are preserved.
Lazy row creation matches the Phase 1 design.
Column additions on existing models to mirror the migration:
- AISession: resolution_note_* / escalation_package_* / state_version
(the preview-cache-invalidation counter consumed by Phase 3).
- ScriptTemplate: source_session_id / source_user_id / source_ticket_ref
(provenance for templates promoted from DraftTemplate).
All four new models registered in app.models.__init__ and __all__.
TYPE_CHECKING-guarded relationship imports throughout, matching the
repo's existing model style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the backing store for the FlowPilot unified session surface, per
the FLOWPILOT-MIGRATION.md Phase 1 deliverable. Descends from production
head 074 (add_network_diagrams_table).
New tables (all tenant-scoped, all RLS-enabled + forced):
- session_facts — "What we know" facts. source_ref is a polymorphic
pointer to a task-lane item inside ai_sessions.pending_task_lane
(no DB-level FK; integrity enforced at service layer per Section 4.2
of the design doc). Soft-delete via deleted_at; active-facts partial
index excludes deleted rows.
- session_suggested_fixes — AI-proposed resolutions. One active per
session at a time (supersession tracked via superseded_at; partial
index on (session_id) WHERE superseded_at IS NULL powers the
"find active fix" query).
- draft_templates — scripts pending post-resolve templatization.
Partial index on (account_id) WHERE status='pending' supports the
"N scripts ready to review" Script Library badge.
- account_settings — new per-account table with JSONB preferences
grab-bag. Rows created lazily on first write; get_setting returns
default when no row exists.
Column additions on ai_sessions:
- resolution_note_markdown / posted_at / external_id
- escalation_package_markdown / posted_at / external_id
- state_version (INTEGER NOT NULL DEFAULT 0) — incremented atomically
by any write that invalidates the resolution note preview cache
per Section 5.5. Phase 3 consumes this.
Column additions on script_templates:
- source_session_id, source_user_id, source_ticket_ref — powers the
"generated from CW #X · resolved by Y · used N times" provenance
chip in the Script Library.
RLS pattern matches the repo convention (074 / network_diagrams is the
nearest template): ENABLE + FORCE, USING + WITH CHECK on
`account_id = app.current_account_id`. Downgrade is reversible —
drops in the inverse order of creation so FK dependencies unwind.
No runtime verification from code-server; migration apply + downgrade
will be verified on the new dev environment per the standing deferral.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Significant rewrite of FLOWPILOT-MIGRATION.md after post-Codex plan review
and the Phase 0 in-flight audit. Archives the pre-rewrite version as
FLOWPILOT-MIGRATION-v1.md and keeps the Codex review under
CODEX-FlowAssist-Migration-PLAN.md for traceability.
Substantive changes that affect implementation:
- Section 0.1 adds a spec-drift note listing corrections integrated into
this revision (API namespace, task-lane item UUIDs, account_settings
creation, missing /tickets/ai-parse endpoint).
- Section 2 adds "Task lane item ID" terminology — stable UUID assigned
to items inside ai_sessions.pending_task_lane so session_facts.source_ref
has something reliable to point to.
- Section 4.1 adds ai_sessions.state_version (INTEGER NOT NULL DEFAULT 0)
and escalation_package_external_id. state_version drives preview cache
invalidation; incremented atomically on writes to facts / suggested
fixes / script_generations.
- Section 4.6 creates account_settings as a new table with JSONB
preferences column, lazy row creation, and a promotion rule for when a
setting should graduate to a typed column.
- Section 5 namespaces all session-scoped routes under
/api/v1/ai-sessions/{id}/... to match the existing codebase pattern.
- Section 5.5 documents the preview caching strategy (state_version
keyed, 500ms client debounce, Redis planned).
- Section 6.6 adds per-service MCP capability flags alongside the model
tier flags.
- Section 7.1 makes the /assistant -> /pilot redirect include the
session-deep-link path and preserve the session ID.
- Section 8.2 adds supersession semantics for [SUGGEST_FIX] markers.
- Section 9 Phase 1 now explicitly includes account_settings and
state_version; Phase 3 uses state_version-keyed caching; Phase 5
mentions MCP inheritance via chat_call_cached wrapper.
- Section 11 adds a dedicated test plan (migrations, backend, frontend,
manual QA).
- Section 14 captures the eight planning decisions made during the
Phase 0 conversation so they are traceable.
No code changes in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the chat caller to a name that signals its actual purpose, and
factors the reusable cached-system-block + cached-history + cache-usage-log
primitives out to app.core.ai_provider so they can be shared with the
provider-generic path without pulling MCP/beta/images into the abstract
interface.
Helpers added to ai_provider.py:
- `build_anthropic_chat_messages(history, new_message, images, format_reminder)`
— owns: copy history, apply cache_control to last history message,
append format reminder to new message, render images as multimodal blocks.
Anthropic-shaped by design; do not call from Gemini paths.
chat_call_cached keeps exactly the concerns that are unique to the one
MCP/beta/multimodal chat caller:
- Anthropic beta endpoint invocation
- Microsoft Learn MCP server wiring (ENABLE_MCP_MICROSOFT_LEARN)
- Retry-without-MCP fallback
- Format-reminder content string (declared as module constant)
- Phase 0.5 telemetry (mcp.turn, mcp.fallback)
Documents in the module docstring AND at the function site that this is
the ONE MCP/beta chat caller and should not become the general provider
path. MCP/beta/images are features of exactly one optional Anthropic beta
endpoint; routing them through AnthropicProvider would leak a provider-
specific concern into the abstract interface that also serves Gemini.
Behavior change: chat_call_cached now reuses the singleton AnthropicProvider
HTTP client via `_get_anthropic_client(...)` instead of instantiating a new
`anthropic.AsyncAnthropic(...)` per call. Matches the provider's own pattern
and avoids burning connections per-turn. No user-visible difference.
No runtime verification from code-server. TODO(phase0-verify) in
ai_provider.py tracks the cache-hit verification owed on the new dev env.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps each static system prompt in a single-block list so Phase 0.1's
AnthropicProvider applies cache_control: ephemeral automatically (policy α,
first block gets marked when no caller-authored cache_control is present).
Call sites:
- ai_tree_generator.scaffold_branches: SCAFFOLD_SYSTEM_PROMPT (~1k tokens)
- ai_tree_generator.generate_branch_detail: BRANCH_DETAIL_SYSTEM_PROMPT
(~2.5k tokens with few-shot example); retries inside the same function
re-read the cached block instead of paying full input cost on each attempt
- kb_conversion.convert_document: TROUBLESHOOTING or PROCEDURAL prompt
(each caches independently by text content)
- ai_fix.generate_fixes: FIX_SYSTEM_PROMPT on first attempt + corrective retry
- script_builder.send_message: SYSTEM_PROMPT_TEMPLATE (per-session language
substitution — same-language sessions share cache entries)
Each edit includes an inline comment explaining why the block is cacheable
(stable-constant, retry-reuse, per-language variant) so a future dev can
see the intent at the cache_control marker site.
script_builder history caching deliberately deferred — per Phase 0.1
decision (option i), AnthropicProvider does not automatically cache the
message list. If script_builder's growing 20-message history turns out
to be a visible cost driver via the anthropic.cache telemetry, route
that caller through the 0.4 chat wrapper which handles history caching.
No runtime verification from code-server; cache-hit behavior will be
confirmed against the new dev environment when it's up, per the inline
TODO(phase0-verify) in ai_provider.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /tickets/ai-parse endpoint named in Phase 0.2 does not exist in the
codebase (verified: zero matches for ai-parse/ai_parse across endpoints,
services, models, and all branches/commit messages). integrations.py:557
is get_ticket_statuses — a CW passthrough with no AI call.
Adding a block-quoted note under the 0.2 deliverable that flags the
drift, records the cached-system-block pattern to apply when the endpoint
is built, and instructs the next editor to remove the note once applied.
No implementation change this commit — guidance only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widens AIProvider.generate_json / generate_text / generate_text_stream
signatures to accept `system_prompt: str | list[SystemBlock]`:
- `str` (the existing call shape): passes through uncached, unchanged
behavior. Every existing caller stays on the uncached path — no silent
behavior change.
- `list[SystemBlock]`: enables Anthropic prompt caching via structured
system blocks. Caller-authored `cache_control` is honored verbatim
(policy α); if no block carries it, the provider applies
`cache_control: {"type": "ephemeral"}` to the first block only.
Gemini ignores cache_control and concatenates list entries into one
system string — the widened signature is strictly additive on that path.
Adds `anthropic.cache` structured-log telemetry: on every Anthropic
response (streaming included, via `stream.get_final_message()`), logs
`cache_read_input_tokens` and `cache_creation_input_tokens`. Telemetry
failure in streaming is swallowed so the user-facing stream never breaks.
Verification deferred: cannot run from code-server (no Python, no DB,
no dev env). TODO(phase0-verify) left inline in the module docstring.
First verification task on the new dev environment is to hit any
FlowPilot endpoint twice within 5 minutes and confirm the second call
shows cache_read_input_tokens > 0 in the `anthropic.cache` log event.
If verification fails, that's a debug task on the new env — not a
blocker for continuing Phase 0.2/0.3/0.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emits structured `mcp.turn` log events on every Anthropic-path chat turn,
capturing whether MCP was wired in (mcp_available), whether the model
actually invoked an MCP tool (mcp_invoked), which tool names fired,
and whether the silent retry-without-MCP fallback was triggered.
Adds a separate `mcp.fallback` event with error type/message for
fallback occurrences.
Establishes baseline data for deciding whether MCP investment is earning
its keep before Phase 2+ expands the product footprint. Scope: the one
MCP-using code path (`_call_anthropic_cached`) — not a general
instrumentation layer.
No new dependencies, no schema changes, no behavior change. Standard
library `logging` is the sink; PostHog is not wired on the backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the locked FlowPilot migration design onto the branch that will
implement it. Includes the annotated target UI mockups (primary session
view + three Script Generator integration states) and the superseded
FLOWPILOT-AND-RESOLUTIONASSIST.md for historical reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous implementation PATCHed the `resources` string directly, which CW
silently ignores because `resources` is a server-derived read-only field (it's
populated from schedule entries of type/id=4, not freely writable).
Per CW docs (openapi line 70949): "Please use the
/schedule/entries?conditions=type/id=4 AND objectId={id} endpoint".
Behavior per spec:
- No owner + assign user → set owner (existing behavior kept)
- Has owner + assign different user → POST /schedule/entries with type/id=4,
member, objectId; owner untouched
- User already assigned (owner or schedule entry) → idempotent no-op
- Remove owner → clear owner (existing behavior kept)
- Remove co-assignee → DELETE /schedule/entries/{entry_id}
- list_resources now merges owner + schedule-entry members, deduped by id
Required CW security role permission on the API member:
- Service > Resource Scheduling > Add/Inquire/Delete
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous `resources`-string PATCH was silently ignored by CW — the
`resources` field is server-derived from the ticket's owner + schedule
entries, not freely writable. Status PATCH could also silently no-op
when a cross-board status id was sent.
- add_resource: when the ticket is unassigned, set the `owner`
MemberReference (the canonical writable primary-assignee field).
If already owned by someone else, append the identifier to the
`resources` co-assignee string best-effort.
- remove_resource: clear `owner` (with remove→replace:null fallback) if
the target is the current owner, otherwise strip from `resources`.
- list_resources: merge owner + resources string, deduped by member id,
so the UI reflects both single-owner and multi-resource assignments.
- update_ticket_status: verify CW applied the status by comparing the
response body's status.id — raises PSAError with a clear message when
CW silently rejects the change (e.g., status invalid for ticket's
board), instead of reporting spurious success.
- Frontend: surface the backend error detail in the toast so users see
the real reason instead of a generic "Failed to update" message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Status update was returning only new_status (string) and the parent list's
onStatusUpdated only set status_name. The <select> was bound to status_id,
which never changed — so it visually reverted to the old status even though
the PATCH succeeded.
- Backend: include new_status_id in the status-update response.
- Panel: own currentStatusId/currentStatusName state so the select reflects
the change immediately and survives stale parent snapshots.
- Parent list: update status_id on both the row and selectedTicket so the
list row stays in sync when the panel stays open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Status filter: aggregate statuses across all boards (deduped by name)
when no board is selected. Backend accepts status_name and filters by
status/name so the same status matches across boards.
- Resource assignment: CW has no /service/tickets/{id}/members endpoint —
assignees live in the ticket's comma-separated `resources` string field.
Rewrote list/add/remove to read/PATCH that field via member identifier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Apply company_id filter in CW search_tickets conditions (was silently ignored)
- Sanitize query string to strip single quotes before CW condition interpolation
- Add psaError state to TicketsPage for permissions error surfacing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GET /boards/{board_id}/statuses endpoint — direct board-to-statuses lookup
without ticket roundabout; used by filter bar and new ticket form
- Fix TicketsPage and NewTicketModal to call getBoardStatuses(board_id) instead
of misusing getTicketStatuses(ticket_id) with a board_id value
- Fix list_members auth: was require_account_owner (owner/super_admin only) —
changed to require_engineer_or_admin so engineers can see member list for
ticket assignment
- list_members: return [] on PSAError instead of 502 (Lesson 111 pattern)
- get_ticket_statuses: return [] on PSAError instead of 502
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- list_resources: return [] on PSAError instead of 502 — stops global interceptor
toast when CW API key lacks ticket members permission (Lesson 111)
- list_boards/list_priorities: add warning logging so Railway logs reveal the
root cause when CW permissions are missing
- TicketsPage: derive board options from ticket search results when listBoards
returns empty (CW permissions fallback)
- TicketFilterBar: replace assignment <select> with searchable member picker —
fixed options (All/Mine/Unassigned) + text-filtered member dropdown
- TicketQueue: remove Load More / infinite scroll; page now exists at /tickets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TicketDetailHeader: Display ticket info with status dropdown
- TicketNotesFeed: Chronological list of ticket notes with internal flag
- TicketAddNote: Form to add notes (requires linked session)
- TicketConfigs: Display related configurations/devices
- TicketRelated: List of related tickets as clickable buttons
All components use type-safe imports from psaContext and integrations APIs.
Styling follows design system (flat dark theme, electric blue accent, Tailwind v4).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Tickets page route to router with lazy loading and code splitting.
Add Tickets navigation entry to sidebar in RESOLVE section for both
icon rail and pinned layouts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create frontend/src/api/tickets.ts with ticketsApi (resources, status, create, ai-parse, priorities, search)
- Update integrationsApi.searchTickets and searchTicketsQueue return types from PSATicketSearchResult[] to TicketListResponse
- Fix TicketQueue.tsx to use results.items (append/set) and results.items.length for pagination check
- Fix TicketPickerModal.tsx to use results.items when setting search results
- Export ticketsApi from api/index.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace false claim about linkedTicket state with explicit fetch step on modal open
- Remove MyQueueWidget references; TicketQueue is the existing component being updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Explicitly call out search_tickets breaking change and all existing callers
- Fix [ACTIONS] marker to use JSON array format matching existing parser
- Route system prompt change to assistant_chat_service.py, not flowpilot_engine
- Pivot detail panel hydration to existing getTicketContext + listResources
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- flowpilot_engine: pass account_id at all 5 AISessionStep instantiation
sites (_create_step_from_parsed x3, briefing step, status update step).
Phase 4 RLS blocked every INSERT with NULL account_id — this broke all
new FlowPilot sessions since the Phase 4 migration was applied.
- integrations: list_boards returns [] on PSAError instead of 502, stopping
the spurious 'Server error' toast on dashboard load (boards are optional).
- client.ts: 5xx global toast now shows backend detail when available.
- useFlowPilotSession: startSession extracts backend detail for error state;
suppresses duplicate toast for 5xx (global interceptor already handles it).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CW resources field is a plain string of member identifiers (login names),
not a navigable object. resources/member/id was invalid syntax causing 403.
Now resolves the CW member identifier from the cached member list and
uses: resources contains '{identifier}' which is the correct condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Requires CW API member security role to have All scope on Service Tickets.
owner/id was incorrect for workflows using resources-based assignment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>