Feedback
A drop-in feedback capability for any MeshWeaver deployment. A user types /feedback <message>
in any thread; the flow captures the message plus the context that makes it actionable, files it
as a draft, and shows the user a polished preview with a Submit button — so they see exactly
what will be sent before it goes. Only when they click Submit does it reach the shared
Feedback space, where Systemorph reviews it.
What gets captured
Every submission is a Feedback/Feedback node whose FeedbackContent records:
| Field | Source | Notes |
|---|---|---|
message |
the user's words, verbatim | the record itself |
submittedBy / submittedByName |
# Current User context |
who |
timestamp |
server, stamped on submit | real ISO-8601 UTC; a client can't backdate |
mainNodePath / mainNodeTitle / mainNodeGist |
# Current Application Context |
where they were (the main panel) + a gist |
sidePanel |
the agent, if reachable | related area open beside the main panel |
extraContext |
the agent, opportunistically | role, repro detail, related paths, … |
category |
inferred, only if obvious | bug | idea | praise | question — and answer, filed by /wrong-answer for a qualitative miss |
status |
server | Draft on capture (invisible to reviewers); flips to New when the user hits Submit; a reviewer then moves it to Triaged / Resolved / WontFix |
issueUrl |
the control instance | the GitHub issue the Systemorph triage agent filed for it, written back when the feedback was submitted here and turned out to be actionable; empty otherwise |
Every context field is optional — the agent fills what it can reach and leaves the rest empty (shown as "not captured" in the review view). A submission never fails because a field couldn't be filled.
The pieces
/feedbackskill (Feedback/Skill/feedback) — the entry point. Instructs the thread agent to capture the message + context, extract more if it can, file a draft, and show the user the preview to review and submit. Prefers the Feedback Agent when present./wrong-answerskill (Feedback/Skill/wrong-answer) — the entry point for a qualitative miss: the chat made something up, said "I don't know" about something in the mesh, answered from the wrong source, or changed something unasked. Reads the offendingThreadMessage(agent, model, tool calls), checks whether the node the user meant is readable, classifies the failure and files the sameFeedback/Feedbackdraft withcategory: answer— which is never handed over per report: once submitted, the card offers to turn it into an eval case. Runs in-thread (never hands off — the evidence is in the thread). Design, kinds and addressees: Wrong answers.EvalCasetype (Feedback/EvalCase, compiled live fromSource/) — a/wrong-answerreport lifted into a repeatable test of the agent (Plugins#2288): the prompt, the node a correct answer must read, the value it must not invent, the value it should state. Created underFeedback/_Evals/{agentId}/{case}with only its source; its own hub lifts the rest on activation. Its Eval area runs the prompt N times against the agent and scores every reply as rates — looked, did not invent, correct. Wrong answers has the checks.Digesttype (Feedback/Digest, compiled live fromSource/) — the weekly wrong-answer digest (Plugins#2288): once the last completed ISO week is due, the schedule on this type's hubs (the shippedFeedback/Digestsnode and every digest) folds everycategory: answerreport intoFeedback/_Digests/{week}— per agent by kind, model and trend, plus the eval cases lifted and their last run — and posts one signeddigestevent per agent to the control inbox, from which the triage agent keeps one standing issue per agent. Its Digest area shows the week; onFeedback/Digestsit lists them all.Feedback Agent (
Feedback/Agent/feedback-agent) — the "feedback agent": does the capture-draft-preview. Callable directly (@agent/Feedback/Agent/feedback-agent) or by the skill.FeedbackContenttype (Feedback/Feedback, compiled live fromSource/) — the record and its views (the card is drawn once and reused, so the submitter's preview is byte-for-byte the reviewer's view):- Compose — the preview-and-submit surface (
Feedback/Compose/{id}or@@("/{User ID}/Feedback/{id}/area/Compose")). Loads the captured draft (as System) and shows it as a card whose message is an editable, data-bound field (host.Edit), with a Submit button. Nothing reaches the reviewers until the user clicks it — this is what lets them SEE and refine their feedback before it's sent. - Detail — the reviewer's full view of one submission (message + all context, present-or-not).
- Inbox — the admin review list on the space root: every submitted submission (drafts excluded), newest first.
- Submit — the space-root "give feedback via chat" call to action (points the user to
/feedback). Feedback is captured in chat, previewed, edited and submitted — there's no separate in-page form.
Every view is built from framework controls styled with CSS — no hand-rolled HTML.
- Compose — the preview-and-submit surface (
FeedbackSubmitter(Source/FeedbackSubmitter.cs) — the submit-as-System core. The production/feedbackskill creates a draft in the user's space;Publish(the Submit button) reads and updates it as System, flips it toNewand stamps the real submit time. The owning hub then moves it into the shared Inbox under System and leaves a navigation redirect at the old address, so preview and issue links still open the submission. The programmaticCreateDraftandSubmithelpers still create directly in the shared namespace but have no production caller. Both routes end with one record in the Inbox; no user-space feedback copy remains after the move.
After Submit: technical feedback is handed to the Systemorph triage agent
Maintainer, 2026-09-12: "the feedback skill must do exactly this ⇒ if it is a technical issue / bug report / feature request ⇒ hand over to systemorph-com triage agent."
When the user clicks Submit — and never before — the draft reads New. Its own hub moves the
record from {User ID}/Feedback/{id} to a stable address under Feedback/_Submissions, then wakes
that new address. The destination's hub hands over technical feedback
(FeedbackHandover.HandOverOnActivation, Source/FeedbackHandover.cs) and stamps
handedOverAt there. Publish merely flips Draft → New; it does not hand over inline.
This is the ONLY hand-over path,
and it is the RequestedX + owning-hub-watcher shape for a reason: a submission created New by
anyone — the /feedback skill, an agent through MCP, the platform's thread supervisor filing an
exhausted thread (Doc/Architecture/ThreadSupervision) — takes the same move and hand-over route
when its hub activates, not only when a person pressed a button in this process. Creation alone
activates nothing (the create is handled by the owner namespace's hub). So nobody has to read
the node back: the Inbox wakes it (next section). The move is issued under System by the mesh's durable node-operation hub: the
source hub cannot receive its own move response after its address is deleted. The destination is
woken after the move because copying a node does not activate its hub. The old address becomes a
Redirect node for navigation; reads and writes still address the new record literally.
🚨 The move waits until storage holds the submitted state (MeshWeaver#5670). The watcher sees
Submit's Draft → New as the owning hub's IN-MEMORY commit. The hub stores that commit
afterwards (the post-commit flush, with the write-behind sampler behind it), and a move copies its
source FROM STORAGE. So a move issued the moment the watcher saw New copied the stored Draft
with the pre-edit message. The destination's watcher skips a Draft, so the report never reached
triage and nothing was logged. On memex.systemorph.com on 2026-09-28 every agent-filed report
reached triage within 2 s of its move, while the portal Submit moved at 14:34:21Z produced
nothing. An agent-filed report never raced: it is created New, so storage already holds New
when its hub activates.
The watcher now relocates a version only once storage holds it. It seeds from the state the hub
loaded at activation and raises that on every post-commit event IMeshInvalidationFeed publishes
for the node, the ordering core's NodeTypeRebindWatcher relies on for the same hazard. A version
still not stored after FeedbackHandover.DurableBudget is logged as an Error and not moved, because
moving it would carry the wrong state. The regression test is
src/MeshWeaver.Hosting.Monolith.Test/FeedbackSubmitRelocationTest. It compiles these sources,
holds the submission's storage writes open, drives the real Publish against a live source hub,
and asserts that nothing relocates the report before its submitted state is stored and that the
Inbox copy is New with the edited message.
handedOverAt on the submission is the
proof it went; a technical New submission without it on a portal whose control inbox is
configured is one whose hub never activated.
The Inbox wakes every submission still waiting (Plugins#2285)
Creating a node does not activate its hub, so a submission an agent filed with MCP create and
nobody read back used to wait in the filer's space for ever: 124 such records sat in one user's
Feedback folder on the control instance. The fix is a change-feed watcher, not a filing rule.
- Where it runs. The shipped
Feedback/Inboxnode is itself aFeedback/Feedbacknode, and its hub, and only its hub, armsFeedbackBacklog.WakeWaitingSubmissionsFromInbox(Source/FeedbackBacklog.cs). The package declares"anchors": ["Feedback/Inbox"]in itsindex.json, and the AI module'sPackageAnchor(pre-installed on every portal) holds every declared anchor activated for the life of the process. So the watcher runs from boot to shutdown, with no page open. The declaration is honoured because Feedback is an INSTALLED package: its install recordPlugins/Feedbacktargets theFeedbackroot. A node somebody typesStore/Pluginin a space of their own declares nothing. - What it does. One synced query,
nodeType:Feedback/Feedback partitions:all, run as System. Its first frame is the backlog: every record that existed before the watcher started, which no later event would ever report. Its laterAdded/Updatedevents are new submissions and Draft →Newflips. A listed record that still waits (FeedbackBacklog.IsWaiting: submitted and outside the Inbox, or in the Inbox, technical, unstamped, and this portal has a route to triage) is woken by opening its stream, which activates its hub. The record's own hub then moves it and hands it over exactly as above. The query only nominates; the record's hub decides from its own stream. - Bounded. One record at a time: the watcher holds a record until it stops waiting (a
Redirectstands at its old path, or it is stamped), or forFeedbackBacklog.SettleBudget, then takes the next. A backlog of hundreds drains as consecutive moves, never as a burst. - Idempotent. A moved record is a
Redirectand no longer matches; a stamped record no longer waits. Waking a record that is already done changes nothing, so a restart, a second replica or a stale listing never hands anything over twice. A record that still waits after the budget is logged with its path and left for the Inbox's next activation; it is not retried in a loop.
The regression test is src/MeshWeaver.Hosting.Monolith.Test/FeedbackBacklogWakeTest: a backlog
of unread records gets exactly one hand-over each once the Inbox comes up, a record created while
it watches gets exactly one, and a restarted watcher adds none. src/MeshWeaver.AI.Test/PackageAnchorTest
proves the anchor activates a declared node that nothing else reads, and refuses a declared path
outside the package.
A submission nothing can read is a dead letter, and it says so (Plugins#3042)
A record created with a content shape that does not bind to FeedbackContent fails both the owning hub's
hand-over check and the watcher's IsWaiting. Examples are {"$type":"Feedback","status":…,"description":…},
where no type named Feedback exists, or the right $type with the text under description, which
reads as New with an empty message. Measured on the control instance: three coordinator filings
waited 1.5 h that way, and nothing logged or marked anything. FeedbackHandover.UnreadableSubmission
now names them, and only them (not Drafts, praise, stamped records or the content-less Inbox node):
- the record's own hub logs a Warning once per version, with the path and the reason, and does not move or hand it over;
- the Inbox's watcher logs each one once as
dead letter #N: <path> … <reason>.
That Warning is the ONLY report, and it is never an Error (MeshWeaver#6232). The hub reads its
record through FeedbackHandover.ReadSubmission, which converts the content WITHOUT a logger and
then asks UnreadableSubmission. With a logger, ContentAs logged a typed content of another class
— MarkdownContent on a note that was re-typed to Feedback/Feedback — at Error on every
activation, before the classifier ever ran, and an error logged before its classifier can never go
quiet. A typed foreign content is named by its CLR type when it serializes without a $type.
Nothing reinterprets such a record. There is no description → message mapping, because the write
that stored the shape is the defect. Core refuses that write now (MeshWeaver ContentSchemaOnWrite,
"A $type that names nothing"). Correct the content to the FeedbackContent shape and the record
is handed over on its next activation.
The package requires mesh 3.0.0-ci.9125: that is the first sealed main image with the durable
node-operation hub API needed by the move. There is no stable 3.0.0 image with that API yet;
raise the package floor to the stable release once it ships.
What is handed over is unchanged:
| The feedback | What happens |
|---|---|
category: bug or idea |
handed over, always |
category: question, or no category |
handed over when the message reads as technical — it names a defect or a feature (FeedbackHandover.LooksTechnical) |
category: praise, or a non-technical message |
stays in this instance's Inbox; nothing is sent |
category: answer (a /wrong-answer report) |
stays in this instance's Inbox even when the message reads as technical — its addressee is the agent's owner, a space owner or a content owner here (Wrong answers); the one kind that is a platform defect is filed as bug by the skill and takes the first row |
"Handed over" means ONE signed feedback event on the control instance's pooled inbox
(https://control.example.com/api/hooks/Hosting/PlatformBuilds) — the same URL and HMAC every
fleet repository's CI posts to. There it becomes a Hosting/TriageItem, a thread with the
triage agent, and — for what is actionable — a GitHub issue, filed via systemorph-com in
the repository that owns what the feedback names — the Space's GitSync repository, else the code it
names, else the most likely owner (never the fleet's inbox). A finding labelled security is only
ever filed in a private Systemorph repository; otherwise a person files it.
When the feedback was submitted on the control instance itself, the issue URL is written back onto
the Feedback node (issueUrl). The mechanism end to end is Hosting's
Triage page.
Configuration — two keys, both required, defined once per portal:
| Key | Value |
|---|---|
Hosting:ControlInbox:Url |
https://control.example.com/api/hooks/Hosting/PlatformBuilds |
Hosting:ControlInbox:Secret |
the control inbox's HMAC secret — the same value as the control instance's Hosting:PlatformWebhookSecret (Key Vault), mapped e.g. as memexcloud-Hosting-ControlInbox-Secret |
With either key absent the plugin logs one warning per technical submission naming both keys
and keeps today's behaviour — the submission stays in the Inbox. The control instance itself
needs neither: it lists Hosting/PlatformBuilds as a webhook target and holds the watcher's
secret, so it delivers the same signed event into its own inbox. A hand-over that fails for any
reason (network, a refused signature) is logged and never fails the submission.
Where submissions live
Drafts created by /feedback live under {User ID}/Feedback/{id} until Submit. Every submitted
record, including one created directly as New by an agent, moves to
Feedback/_Submissions/{stable-id} — an underscore container (kept out of catalogs). The stable id
is derived from the full original path, so identical ids from different users cannot collide and a
retry cannot choose a second destination. The Feedback space's anchored Inbox area lists
them for reviewers. The move retains the record and its satellites; the old user-space address
holds only a navigation redirect. Feedback is admin-reviewed: seed the space so only platform
admins read it. Eval cases live beside them in Feedback/_Evals/{agentId}/{case}, with
the threads a run started under each case, and the weekly digests in Feedback/_Digests/{week}.
All three containers are protectedSegments of the package.
Tests
- Unit (
Feedback/Feedback/Test/FeedbackTests.cs, run by theTestslayout area and asserted by CI): content-type defaults, the pure submission-building core (timestamp stamping,New/Draftpinning, the Draft→New publish transition, the draft/Inbox filter, id/summary/description shapes), the shared card renderer (message shown, empty context hidden in the preview, HTML escaped, main node linked) and the message-only graceful-degradation contract. - Eval cases (
Feedback/EvalCase/Test/EvalCaseTests.cs, run by that type'sTestsarea): the lift from a report's labelled block, the case path, the three checks and the run summary. - Digest (
Feedback/Digest/Test/DigestTests.cs, run by that type'sTestsarea): the window, the counts by agent, kind and model, the trend, the cases lifted, a quiet week posting nothing, the rendering and the one-event-per-agent shape. - e2e (
e2e/feedback-skill.spec.ts,npm run feedback-skill): drives the memex portal —/feedback <marker>in a thread → agent confirms → the node lands in the Inbox with the message, submitter and main-node context.