Skip to content

In-session feedback capture: an owner's complaint should land as a durable, turn-anchored record — today it lands in memory or nowhere, and the turn it is about is not addressable #514

Description

@serge-ivo

The owner's request

"User should be able to give feedback to an agent during the session and it should be recorded by
the agent into a separate storage with that timestamp. Should appear in feedback page/tab.
Basically we have to be able to in real-time capture what is wrong with agent so later when we
improve it it can be related to the conversation to get full picture so we could file the tickets
better like we did now."

"Like we did now" is the acceptance test. #503, #504, #505 and #510#512 were produced last
night by reading transcripts and traces by hand and correlating them to code. This feature succeeds
if it makes that correlation a click, and fails if it only collects opinions.


1. What "like we did now" actually required — the evidence audit

I read the four issues to find out what turned out to be load-bearing, because that is the field
list, and nothing else belongs in it.

Issue What made it filable
#503 The verbatim exchange across two days, and which tool was called on the turn that finally worked (list_subordinates, not subordinate_status)
#504 One trace_id (2ab928b6…) and the per-step messages with timestamps — 11 of 19 empty
#505 The two owner turns bracketing a claim, with their timestamps, proving the absence of a third
#510#512 The audio clip and what the recognizer heard vs what reached the agent

So a feedback record must carry, at capture time: the instance, a precise timestamp, the message it
is about (and the turn before it), the trace_id that reaches the tool calls, the coding session
if there is one, and — for a voice turn — the audioKey and the dictation. Everything else is
decoration.

Two of those are pure snapshots (message text, preceding turn) and two are pointers (trace_id,
audioKey). Section 4 explains why both kinds are needed and neither is sufficient.


2. Verified: nothing like this exists today

grep -rin "feedback" --include=*.ts --include=*.tsx --include=*.sql platform/

returns 30 hits, none of them user feedback: the browser runner's form-validation write-back
(packages/browser-runner/src/runner.ts:786-788,1030), UI hover affordance comments
(store/console/src/lib/control-classes.ts:89), and voice-cue prose (packages/sdk/src/voice/cues.ts:10).
grep -rEin "thumbs|rating|upvote|downvote|reaction" finds no rating mechanism on a message
anywhere. gh issue list --state all --search feedback returns nine issues, all about UI
responsiveness (#163, #284) or observability of failures (#424, #291) — none about capturing what
the owner thinks.

There is nothing partial to extend. This is new.

The closest live precedent is the failure mode this feature exists to end: #506, where the owner
asked twice for a bug to be filed and the report ended as
write_memory → fact:pending issue:Heartfull:event link shows ID instead of event name, with a
promise nothing schedules and nothing re-reads. That is what a complaint does today when it has no
home.


3. Verified + measured: the correlation key is broken before we add anything

This is the centrepiece, and it is worse than "not yet wired".

A console chat turn produces two different trace ids, and one of them is null.

workers/api/src/routes/instances-chat.ts:99 mints the turn id after the DO has already
answered:

if (doRes.ok) {
    const turnId = crypto.randomUUID();
    
    await logEvent(c.env, { source: "chat", event: "chat.in",, traceId: turnId, ts: now });
    await logEvent(c.env, { source: "chat", event: "tool.call",, traceId: turnId, ts: now + 1 });
    await logEvent(c.env, { source: "chat", event: "chat.out",, traceId: turnId, ts: now + 2 });
}

Meanwhile the DO's own warn-level events about that same turn use a different source:

  • workers/api/src/agent-think.ts:774chat.truncatedtraceId: delegation?.traceId ?? null
  • workers/api/src/agent-think.ts:908chat.invented_resulttraceId: delegation?.traceId ?? null

delegation.traceId is populated only by the Loop (workers/api/src/workflows/agent-loop.ts:130,
traceId: runId). instances-chat.ts:60-68 passes no traceId in the DO body. So on every
console chat turn, the two events that say "the platform caught its own agent misbehaving" are
written with trace_id = NULL and can never be joined to the chat.in/chat.out pair beside them.

And the transcript is not addressable from the trace at all. AgentMessage
(workers/api/src/agent-types.ts:1-24) has id, role, content, channel, userId,
audioKey, dictation, createdAtno trace id. agent_events (migration 0038) has
trace_id but no message id. The only join available is a timestamp.

Measured, live, this account, just now

Instance 5fab318d… (Coder Lead) — the #503 conversation.

instance_messages:

user      846c1695-57a9-4043-b202-beb5ebe718d7   createdAt 2026-08-11T01:57:51.431Z
assistant 06344814-6e21-40c0-a923-2003608d6511   createdAt 2026-08-11T01:57:59.126Z

agent_trace for the same exchange, trace_id d4bd169f-7d1e-45cb-921e-2828d85d3b64:

chat.in   ts 1786413479223  = 2026-08-11T01:57:59.223Z
chat.out  ts 1786413479225  = 2026-08-11T01:57:59.225Z
assistant message → chat.out :     99 ms apart
user message      → chat.in  :  7,792 ms apart

The chat.in event is stamped with Date.now() after the reply was generated, so the trace's
record of "when the user spoke" is off by the entire turn duration — 7.8 seconds here. Timestamp
matching a user turn to its trace is therefore not merely fuzzy, it is systematically wrong by one
turn
, and with concurrent turns on one instance (#429) a 7.8-second window can contain another
exchange entirely.

Nothing downstream can fix this at read time. It has to be stamped at write time, and it is step 1
of this feature.
It is also worth landing on its own merits: it repairs the orphaned
chat.truncated / chat.invented_result events, which are precisely the rows a future investigator
looks for.


4. Storage — where the record goes, and why not the three obvious places

Not vectors. Retrieval is top-k and fuzzy. A feedback list must be complete and exact; "the
three complaints I made about this agent" is a SELECT, not a similarity search.

Not memory. memoryPrompt is injected into the system prompt on every turn
(workers/api/src/agent-think.ts:304), so a growing feedback log becomes a growing per-turn token
cost — the owner measured his Coder Home memory block at ~19,924 chars ≈ 4,981 tokens/turn (his
measurement, not re-verified by me
). Worse, memory steers: #495 is a live instance still
carrying "write access is not enabled" in the same prompt as "[write — consent GRANTED]". A log of
"you got this wrong" injected every turn produces an agent that apologises instead of one that
works.

Not agent_events, despite the tempting fit. Two verified reasons:

  1. Retention. workers/api/src/lib/events.ts:76-82 prunes opportunistically on ~1% of writes:
    DELETE FROM agent_events WHERE created_at < datetime('now', '-14 days'), and the docstring is
    explicit — "The trace is a debugging aid, not an archive." Feedback is exactly an archive: the
    owner's sentence is "later when we improve it".
  2. It has no status. Feedback has a life cycle (open → filed → dismissed) and a link to the
    issue it became. agent_events is a write-once stream with no mutable column, correctly.

So: a new D1 table, modelled on the two precedents that already answer this shape —
error_log (owner-scoped, append-only, its own read route, workers/api/src/routes/errors.ts) and
board_items (migration 0036: "This table only persists what the automation can't know: the
human's status override"
).

Proposed schema

Take the next free migration number at implementation time and run
node scripts/check-migrations.mjs before committing — 0119 is the highest on main right now
and several agents are landing migrations today; #369's guard exists because exactly this race
produced two 0092_* files.

CREATE TABLE IF NOT EXISTS agent_feedback (
  id            TEXT PRIMARY KEY,
  ts            INTEGER NOT NULL,               -- ms epoch: ordering, and the join axis to agent_events.ts
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  user_id       TEXT NOT NULL,
  instance_id   TEXT NOT NULL,
  author        TEXT NOT NULL DEFAULT 'user',   -- 'user' (console) | 'agent' (record_feedback)
  surface       TEXT NOT NULL,                  -- 'chat' | 'coding' | 'board' | 'apply' | 'other'
  sentiment     TEXT,                           -- 'bad' | 'good' | NULL  (optional, closed set)
  body          TEXT NOT NULL,                  -- what the owner said (<= 4000 chars)

  -- POINTERS: full fidelity while the referents live
  trace_id      TEXT,                           -- agent_events.trace_id for this turn
  message_id    TEXT,                           -- AgentMessage.id in the instance DO
  session_id    TEXT,                           -- coding_sessions.id
  timeline_seq  INTEGER,                        -- coding_timeline.seq (migration 0023 — a stable PK)

  -- SNAPSHOT: survives Clear chat, delete-turn (#342) and the 14-day trace prune
  target_role   TEXT,                           -- 'assistant' | 'user' | 'system'
  target_text   TEXT,                           -- <= 2000 chars of the message complained about
  target_at     TEXT,                           -- its createdAt
  prompt_text   TEXT,                           -- <= 1000 chars of the PRECEDING user turn (#505's evidence)
  context       TEXT,                           -- JSON: {agentSlug, model, engine, audioKey, dictation, appVersion}

  -- TRIAGE
  status        TEXT NOT NULL DEFAULT 'open',   -- open | triaged | filed | dismissed
  issue_url     TEXT,                           -- what it became (#506's github_create_issue closes the loop here)
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_agent_feedback_user_ts     ON agent_feedback(user_id, ts DESC);
CREATE INDEX IF NOT EXISTS idx_agent_feedback_instance_ts ON agent_feedback(instance_id, ts DESC);
CREATE INDEX IF NOT EXISTS idx_agent_feedback_trace       ON agent_feedback(trace_id);
CREATE INDEX IF NOT EXISTS idx_agent_feedback_status      ON agent_feedback(user_id, status, ts DESC);

Pointers AND snapshots, deliberately. A pointer alone dangles: DELETE /v1/instances/:id/messages
removes every message and its R2 audio (workers/api/src/agent-do.ts:855-868), delete-turn removes a
span (:888-901), and the trace self-prunes at 14 days. A snapshot alone loses the tool calls, which
is what #503 and #504 were actually built from. Both, and the read surface renders the snapshot with
a "view full trace" link that degrades honestly when the trace is gone.

No retention sweep on this table. State it in the migration comment so nobody adds one by
symmetry with error_log/agent_events.


5. Fork 1 — who records it: the model, or the UI?

Recommendation: both, with the UI as the load-bearing path and the tool as the conversational
convenience. I agree with the framing in the brief and here is the argument.

The owner's words are "recorded by the agent", which reads as a tool. But the turn on which an
agent is malfunctioning is the turn least able to correctly call a tool about its own
malfunction
, and this repo already contains the proof: #504 — a Pilot that emitted 11 empty
tool_use blocks out of 19 would not have emitted a correct record_feedback call either; and
#503 — an agent whose tool results were being silently truncated could not even report the
truncation as a fact. A capture path that runs inside the failing turn is a capture path that is
absent exactly when it matters.

So:

The tool must stamp the SAME row shape, with author='agent', trace_id from
RegistryToolCtx.traceId (workers/api/src/lib/connectors/types.ts:42, already threaded), and the
model's own paraphrase in body — not a summary of what it thinks the problem is. create_ticket
(workers/api/src/lib/tool-registry.ts:365) is the template: tier: "base", deferred import,
owner-scoped from ctx.instanceId/ctx.userId.

What would change my mind: if the owner would rather have zero new chrome on the message bubble
(section 7 shows it is crowded), the tool alone plus a composer-level /feedback command is
defensible — but then the capture rate depends on a working model, and the first feedback we lose
will be about the model not working.


6. Fork 2 — feedback is a bug report, NOT a correction the agent applies

This is the boundary that must be stated in the code, not just decided here, because an
implementer who blurs it builds a second, worse memory system — and this repo has already paid for
that twice (#226 stored preference:response_style in memory; #495 is memory-rot steering a live
agent today).

Three destinations already exist and none of them is this one:

The owner says Goes to Where
"be less technical", "stop ending with a question" Behaviour set_behaviour, workers/api/src/lib/agent-behaviour.ts
"always check the CI before you say it's deployed" Rules & Tips agent_instances.config.specialInstructions
"the admin app is the one with the version gate" Memory write_memory
"you just told me I chose that — I never said it" Feedback this table

Feedback is the class where the platform is at fault and no configuration fixes it. It is
evidence for a ticket, and it is never read back as instruction.

The prompt already carries the precedent paragraph for this, at
workers/api/src/agent-think.ts:314-318 ("MEMORY vs BEHAVIOUR: memory is for facts about the
SUBJECT you work on…"). Extend it, in the same place and voice:

FEEDBACK vs MEMORY vs BEHAVIOUR: when the user tells you that YOU got something wrong — a
wrong answer, an action you claimed but did not take, a step that failed silently — call
record_feedback. That is a report about the platform, kept for the people who improve it; it is
not a fact to remember and not a manner to adopt. Do not write it to memory, do not add it to
your behaviour, and do not promise to "remember it for next time". Record it, say you have, and
carry on with the task.

The last clause is deliberate: #506's agent promised to file something later and nothing scheduled
the promise.


7. The read surface

Recommendation: a per-instance feedback tab AND a platform-wide /feedback page, served by one
route.
Build the tab first if only one can ship.

  • The tab goes in store/console/src/lib/surfaces.tsx SURFACES with show: () => true
    the same universal treatment as Behaviour (:265-278) and Stats (:245-256), and for the same
    stated reason: every agent can be complained about, so gating it on capabilities.surfaces would
    hide it exactly where a new agent type most needs it. It must be a tab, not a Knowledge/Activity
    sub-tab: [bug] Knowledge → Documents, Files and Index are shown on agents that declare no tool to read them — the gate the Indexing surface got was never applied one level in #509 landed last night on precisely the failure of hiding a surface one level inside a
    composite.
  • The page goes at /feedback in store/console/src/App.tsx beside /terminals and /usage
    (:56-57) with a nav entry in Layout.tsx:19-20. This is the triage view — "everything I have
    flagged, across every agent, still open" — and it is the one the owner will actually use when
    filing.
  • One route serves both, following errors.ts exactly (errorRoutes.get("/") is user-scoped
    with optional filters): GET /v1/feedback?instance_id=&status=&limit=. The tab passes
    instance_id, the page does not.

A row renders: timestamp, agent name, the owner's words, the quoted target message, status, and two
links — "open the conversation" (deep link to the instance chat) and "open the trace"
(/v1/instances/:id/trace?trace_id=…). Those two links are the whole feature; without them this is
a suggestion box.


8. MCP — the tools that close the loop

Without these the feature stops at collection and never reaches "file the tickets better". A
pags-ba agent must be able to pull feedback the way it pulls instance_messages today.

Register in workers/mcp/src/instance-tools/observability.ts — it is already the home of
instance_messages, agent_trace and list_errors, and its docstring says so:

Housekeeping the implementer must not miss: workers/mcp/src/tool-count.ts — bump
MCP_TOOL_COUNT 133 → 135 and MCP_TOOL_ALWAYS_ON 115 → 117, and update the "N tools" prose in
platform-docs/mcp.md, store/llms-full.txt and workers/mcp/README.md, or index.test.ts and
scripts/docs-drift.mjs fail.


9. Voice — capturable, but never load-bearing

The owner talks to these agents, so spoken feedback matters. Two decisions:

Do NOT add a feedback voice command. VoiceCommand is a closed union of seven
(packages/sdk/src/voice/convo.ts:157) and every one of them is a distinctive word chosen so a
whole-phrase match cannot hijack ordinary speech ("repeat", "scrap", "exit"). The natural phrases
for feedback — "that's wrong", "no, that's not right" — are ordinary speech, spoken constantly
during normal use. A command word there would fire mid-sentence and turn half a conversation into
feedback rows. (matchVoiceCommand at convo.ts:516 and the per-language tables at :755-762 are
where this would go, and should not.)

Instead: the feedback composer is a text field the existing STT can dictate into, exactly like
the chat composer, plus typing. And it must record voice provenance:

When the target message carries audioKey and/or dictation (agent-types.ts:9-17), copy BOTH
into context.

That is not a nicety — it is the only way a voice bug is filable at all, and #510#512 are three
live examples. From the same live pull as section 3, the user turn that produced #503:

content   : "…how many agents do you have in total? Give me their names."
dictation : "…how many ages does Kevin total give me the next"

Feedback on that turn without the dictation field is a report about the agent; with it, it is a
report about the recogniser.

Honest limitation to write into the issue and the UI: the R2 audio blob is deleted by Clear chat
and delete-turn (agent-do.ts:855-868, :888-901), so a stored audioKey may dangle. The record
says a recording existed and names its key; the blob keeps the transcript's lifetime. Do not build a
second retention rule for it — that is the mistake dictation was moved onto the message to avoid.

And the standing constraint: voice capture itself is currently unreliable (#510, #511, #512,
#425). Nothing in this feature may depend on a working microphone. The UI path (section 5A) is the
guarantee precisely because it does not.


10. Deletion, edit, and whether the agent can see it

Three decisions, each with its reason:

  1. The body is never editable; the status is. A feedback row is a record of what the owner said
    at a moment. Editing the body destroys the only property that makes it evidence — and [bug] The Pilot overrode the engine's correct objection and reported it to the owner as his own explicit choice — the deploy it broke was never his decision #505 is
    specifically about a record that did not match what was said. status and issue_url are
    mutable, updated_at moves; no audit sub-table (over-engineering at this volume).
  2. Hard-deletable by the owner. It survives Clear chat by design, it can contain anything he
    typed including PII or a pasted secret, so "delete my data" must reach it. DELETE /v1/feedback/:id,
    owner-scoped.
  3. NOT visible to the agent. No injection into the system prompt (that is [bug] The summariser stores transient platform state as permanent memory — an instance still carries "write access is not enabled" while its own prompt says consent is GRANTED #495 by another
    route, plus unbounded per-turn tokens — section 4), and no agent read tool in v1. An agent
    that reads complaints about itself starts answering them: apologising, over-correcting, and
    treating a bug report as a standing instruction, which is exactly the boundary section 6 draws.
    The owner-authenticated MCP tools are the read seam, so a supervisor or BA agent can still get
    at it — deliberately out-of-band, never in the prompt of the agent being complained about.

11. Plan — cheapest first, each step shippable

Step 1 — make a chat turn addressable (independently valuable; land it alone if you like).

  • workers/api/src/routes/instances-chat.ts: mint const turnId = crypto.randomUUID() and capture
    const startedAt = Date.now() before the DO fetch; pass traceId: turnId in the body (the
    field is already read — agent-do.ts delegation.traceId, and agent-loop.ts:130 already
    supplies it); use startedAt for the chat.in event's ts.
  • workers/api/src/agent-types.ts: add optional traceId?: string to AgentMessage; stamp it on
    both the user and assistant messages of the turn in agent-do.ts runTurn; return it from
    /messages.
  • Result: chat.truncated and chat.invented_result stop being orphans, chat.in stops being 7.8s
    late, and the console holds the trace id for the next step.

Step 2 — the store + capture route. Migration (next free number, guard-checked),
workers/api/src/lib/feedback.ts (pure: validate, clamp, build the row), new router
workers/api/src/routes/feedback.ts mounted at /v1/feedbackPOST /, GET /, PATCH /:id,
DELETE /:id; requireUser + requireOwnedInstance for the instance scope; default rate limit
(240/min) is fine. Add the four paths to store/openapi.yaml or scripts/openapi-coverage.mjs
fails.

Step 3 — the capture affordance. See the layout note below; smallest correct version first.

Step 4 — the read surface. FeedbackTab.tsx + a feedback entry in SURFACES; then
pages/Feedback.tsx + the route and nav entry. Same fetch, two callers.

Step 5 — the agent tool. record_feedback in tool-registry.ts beside create_ticket, added
to BASE in workers/api/src/agent-do-tools.ts:15-70 with a comment in that file's established
style, plus the prompt paragraph from section 6.

Step 6 — MCP. list_feedback + resolve_feedback, tool-count and docs-drift updates.

The layout note the implementer needs before step 3

The assistant bubble corner already holds two controls and the measurement is written down at
store/console/src/pages/InstanceDetail.tsx:1145-1166: Copy at right-1, Delete at right-8, and

"Delete's outer edge is 56px from the bubble border (right-8 + its 24px box) against a content
box starting 12px in, so 44px must be reserved and pr-12 is 48."

Below sm both are permanently visible (there is no hover on touch), and #426 was the timestamp
being covered. A third icon at right-14 puts the outer edge at 80px → 68px must be reserved →
pr-20, on a bubble that is 200px wide at 320px viewport.

For the coding surface, the anchor is session_id + coding_timeline.seq (migration 0023
seq is an AUTOINCREMENT PK, so it is already a stable address), and the affordance belongs on a
Co-pilot turn.


12. Alternatives considered and rejected


13. Acceptance criteria

  1. A console chat turn's user message, assistant message, and its chat.in / tool.call /
    chat.out / chat.truncated / chat.invented_result events all carry the same trace_id.
    Checkable: send a message, read /messages and /trace, compare.
  2. chat.in's ts is within 250 ms of the user message's createdAt (today: 7,792 ms on the
    measured turn).
  3. Clicking the feedback control on any message, typing a sentence, and submitting creates exactly
    one agent_feedback row whose message_id, trace_id, target_text, target_at and
    prompt_text are populated from that turn — verified with the model API mocked out entirely,
    i.e. the path does not touch a model.
  4. Feedback given on a voice turn has audioKey and dictation in context when the message
    carries them.
  5. GET /v1/feedback returns the row for the owner and 404/empty for a different user; the same
    route with ?instance_id= returns only that instance's.
  6. Clearing the chat (DELETE /v1/instances/:id/messages) leaves the feedback row intact and its
    target_text still readable; the read surface shows the conversation link as unavailable rather
    than erroring.
  7. The Feedback tab appears on every agent, including one with surfaces: [] (e.g. Coder Lead
    5fab318d…) and one with surfaces:["repo"].
  8. list_feedback over MCP returns rows including trace_id; passing that trace_id to
    agent_trace returns the tool calls of the complained-about turn. This is the "like we did
    now" criterion — demonstrate it end to end on a real complaint.
  9. resolve_feedback sets status:"filed" + issue_url and is refused without the MCP write
    scope.
  10. An agent asked "remember that you got that wrong" calls record_feedback and does not call
    write_memory — assert on the tool calls, not on the prose.
  11. No feedback text appears anywhere in the built system prompt. A unit test over the prompt
    builder with rows present is the check.
  12. node scripts/check-migrations.mjs, scripts/openapi-coverage.mjs, docs-drift.mjs and
    check-file-size.mjs all pass; MCP_TOOL_COUNT matches the real registration count.

14. Regression risk

Change What it could break The test that catches it
traceId on AgentMessage /messages readers, turnSpanFor (#342 delete-turn), lib/message-page.ts cursors, lib/fabricated-history.ts existing agent-do / message-page / delete-turn suites — the field is optional and additive, so a failure here means something is comparing whole message objects
Minting turnId before the DO call events are currently written only inside if (doRes.ok); keep that. A failed turn now could be traced, which is an improvement, but do not change error behaviour in the same step routes/chat.integration.test.ts; no test currently asserts the ts values (grepped)
chat.in ts = startedAt trace ordering (listEvents sorts by ts) ordering holds because startedAt < now; assert oldest→newest in the trace test
A third bubble control #426 (timestamp covered below sm), #389 (44px tap targets), #342 (Delete must not be swallowed by a wider neighbour — its own comment warns that later-painted overlays win the hit test) a WebKit 320px measurement, per the note in step 3; the Chromium e2e will not catch it
A 13th tab mobile tab strip is icon-only + scrollable, so this is width, not overflow visual check at 320px
record_feedback in BASE every agent's tool list grows by one; a declared capabilities.tools allowlist does not exclude it (BASE is always added — agent-do-tools.ts:187-193), which is intended tool-reachability.test.ts; note it asks "does SOME agent declare this?", so it cannot catch a per-agent gap (#506's lesson)
New MCP tools index.test.ts counts registrations; docs-drift.mjs reads three docs both fail loudly if tool-count.ts is not bumped

15. Open questions for the owner

  1. Sentiment field — keep or drop? I have included an optional sentiment: 'bad'|'good' because
    a one-tap 👎 costs nothing and catches feedback that would otherwise not be typed. It is not
    needed for ticket-filing. I would keep it, and make the tap open the composer rather than submit
    silently — a bare thumbs-down with no words is not filable evidence.
  2. Should a supervisor (Coder Lead) get a read_feedback agent tool so it can triage complaints
    about its subordinates? I would defer this until there is volume: it is the one path that puts
    feedback back into a prompt, and it should be decided on evidence, not in advance.
  3. Anonymous/creator-visible feedback. Everything above is owner-private. When PAGS has external
    subscribers, "should a creator see feedback on their published agent?" becomes a real product
    question with a privacy answer attached. Out of scope here; the schema does not preclude it
    (the agent id is reachable via instance_id).

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workenhancementNew feature or requestfrontendFrontend / UI work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions