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

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.

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

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