Session Helper
The session helper answers questions over the sessions and project knowledge you can already read: “find the session where we fixed the SSO redirect”, “what did we decide about Redis and why?”, “summarize these three sessions”. It searches sessions, reads summaries and excerpts, searches project knowledge and finds sessions that touched a file, then answers with citations back to the sessions and messages it used.
It sees exactly what you can see: your own sessions, the projects you can access, and your org memberships. An org admin doesn’t get a wider view through it.
For questions about SessionFS itself — how to set up Jira, what Auto Narrative does, which commands and settings exist — it searches the official docs instead of guessing. Those answers cite the docs pages and sections they drew from alongside the session citations, and each link opens the page in a new tab.
Available on the Enterprise plan. Added in v0.23.0.
Requirements
Section titled “Requirements”- Enterprise plan. A project-scoped question uses the plan of the project’s org, and a group-scoped question the plan of the group’s org; otherwise your own plan. Other plans get
upgrade_required. - A model key you supply. SessionFS never supplies one. For each question the helper uses, in order:
- your own key, from Settings → Data protection → Security review model;
- your org’s helper key, when an org owner or admin has set one and switched it on;
- otherwise the request returns
409 helper_key_required.
Answers and audit rows record which of the two was used (user or org), never the key.
Supported providers
Section titled “Supported providers”The same provider list powers the security review, the helper, summaries and Jira drafting (v0.24.0): Anthropic, OpenAI, Google Gemini, OpenRouter, DeepSeek, Kimi (Moonshot), xAI (Grok), Mistral, Z.ai (GLM), Qwen (Alibaba Cloud), MiniMax, Groq, Together AI and Fireworks AI. Pick the provider, paste your key and choose a model: Fetch models lists the models your key can use, and any model id can be typed in. A custom OpenAI-compatible endpoint URL (LiteLLM, vLLM, Ollama, or a regional endpoint) replaces the provider default. A saved key is only ever sent to the provider or endpoint it was saved with; changing either requires entering the key again.
Gateways that reject temperature, top_p or reasoning settings for a reasoning model are handled automatically: the request is retried once without them, and that is remembered for the endpoint and model.
Where to use it
Section titled “Where to use it”-
Dashboard. Ask the helper in the sidebar or the Cmd+K palette opens a drawer. The same button on a project or session page scopes the question to it, and the drawer’s scope menu also lists the project groups you can see. The drawer remembers the scope you last picked for the rest of the browser tab, so a new conversation starts where you left off. The memory belongs to the account you’re signed in with: signing in as someone else in the same tab starts from All my sessions. Answers stream in with citations; a citation opens the session at the exact message and highlights it (
/sessions/<id>?msg=<n>). The composer stays pinned at the bottom, and earlier threads and New are always at hand. Retrying a failed answer doesn’t save your question twice.The expand button in the drawer header switches between Small (the default) and Large, a much wider panel for reading long answers, tables and code. The browser remembers your choice. On a phone the drawer is always full screen, so the button isn’t shown.
To rename a thread, open the history, choose Rename next to it, type the new title and press Enter (Escape cancels; clicking away saves). Titles are up to 200 characters and are DLP-redacted like your questions. Renaming doesn’t move a thread in the list, which stays ordered by last activity (v0.29.11).
Ticket and wiki citations become links as soon as the answer finishes, without reloading the conversation.
Ids in an answer are clickable. A ticket id (
tk_…) opens a preview right over the conversation, with its status, priority, assignee, project, description and latest comments, plus Open ticket for the full view. Project (proj_…) and session (ses_…) ids open their pages, and when the scoped project has a single GitHub-verified repo,PR #123and commit SHAs link to GitHub. Ids inside code blocks stay as text. -
CLI.
sfs helper ask "Where did we fix the SSO redirect?"streams the answer to the terminal;--project,--group(a group idpg_…or slug) or--sessionscope it.sfs helper conversations list|show|rename|deletemanages threads, andsfs helper statusshows whether the helper is available and which key it would use. See the CLI reference. -
MCP. The
ask_session_helpertool returns the whole answer with its citations, so an agent can ask without shelling out. It takesproject,group_idorsession_idsto scope the question. See MCP Server.
Scope a question to a project group
Section titled “Scope a question to a project group”A question can cover a whole project group instead of one project (v0.28.0). Pick the group in the drawer’s scope menu, run sfs helper ask --group SLUG "...", or pass group_id to ask_session_helper. A question takes one scope: a project, a group or a list of sessions.
- Only what you can read. The helper searches sessions, knowledge, tickets and wiki pages from the group’s member projects that you can read, and nothing else. With restricted project visibility, a member project you can’t read stays out.
- The group’s overview. When the group has a compiled overview and you can read every project that went into it, that overview is added to the prompt, redacted with your DLP policy like everything else sent to the model. If you can’t read one of those projects, the overview is left out rather than sent in part.
- Plans. Project groups need the Team plan or above on the group’s org, and the helper needs Enterprise.
- Not found. A group that doesn’t exist and a group you can’t see both return
404 group_not_found.
Actions
Section titled “Actions”Besides answering, the helper can propose these changes for you:
- Turn Auto Narrative on or off for one or more projects.
- Move sessions to Trash, for example to clean up old scratch sessions or “every session that used DeepSeek”.
- Create a ticket with a title, description, acceptance criteria, priority and optional assignee. When you ask for it, the same confirmation also publishes the new ticket to Jira (Enterprise, with a Jira mapping for the project). You can name a different Jira project (one mapped to a SessionFS project you can edit), a story template (pinned to its current version until you confirm) and an active or future sprint (v0.28.0).
- Assign a ticket to a persona or directly to a teammate. You can name a persona, a teammate’s email, or a display name; a display name goes to the persona that represents that person when there is one, otherwise to the person.
- Comment on a ticket with a plain user comment, optionally sent to the linked Jira issue too when the org forwards comments to Jira.
- Link a ticket to an existing Jira issue by its key (v0.27.0).
- Publish existing tickets to Jira (“turn these tickets into Jira issues”): one proposal per ticket that has no Jira link yet, with the same optional Jira project, template and sprint choices as creating a ticket. On Confirm the ticket is published exactly like Promote to Jira on the ticket page; a ticket that is already linked is refused and the helper offers to overwrite the linked issue instead (v0.29.10).
- Set a reminder for you (“remind me to book a follow-up with Tolu on Thursday”), optionally tied to a meeting, meeting item or ticket you can read. The card shows the absolute date and time in your timezone (for example Thu 6 Nov 2026, 09:00 Europe/London). A helper reminder is always for you, never for someone else; the helper can also list your reminders with
list_my_reminders. See Reminders. - Save a pasted meeting transcript (“save this meeting and pull out the action items”, with the transcript pasted in the same message). The helper proposes saving it as a meeting in the conversation’s project or group; the text is taken from your own message (as stored, already DLP-redacted), never retyped by the model, and it can leave out an instruction line above the transcript. On Confirm the meeting is saved with the same rules as Meetings → Add meeting (edit access, the group’s plan, DLP with block-mode refusal, the 512 KB / 10,000-line caps, caption timing stripped) and, unless you asked otherwise, its action items are extracted with your AI key; the card links to the meeting and says why extraction didn’t run if it couldn’t. A helper message holds up to 20,000 characters, so add longer transcripts from the Meetings tab. Only available in a project- or group-scoped conversation (
propose_meeting_upload). See Meetings. - Overwrite one side of a linked ticket and Jira issue: replace the Jira issue’s summary and description with the ticket’s, or the ticket’s title and description with the issue’s. See Link an existing issue.
Jira proposals are refused up front when they can’t work, for example when the ticket has no Jira link, you can’t edit it, or the org doesn’t forward comments to Jira. Every Jira condition is checked again when you confirm. If Jira times out, the card never says nothing changed: the issue may still have been created or updated, so it points you to the ticket’s Jira link (v0.29.10).
Each proposed field cites where it came from, with the same markers as answers: a session, ticket, wiki page, docs page, or a meeting or chat range ([meeting:<id> L<a>-<b>], [chat:<id> M<a>-<b>]) that a tool showed earlier in the conversation. A meeting or chat source is checked like a citation: you can still read it in this scope, it is the right kind, and the range is inside the transcript (v0.29.10).
The helper never makes a change itself. Each proposal appears in the answer as a card listing every target and what will happen to it: will change, unchanged, or skipped with the reason. Nothing changes until you press Confirm (or Move to Trash); Cancel drops it. Proposals expire after 15 minutes.
Confirming applies exactly the targets on the card, with your own permissions. Access is checked again for each target when you confirm, and the card shows which targets applied and which failed. Turning Auto Narrative on or off needs change rights on the project (its owner or a member of its organization); a project you can only view is skipped as You can view but not edit this project.
Ticket cards are editable before you confirm: the create form’s title, description, acceptance criteria, priority and assignee are pre-filled from the helper’s draft and can be changed, and the comment form’s body can be edited the same way. Confirm creates the ticket or comment under your own name and links the new ticket from the card. A ticket a captured-session reader creates starts suggested so a project editor can approve it; a helper comment is always a plain comment, never a trusted review verdict.
To trash sessions by model, provider, project, tool or date range, the helper hands those filters to the server, which picks the matching sessions itself — it never trashes sessions just because their text mentions a model. One proposal covers at most 100 sessions, the most recently active first. When more match, the helper says how many are left; confirm, then ask again for the next batch.
Cleanup is reversible: sessions go to Trash. Restore them from Sessions → Trash within 30 days; after that they can be purged for good.
What it can read
Section titled “What it can read”Sessions by model
Section titled “Sessions by model”Each session records the AI model it ran on, as reported by the tool that captured it. Ask “which models do my sessions use?” or “show my sessions that used DeepSeek” and the helper:
- lists the models across your sessions with
list_session_models: session counts, the capturing tools, and first and last dates; - filters the session tools (
search_sessions,list_recent_sessions,sessions_touching_path) bymodelorprovider. Matching ignores case and catches a whole family, sodeepseekfinds bothdeepseek-v4-proanddeepseek-flash. Several values match any of them, andunknownfinds sessions with no recorded model. These sessions are grouped the same way as in the dashboard’s model filter; - narrows by date with
days,sinceoruntil(last activity, in UTC).search_sessionsdoesn’t need a keyword when a filter is given.
Every session line shows its model, for example [claude-code] model: deepseek-v4-pro. A Claude Code session can show a DeepSeek model when Claude Code was pointed at DeepSeek. Long lists come in pages (up to 100 sessions, or 20 for a keyword search) that start with “Showing 1–100 of 240”; the helper pages through with offset. In a conversation scoped to one project, the helper can’t reach another project’s sessions: it tells you to start an unscoped conversation instead.
Who is asking (v0.29.0)
Section titled “Who is asking (v0.29.0)”The helper knows who you are, so “my tickets” and “what is assigned to me” work. A name is looked up as a person, a persona or a project group (by name, slug or abbreviation), and tickets assigned to either a persona or a person are found. It still reads only what you can already see.
Project work items
Section titled “Project work items”Beyond sessions and project knowledge, the helper reads the project’s own work items and people — still only what you can already see:
-
Tickets —
list_tickets/get_ticket. Work items (ids liketk_…), with status, priority, assignee, project, acceptance criteria, dependencies and the last 10 comments. -
Wiki pages —
list_wiki_pages/get_wiki_page. A project’s knowledge pages, with the body capped at 20k characters. -
Agent runs —
list_agent_runs/get_agent_run. CI review runs, with persona, status, severity and findings. -
Personas —
list_personas. The project’s own personas and the org personas visible from it, with role and active state. -
Jira issues —
get_jira_issue. The Jira issue linked to a ticket: summary, status, assignee, priority, labels, components, last update and the latest comments, read through the org’s Jira connection. It takes a ticket id, never a free-form Jira key, so it can only read an issue a ticket you can see already points at. Comments that came from Jira are marked as such onget_ticket. -
Meetings —
search_meetings,list_recent_meetings,get_meeting,list_my_meeting_actions. Stored meeting transcripts: search titles and text, list the newest, read one as numbered lines (L12| …, at most 400 lines per call, with a range to read on) together with its participants and extracted action items and decisions, and list the pending action items owned by you or by a persona that represents you. The same tools read chat threads (kind=meeting,chatorall, defaultall): a thread is shown as numbered messages (M12| [time] author: text), andlist_my_meeting_actionslists your conversation actions from meetings and chat threads alike. Scope is a hard filter: a project-scoped question reads that project’s meetings only; a group-scoped question reads the group’s own meetings plus the meetings of its member projects you can read; a session-scoped question reads the meetings of the sessions’ projects; an unscoped question reads every meeting you can read. Group meetings also need standing on the group and a plan with project groups. Transcript text is untrusted data: it is already DLP-redacted when stored, and the helper treats instructions inside it as text. -
What needs you —
list_my_attention. Your For You list (bandsnow,your_turn,upcoming,lately), the same items the dashboard, MCP and the CLI return. Ask “what needs my attention?”. In a project or group conversation it keeps only items inside that scope.
These tools follow the same plan gates as the matching pages: tickets and agent runs need the project’s plan to include them, and personas need it to include personas. On a project whose plan doesn’t, the helper says they aren’t available instead of reading them. Long fields are capped with a visible “…[truncated]” (ticket descriptions, each acceptance criterion, run summaries and finding messages), and one tool result is capped at 40,000 characters.
In a conversation scoped to a project, nothing from any other project comes back. A session counts as the project’s when it is attributed to it, or when it has no project and sits on one of the project’s repositories. A session attributed to another project stays out, even on a shared repository.
One scope per conversation
Section titled “One scope per conversation”A conversation stays in the scope it started with: one project, one group, one set of sessions, or an unscoped conversation in one organization. Asking in a different scope with the same conversation id is refused with 409 scope_mismatch; the dashboard starts a new conversation when you change the scope.
An unscoped question reads one organization: the organization the conversation belongs to, or for a new conversation your default organization (pass scope.org_id to pick another organization you belong to). It covers that organization’s projects, groups, meetings, tickets and your sessions in them, plus your personal projects and sessions that belong to no project. It never returns another organization’s data, and the AI key and settings come from that same organization. If you belong to several organizations, start a separate conversation for each. A list of sessions that mixes an organization’s sessions with sessions from personal projects (or from two organizations) is refused with 400 scope_mixed_orgs.
How answers stay grounded
Section titled “How answers stay grounded”- A citation is kept only when it points at something a tool returned in this conversation: a session a tool listed or read, a message number (
msg:<n>) a tool actually showed for that session, a docs page or section a docs tool returned, and tickets, wiki pages, meetings and chat threads the tools returned. Every other marker is removed, and markers are checked before they are streamed, so an invalid one never appears. - An answer that cites nothing although its tools returned results ends with “(No sources cited for this answer.)”.
- Earlier answers in the thread are passed back to the model labelled as earlier replies that are not evidence, and failed tool calls stay marked as failed.
- When the helper runs out of lookup rounds it makes one last call without tools to answer from what it already found and say what it could not check; if that fails too you get “I couldn’t finish looking this up” instead of an empty answer.
- Lists and searches say whether they are complete or truncated (for example
[returned 20 of 57 knowledge entries; truncated]), and a session whose stored messages can’t be read right now says the archive is unavailable rather than “no excerpts”. The helper only says something doesn’t exist after a complete search found nothing; otherwise it says what it checked. - A ticket shows when its description was written (“may be out of date”) and the current status of every ticket the description, criteria or comments mention, so the helper reports current state from status fields and dates rather than from older prose. Compiled project and group context shows when it was compiled and whether it is more than 7 days old.
How answers show their sources
Section titled “How answers show their sources”Each result a tool returns is numbered for the model ([evidence e12] on its first line). The model puts those numbers after every sentence that states a fact, and the server checks each number against the results this conversation actually returned. You never see the numbers:
- A number that points at one ticket, wiki page, session or docs section becomes that item’s normal source chip.
- A number that points at a list or a search (for example “the 20 most recent sessions”) stays as a quiet chip that names the lookup.
- A sentence that states a fact (a number, date, id, name or status) with no source behind it is marked: in the dashboard it gets a dotted underline with “No source was found for this sentence” when you hover it; in the CLI and API its text ends with “(not verified)”.
- Greetings, questions and sentences about what the helper will check next are never marked.
- If the model doesn’t use the numbers at all, the answer is still shown. The older checks then apply: invalid markers are removed and an answer citing nothing ends with “(No sources cited for this answer.)”.
Answers are streamed one sentence at a time, so each sentence arrives with its sources. The final text (with any “(not verified)” marks) arrives once the answer is complete.
The grounding setting turns this on or off: strict (the default) or off (markers only, as before). See Configure the assistant.
Docs and your plan. A docs result also says whether the feature it describes is available to you, for example Availability: Enterprise plans; your plan: Team (not available); cloud and self-hosted. The plan is the one for the conversation’s scope (the scope org’s plan, or your own), and “cloud only” or “self-hosted only” pages say so on the other kind of install. Pages without a recorded availability say so instead of guessing.
The helper may cite a ticket or wiki page only after one of its tools returned it in that question, and only when it belongs to a project you can read inside the conversation’s scope; any other [ticket:…] or [wiki:…] marker is removed from the answer.
A meeting passage is cited as [meeting:<id> L<first>-<last>]. The marker is kept only when a tool returned that meeting earlier in this conversation (this question or a previous one in the same thread), you can still read it in the conversation’s scope, and the line range lies inside the transcript (1 <= first <= last <= line count); anything else is removed from the answer. In the API a kept citation is {"kind": "meeting", "meeting_id", "line_start", "line_end", "title", "project_id", "group_id", "at"}, and a streaming turn sends it as a meeting_citation event. Chat messages are cited as [chat:<id> M<first>-<last>] under the same rules (the tag, id and range letter must agree with the stored thread); a kept chat citation has "kind": "chat" plus channel_name, and streams as a chat_citation event.
Every line a tool returns carries a source date as YYYY-MM-DD (Nd ago). When the newest source for a claim is more than 7 days old, the helper says so with “As of <date>, …” so you can see the information may be stale.
Org AI key
Section titled “Org AI key”Enterprise org owners and admins can set one provider, model, API key and optional base URL for members who don’t have a key of their own: Organization → Policies → AI key for the session helper, or GET/PUT/DELETE /api/v1/orgs/{org_id}/llm-settings.
- The key is encrypted at rest and never shown again after saving; only its last four characters are displayed.
- It can be switched off without deleting it.
- A custom base URL goes through the same address checks as other user-supplied LLM endpoints.
- Fetch models lists the models the key can use, the same way as the personal key form (v0.28.0). When the provider can’t list them, type the model id yourself. Model lookups are rate-limited per user (
429 models_rate_limited).POST /api/v1/orgs/{org_id}/llm-settings/modelsdoes the same. - Test connection sends one tiny chat request with the form’s values (the stored key only when the provider and base URL are unchanged) and shows the endpoint, model and latency, or why it failed.
POST /api/v1/orgs/{org_id}/llm-settings/testdoes the same. - Leave the base URL empty to use the provider’s own API. It must be an API address, not a console:
https://platform.deepseek.comis refused with “use https://api.deepseek.com”. Saving a new base URL runs the test first; a timeout or rate limit still saves, with a warning.
Configure the assistant
Section titled “Configure the assistant”Model settings live at two scopes — your own (GET/PUT /api/v1/settings/helper) and the org’s (GET/PUT /api/v1/orgs/{org_id}/helper-settings; owners/admins write, members read the effective values) — and every turn resolves an effective set from them. Each field is optional; anything unset falls back to the default.
| Setting | Range | Default |
|---|---|---|
max_output_tokens |
256–32000 (integer) | 4096 |
temperature |
0–2, or null for the provider default |
0 |
top_p |
0–1 | unset (not sent) |
max_tool_steps |
1–20 (integer) | 8 |
reasoning_effort |
off · low · medium · high |
unset |
answer_style |
concise · detailed |
concise |
grounding |
strict · off |
strict |
grounding: strict asks the model to name the source of every factual sentence and marks the ones without a source (see How answers show their sources); off keeps only the citation-marker checks. It adds no extra model call.
max_tool_steps counts tool rounds: after its last allowed round the model always gets one more step to answer. On Anthropic, setting top_p drops temperature, since the API refuses both together.
Settings are stored apart from the LLM keys: saving them never creates a key, and deleting or rotating a key never touches the settings, instructions or budget.
Plan. The personal routes need the helper on your plan (403 upgrade_required otherwise); the org PUT needs the org’s Enterprise plan. The org GET works on any plan and reports plan_includes_helper.
Updating. PUT merges: a field you leave out keeps its value and an explicit null clears it, and inside settings each key merges the same way. {"monthly_token_budget": 2000} changes only the budget; {"instructions": "…"} leaves the budget and settings alone. For temperature, top_p, reasoning_effort and the ceilings, null is itself the value (provider default, not sent, no ceiling).
Precedence. A turn using your own key uses your personal settings, with defaults for anything unset. A turn using the org key uses the org’s settings, which the org can additionally cap with two ceilings (ceiling_max_output_tokens, ceiling_max_tool_steps). Ceilings only apply on the org-key path; a personal-key turn is governed solely by your own settings. The range bounds above are absolute caps on every path, and GET /api/v1/helper/status reports the effective settings for the key a turn would use.
Custom instructions. Org instructions allow up to 4000 characters, personal instructions up to 2000.
- Org instructions apply when the turn uses that org’s key, and when the conversation is scoped to that org’s project (or its projects’ sessions), whichever key is used.
- Instructions from an org you are not an active member of never apply.
- Your personal instructions always apply, after the org block.
Instructions are not secret: any member can ask the helper to repeat them, and when a member uses their own key the org’s instructions go to that member’s provider. Never put credentials or confidential information in them.
The prompt always starts with the helper’s fixed rules; each instruction block follows between <org-instructions> / <personal-instructions> tags, under a header stating it cannot override those rules (access, scope, citations, treating tool output as data, confirming before acting). Saving text that contains those tags fails with 422 instructions_invalid. The text is control-stripped and DLP-redacted before it is stored, the length limit applies after redaction (422 instructions_too_long), and a failed DLP scan is refused with 422 instructions_redaction_failed. Each turn redacts the stored text again under that turn’s DLP policy.
Usage and budget. Usage is recorded in an append-only ledger, one row per turn that used any tokens — including turns that end in a disconnect, a provider error, the deadline or the step limit. Budgets and usage figures read only this ledger, so deleting a conversation or the retention sweep never changes them; ledger rows are kept 13 months. An org admin can set monthly_token_budget and read a per-member summary for the current UTC calendar month; members see only their own usage. Only turns using the org’s key count against its budget, and once it is reached the next turn fails with 429 helper_budget_exceeded. The budget is checked before each turn, so turns running at the same time can each overshoot it by up to about one turn’s maximum tokens. GET /api/v1/settings/helper reports your own month’s usage.
Every settings change writes an audit row carrying the instruction text’s length and sha256 only, never the text.
User API keys only; service keys get service_key_not_allowed. Each user can run 2 questions at a time per server process (409 helper_busy). An organization can have 8 questions running at a time across all servers by default; beyond that a question gets 429 org_concurrency_limit with Retry-After (see Tenancy and Isolation).
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/helper/turns |
Ask a question. Server-Sent Events by default; one JSON document with "stream": false |
| GET | /api/v1/helper/conversations |
Your threads, newest first |
| GET | /api/v1/helper/conversations/{id} |
One thread with its messages |
| PATCH | /api/v1/helper/conversations/{id} |
Rename a thread: {"title": "…"} (v0.29.11) |
| DELETE | /api/v1/helper/conversations/{id} |
Delete a thread and its messages |
| GET | /api/v1/helper/actions/{id} |
A proposed change with its targets and state |
| POST | /api/v1/helper/actions/{id}/confirm |
Apply a proposed change (access re-checked per target) |
| POST | /api/v1/helper/actions/{id}/cancel |
Drop a proposed change |
| GET | /api/v1/helper/status |
Whether the helper is enabled for you, which key source, provider and model it would use, the effective settings, and whether org/personal instructions are set (never their text) |
PATCH /api/v1/helper/conversations/{id} answers with the same object as a list item. Only the thread’s owner can rename it; a missing thread, someone else’s, or an org thread after you leave the org all get the same 404. Whitespace is collapsed and control and bidirectional-override characters are dropped; an empty title is 422 title_required and one over 200 characters is 422 title_too_long. The title is then DLP-redacted under the same policy as a question in that thread, and if redaction can’t run the rename is refused (422 title_unscannable) rather than stored unchecked. updated_at doesn’t change.
Request body for POST /api/v1/helper/turns:
{ "conversation_id": "hc_…", "message": "What did we decide about Redis and why?", "scope": { "project_id": "proj_…" }, "stream": true}conversation_id and scope are optional. scope takes at most one of project_id, group_id (v0.28.0) or session_ids (up to 100 ids); an unscoped question may instead pass org_id to choose which of your organizations it reads (see One scope per conversation). Set "retry": true when re-asking after a failed answer: if the thread’s last question is still unanswered and identical, it is reused instead of being stored twice. When streaming, the events are conversation (always first), text, tool_call, tool_result, citation, doc_citation, ticket_citation, wiki_citation, grounding, action_proposed, done and error. grounding (sent before the citations when the answer used evidence numbers) carries the final answer text, mode, unverified_count, sentences and evidence (described below); clients should show its text in place of the streamed text. Every citation carries kind (session, doc, ticket or wiki); session, ticket and wiki citations also carry at, the source’s last update as an ISO timestamp. A keep-alive comment is sent every 15 seconds. If the client disconnects, the model call is cancelled and the partial answer is kept.
With "stream": false the response is:
{ "conversation_id": "hc_…", "answer": "…", "citations": [ { "kind": "session", "session_id": "ses_…", "message_index": 0, "title": "…", "at": "2026-10-20T09:12:00+00:00" }, { "kind": "ticket", "ticket_id": "tk_…", "project_id": "proj_…", "title": "…", "at": "…" } ], "grounding": { "mode": "strict", "unverified_count": 1, "sentences": [ { "start": 0, "end": 41, "status": "supported", "evidence": ["e3"] }, { "start": 42, "end": 90, "status": "unverified", "evidence": [], "flag": [74, 89] } ], "evidence": [ { "id": "e3", "tool": "get_ticket", "args": { "ticket_id": "tk_…" }, "at": "…", "marker": "[ticket:tk_…]" } ] }, "tool_calls": [{ "id": "…", "name": "…", "args": {} }], "usage": { "input_tokens": 0, "output_tokens": 0 }, "stop_reason": "stop", "key_source": "user"}grounding is null when the setting is off or the model didn’t use evidence numbers. start/end are offsets into answer in UTF-16 code units (JavaScript string positions); flag is the range of the “ (not verified)” text. Conversation messages (GET /api/v1/helper/conversations/{id}) carry the same grounding object on answers.
Provider failures return 502 provider_error with a sanitized message. When the failure has a known cause, reason names it (for example endpoint_not_api when the base URL is a provider’s console, not its API, and the message names the right address).
Retention
Section titled “Retention”Threads are private to the user who started them. Helper messages are deleted 90 days after they were written by default. An org can set 1–3650 days with helper_retention_days on PUT /api/v1/orgs/{org_id}/settings; an explicit null returns it to the default. Threads without an org use the 90-day default.
The token-usage ledger is kept 13 months: the same sweep deletes ledger rows older than 400 days, whatever the org’s message window.
Deletion runs from the sweep POST /api/v1/admin/sweeps/helper-retention (a platform admin or a platform_sweeps:run service key). On self-hosted servers the Helm chart can schedule it; see Self-Hosted.
Data protection
Section titled “Data protection”- Every excerpt, summary and knowledge text sent to the model is redacted with your org’s DLP policy (the default policy for personal scope). When several orgs are involved, the strictest policy applies.
- The helper’s tools check your access on every call, and a thread bound to an org re-checks your membership on every turn.
- Messages are stored redacted.
- Each question writes one retrieval-audit context and, per tool call, one event for each project the call read from, so every project’s audit trail shows what the helper took from it. A call that returned nothing from a project writes no event for that project, and repeating an identical call in the same question writes nothing new. That way an org can answer “what did the helper send to the model?”
- Logs record ids, token counts and timings, never prompts, excerpts or keys.
Proxies
Section titled “Proxies”Answers stream as they are written. The Helm chart’s dashboard nginx already passes /api/v1/helper/turns through unbuffered with a 600-second timeout. If you run another proxy in front of SessionFS, turn off response buffering for that path.