Repository navigation
Docs audit — request logs — 2026-08-17 #69
Description
Audited: the request logs surface of mnfst/manifest at main 633455b98 (v6.18.0), against the docs at 7a5cdbb.
Scoped run, topic request logs. Pages in scope: request-logs.mdx, observability.mdx, reference/telemetry.mdx. Platform window: the whole logs and data/telemetry surface at main, plus the PRs that built it (#2317 removed body storage in v6.12.0; #2565 per-attempt logs and storage backends, #2627 logs on by default for new harnesses, and #2678 API surface on the request all shipped in v6.18.0).
Findings
1 · [WRONG] The data page says inline image data is stripped out. A stored log keeps the images, and everything else the request carried, exactly as sent
Page: https://manifest.build/docs/reference/telemetry (What Manifest stores, the first paragraph)
What actually happens:
- You turn logs on for a harness.
- Your agent sends a request carrying an inline image, a token, a customer record, anything.
- Manifest stores the exact body it forwarded to the provider: images included, nothing removed, nothing shortened, no size limit.
- Anyone with access to that workspace's Requests page reads it back word for word on the attempt's Raw tab.
- The stripping the page describes happens on a throwaway copy Manifest uses to pick a model. That copy is never stored and never sent anywhere.
Options:
- Drop the stripping claim from the metadata paragraph, and state in the bodies paragraph that a stored body is kept exactly as sent. Recommended (this is the privacy page: the current order makes the reader apply the protection to stored bodies).
- Keep the sentence but scope it to the model-choosing step, and add one line to the bodies paragraph saying stored bodies are unfiltered and uncapped.
- Drop the stripping claim only, and leave the bodies paragraph as it is.
Suggested wording (option 1), replacing the paragraph at reference/telemetry.mdx:14:
Manifest always keeps metadata about each request: model, provider, tier, token counts, cost, latency, and an error message. Error text is scrubbed for secrets before it lands, and the request headers Manifest stores drop credentials and IP addresses. The usage and cost views in the dashboard are built from that metadata alone. See Observability for what the record looks like in practice.
and replacing the paragraph at reference/telemetry.mdx:18:
A stored body is exactly what went over the wire: every message, system instruction, tool definition and inline image, at full length. Manifest doesn't redact or truncate it, and there is no size limit. Stored bodies are held outside the database, in the object storage your instance is configured for, and deleted on a retention schedule. With logs off for a harness, that harness's prompts and completions stay between you and the provider.
Evidence (for the agent)
reference/telemetry.mdx:14— "Manifest always keeps metadata about each request: model, provider, tier, token counts, cost, latency, and a scrubbed error message. Inline image data is stripped out, and error text is scrubbed for secrets before it lands." Followed at:16by "Manifest also stores the request and response bodies of each provider attempt" and at:18by "Recorded bodies are held outside the database", so the protection reads as covering those bodies.packages/backend/src/routing/proxy/proxy.controller.ts:382—routingBody = redactInlineImageDataUrls(body); the untouchedbodyis what is passed on (:388-391).packages/backend/src/routing/proxy/proxy.service.ts:249,266— the redacted copy becomesroutingSourceand is passed only toresolveRouting;bodyis what is forwarded.packages/backend/src/routing/proxy/inline-image-redaction.ts:37-48— the redactor swaps the data URL for a placeholder in that copy.packages/backend/src/routing/proxy/provider-client.ts:312-352— the log is started with the wire body built frombody.packages/backend/src/routing/proxy/attempt-recording-capture.ts:19-54—buildRecording()returnsrequest_bodyverbatim: no filter, no scrub, no cap. Specattempt-recording.service.spec.ts:59-70asserts "stores large recordings completely rather than truncating them" on a 2 MiB body.packages/frontend/src/components/RequestDrawer.tsx:510-523— the Raw tab printsrequest_bodyandresponse_bodyas-is;:240-249makes the tab appear whenever a body is stored.- True halves, so the fix does not overshoot:
proxy-message-recorder.ts:772scrubs the error message, androuting/proxy/request-headers.ts:9-41dropsauthorization,cookie,set-cookie,proxy-authorization,x-api-keyand the IP headers. - A reader of the privacy page concludes images and secrets are removed before storage. They are not, on the one surface where message content is actually stored.
2 · [MISSING-CASE] The retention table promises Pro a year. When the subscription stops being active, the workspace drops to the 7-day window and the next nightly cleanup deletes the rest
Page: https://manifest.build/docs/request-logs (Retention, the deployment table)
What actually happens:
- You are on Pro, and your logs are kept for a year.
- Your card fails, or you cancel. The subscription is no longer active and no longer in a trial.
- The nightly cleanup stops treating the workspace as Pro.
- On its next run, in one pass, it deletes every stored body older than 7 days.
- The requests stay listed with their cost and model. The conversations are gone, and nothing in the dashboard says they expired: the tabs are simply not there any more.
Options:
- Add one line under the table: the Pro window applies while the subscription is active or trialing, and a lapsed one falls back to the free window on the next nightly cleanup. Recommended (smallest edit, covers cancellation and payment failure at once).
- Replace the two Cloud rows with rows keyed on the subscription's state: active or in trial, canceled, payment failed, no subscription.
- Say it in the sentence about what the cleanup removes rather than in the table.
Suggested wording (option 1), inserted at request-logs.mdx:66, between the table and "Self-hosted installs can override this...":
Cloud Pro keeps 365 days only while the subscription is active or in a trial. A canceled workspace, or one whose payment failed, drops to 7 days, and the nightly cleanup applies that on its next run, deleting everything older than 7 days.
Evidence (for the agent)
request-logs.mdx:59-67— "Recordings are deleted on a schedule." then the rows "| Cloud Free | 7 days |", "| Cloud Pro | 365 days |", "| Self-hosted | 365 days |".packages/backend/src/database/request-recording-retention.service.ts:8-10—FREE_RETENTION_DAYS = 7,PRO_RETENTION_DAYS = 365.:27-59— a tenant counts as Pro only viaEXISTS (... subscription.plan = 'pro' AND subscription.status IN ('active','trialing')). Every other state (canceled, past_due, none) falls into the 7-day branch. Specrequest-recording-retention.service.spec.ts:46-77asserts the params[7, 365].:100-106— the plan-aware query runs whenever billing is enabled and no global override is set;billing/billing.config.ts:8-15makes that Cloud-only, so the table's self-hosted 365 is correct.:87-91—@Cron(CronExpression.EVERY_DAY_AT_2AM, { waitForCompletion: true }), so the drop lands within a day and applies retroactively over all history.:61-66,148-155— the blob is deleted, then the pointer nulled. Grep for "expired"/"deleted" inRequestMessages.tsxandRequestDrawer.tsx: 0 hits, so the tabs vanish with no explanation.- A Pro user whose payment fails loses a year of conversation history overnight, after the docs promised 365 days.
3 · [MISSING-CASE] A self-hosted install can show logs switched on everywhere and store nothing, with no sign of it in the dashboard
Page: https://manifest.build/docs/request-logs (Self-hosted storage, around the warning about partial storage settings)
What actually happens:
- You configure log storage and get one value wrong: a bucket with no region, one half of a key pair, a mistyped storage mode. Or you set the mode to off on purpose.
- Manifest starts normally and serves every request. Nothing fails.
- Each harness's Settings page still shows the switch on, and flipping it still confirms logs are enabled.
- Requests keep appearing on the Requests page, and their attempts have no Messages, Tools or Raw tab, exactly as if logs had never been turned on.
- The only statement of the problem is one warning line in the server's own startup output.
Options:
- Add a short "Check it's working" step to the self-hosted storage section: the warning line to grep for, and the symptom in the dashboard. Recommended (turns a silent multi-week data loss into a 30-second check).
- Extend the existing warning with one sentence saying the dashboard cannot show this, and naming the symptom.
- Leave it: treat the existing warning about partial settings as sufficient.
Suggested wording (option 1), inserted at request-logs.mdx:119, after the "Full variable list" line and before "Platforms without a persistent disk". The command is Compose-specific; on another host the reader greps their own process output:
Check it's working
A storage problem never stops traffic, and the dashboard never mentions it. Flipping Enable logs on still confirms the change, and attempts come back without Messages, Tools, or Raw tabs, exactly as if logs were off. The server output is the only place the problem shows.
docker compose logs manifest | grep "Request recording storage"A line reading
Request recording storage unavailable: <reason>means Manifest is storing nothing, and the reason names what to fix. No such line means the backend resolved.
Evidence (for the agent)
request-logs.mdx:111-116— the existing Warning covers partial S3 settings ("leaves recording with no working backend instead of falling back to local disk") but never says the state is invisible in the product.:109— "Set it tos3,filesystem, ordisabledto decide explicitly"; a value that is none of those also yields no backend.packages/backend/src/common/services/request-recording-storage.service.ts:46-107— resolution::82-86incomplete S3 givesbackend: null;:90-92disabledgives null;:93-98an unknown value gives null with the reasonUnknown REQUEST_RECORDING_STORAGE value "<value>";:99any single S3 value commits to the S3 branch with no filesystem fallback.:216-229— the only signals arelogger.warn('Request recording storage unavailable: <reason>')at boot andlogger.error(...)ifprepare()fails later.packages/backend/src/routing/proxy/attempt-recording.service.ts:17-19—availableisstorage.backend !== null. Grep forattemptRecording.available|recordingAvailable|storageAvailableacrosspackages/backend/srcandpackages/frontend/src: one hit,proxy.controller.ts:273, where it is ANDed with the per-harness flag. No endpoint, no frontend read.packages/backend/src/routing/tier.controller.ts:241-245— the Settings switch reads only the stored per-harness flag;packages/frontend/src/pages/SettingsRecordingSection.tsx:24shows the confirmation toast regardless.packages/frontend/src/components/RequestDrawer.tsx:239-249— the three log tabs are gated purely on a stored body being present, so a broken backend is indistinguishable from logs being off.- A self-hoster believes logs are on for weeks, and finds out on the day they need one that nothing was ever stored.
4 · [WRONG] Both pages say a request Manifest blocked itself has zero attempts. The Attempts list actually shows one entry for it
Page: https://manifest.build/docs/observability (Requests and provider attempts, the paragraph after the table) and https://manifest.build/docs/request-logs (What gets recorded, the last paragraph)
What actually happens:
- Your agent sends a request and Manifest rejects it itself: a usage limit, a rate limit, a malformed body, an internal failure.
- The request appears on the Requests page like any other.
- Opening it shows an Attempts list with one entry, labelled with the model you asked for, or "No provider" when the request carried none, and the failure status.
- That entry has a Details tab, plus a Request headers tab when headers were sent. No Messages, Tools or Raw tab, because no provider was called and no conversation was stored.
- The "Manifest rejected this request before contacting a provider" screen that the zero-attempt wording describes appears only when the request truly has no attempt at all, which is not the case for any of the blocks the two pages give as examples.
Options:
- Correct both sentences: a blocked request still shows one entry in the Attempts list, and that entry has no conversation to read. Recommended (keeps the point each page was making, fixes the count).
- Fix only the request-logs sentence, where the missing tabs are explained, and drop the example from the observability counting rule.
- Replace the example on both pages with the fallback and Autofix case, which is the one the counting rule really needs.
Suggested wording (option 1), replacing the paragraph at observability.mdx:25:
A request that succeeds on the first try is one Request with one attempt. A request that fails twice before succeeding is still one Request, with three attempts. A request Manifest blocked itself, like a hard limit or a malformed body, is one Request with one attempt that never reached a provider. That attempt is listed under the model you asked for, or No provider when the request named none, and carries the error that stopped it.
and replacing the paragraph at request-logs.mdx:28:
A request Manifest blocked itself, on a hard limit or a malformed body, still lists one attempt, labelled with the model you asked for or No provider. It carries the failure status and no Messages, Tools, or Raw tab: Manifest sent nothing to a provider, so there is nothing to read back.
Evidence (for the agent)
observability.mdx:25— "A request Manifest blocked itself, like a hard limit or a malformed body, is one Request with zero attempts, since no provider was ever called."request-logs.mdx:28— "A request Manifest blocked itself, on a hard limit or a malformed body, has zero provider attempts and so no recording. There was no exchange to record."packages/backend/src/routing/proxy/proxy-message-recorder.ts:725-737—recordManifestBlockedRequestis described in code as "The single writer for every Manifest-authored failure row: setup errors (M100/M101), limits (M200/M204), rate limits (M201-M203), malformed requests (M300), internal errors (M500), and expired keys (M004)". It writes a row withprovider: null,routing_tier: null, andmodelset to the requested model (:775-790).packages/backend/src/routing/proxy/proxy-message-recorder.ts:803—persistAttempt(row)writes the row, which is what puts the entry in the Attempts list.- Call sites:
proxy.controller.ts:342(M204),:673-686(every recordable ManifestError),:693-705(M500),:598-615(the friendly-stub path),proxy-exception.filter.ts:63-78,138(M004). packages/backend/src/common/errors/manifest-error.ts:36,64-66— only M001/M002/M003/M005 write nothing, and the auth guard throws those before a Request row exists (otlp/guards/agent-key-auth.guard.ts:118-133,177), so they never reach the dashboard at all.packages/backend/src/analytics/services/message-details.service.ts:122-127— attempts are every stored row for the request, unfiltered.packages/frontend/src/components/RequestDrawer.tsx:364-409— the Attempts sidebar renders whenever there is at least one attempt, showingatt.model ?? 'No provider';:239-249gates the three log tabs on a stored body;:413-439is the zero-attempt pane.- A user counting attempts, or looking for the "rejected before contacting a provider" screen the docs describe, sees neither.
5 · [MISSING-CASE] The telemetry field table does not match the report that is actually sent: agent groupings can arrive as combined keys, three fields carry an "unknown" bucket the table never names, and the message count counts provider calls
Page: https://manifest.build/docs/reference/telemetry (What gets sent, the field table)
What actually happens:
- You point the report at your own collector, exactly as the page describes.
- Agent groupings arrive with combined keys for anything classed as "other", the category and the word "other" joined, not the bare names the example shows.
- The provider, tier and auth-type groupings can each carry an "unknown" bucket, for activity Manifest blocked before a provider or tier was picked. The table names only the "custom" bucket.
- The message count is one per call to a provider, so a request that fell back twice counts three, and blocked requests count as well.
- A dashboard built from the documented shape mislabels the combined keys, drops the unknown buckets, and reads the count as request volume.
Options:
- Correct the five rows in place, keeping the examples but showing the real key shapes. Recommended (the page promises a private collector "receives the same payload documented above").
- Add a short "shape notes" paragraph under the table instead of touching the rows.
- Fix only the agent grouping row, where a wrong key shape actually breaks a collector, and leave the counting nuance.
Suggested wording (option 1), replacing the rows at reference/telemetry.mdx:42-45:
| `messages_total` | `1284` | Daily activity per install. One count per provider attempt, so a request that fell back twice counts three. Requests Manifest blocked count too | | `messages_by_provider` | `{"anthropic": 700, "openai": 500}` | Provider mix. Anything we don't recognize collapses to `"custom"`, so self-hosted provider names and URLs stay local. Activity with no provider, like a blocked request, lands in `"unknown"` | | `messages_by_tier` | `{"default": 900, "simple": 300, ...}` | Routing tier usage. A tier outside the known set collapses to `"other"`, and activity with no tier lands in `"unknown"` | | `messages_by_auth_type` | `{"api_key": 1200, "subscription": 84}` | API key, paid subscription or local usage. Anything else collapses to `"other"`, and activity with no auth type lands in `"unknown"` |
and the row at reference/telemetry.mdx:51:
| `agents_by_platform` | `{"openclaw": 3, "hermes": 1}` | Which agent clients people use. A harness whose platform is `other` is keyed by its category instead: `personal:other`, `app:other` or `coding:other` |
Evidence (for the agent)
reference/telemetry.mdx:51— "|agents_by_platform|{"openclaw": 3, "hermes": 1}| Which agent clients people use |".:43— "Anything we don't recognize collapses to"custom"".:42— "|messages_total|1284| Daily activity per install |".:90— "Your collector receives the same payload documented above".packages/backend/src/telemetry/payload-builder.service.ts:106-133—agentsByPlatform()emits`${r.category}:${r.platform}`whenever the platform isother, giving keys likecoding:other.:74and:200-214—bucketsToRecord(rows, 'unknown', ...): a NULL grouping value becomes the keyunknown; a tier or auth type outside the whitelist becomesother.:195-199—bucketFor(null)returns'unknown', not'custom'; blocked rows carryprovider: null(proxy-message-recorder.ts:776), so the bucket is routinely non-empty.:145-155— the counter isCOUNT(*)over the last 24h ofagent_messages, which is the provider-attempt table since1801400000000-MoveRecordingsToProviderAttempts.ts:3-10, plus one row per blocked request.- Field count verified against
:57-77andtelemetry/dto/telemetry-payload.ts:11-51: exactly the 15 documented fields, none renamed, none missing. The rest of the table is accurate. - Someone running their own fleet dashboard, the exact use case the page invites, builds it against a shape the payload does not have.
6 · [OBSOLETE] The data page tells users to turn off "message recording" on the Settings page. The product calls it Logs, and the same page says so two paragraphs earlier
Page: https://manifest.build/docs/reference/telemetry (What Manifest stores, the note at the end of the section)
What actually happens:
- You decide one harness should not have its prompts stored.
- The note tells you to turn message recording off on that harness's Settings page.
- On that page nothing is called message recording: the section is Logs, the switch is Enable logs.
- Two paragraphs above, the same page already sends you to Settings → Logs.
Options:
- Align the note with the shipped names and with the paragraph above. Recommended.
- Same, and drop the old name from the page's search keywords.
- Leave it: the old name still helps people who search for it.
Suggested wording (option 1), replacing the note at reference/telemetry.mdx:20-24:
<Note> If a harness handles data you'd rather Manifest never hold onto, switch **Enable logs** off in its **Settings → Logs** section. Metadata collection isn't affected either way. </Note>
Evidence (for the agent)
reference/telemetry.mdx:21-22— "If a harness handles data you'd rather Manifest never hold onto, turn message recording off on its Settings page."reference/telemetry.mdx:16— "to make sure a harness records its logs, check its Settings → Logs", the accepted wording from issue Docs audit — message recording — 2026-08-06 (mnfst/manifest PRs #2565–#2662) #45 finding 2.packages/frontend/src/pages/SettingsRecordingSection.tsx:35— the section title is "Logs";:39— the row label is "Enable logs".- Not a re-raise: the rename sweep accepted as issue Docs audit — introduction (getting started) — 2026-08-07 #49 finding 1 (PR docs: resolve getting-started audit findings (issue #49) #50, commit
58ca5c2) replaced the old label in link text and cards on six pages and did not touch this sentence; the earlier rename commite867ff9changed only the link at:16of this file. This sentence has never been raised and never been rejected. - A user hunting the Settings page for a control named "message recording" does not find one.
7 · [IMAGE] The Messages tab, the screen the whole feature exists for, has no screenshot
Page: https://manifest.build/docs/request-logs (Reading it back, directly under the paragraph describing the conversation view and its side rail)
The shot must show one attempt's Messages tab: the turn rail on the left with its filter box and role pills, one turn selected, and the conversation card open in the main pane.
Evidence (for the agent)
request-logs.mdx:38— "The Messages tab renders the exchange as a conversation, with a rail down the side listing every turn. Search the rail or filter it by role, and click any row to jump to that turn in the main pane." Prose only, no image.- The existing image at
request-logs.mdx:34-36(/images/request-logs-drawer.png) shows the Details tab, not Messages. Opened and checked: otherwise current. packages/frontend/src/components/RequestMessages.tsx:310-360— the rail: filter box, role pills, one row per turn with role badge, index, preview, token estimate;:154,214-224the collapsible turn cards.- The other existing image (
/images/request-logs-settings.png) was opened and is current: Logs section between Autofix and API Key, "Enable logs" row, switch on. No replacement needed.
Also checked (not raised)
- Logs being off on every pre-existing harness after a v6.18.0 upgrade: the accepted wording at
request-logs.mdx:44gives the path to check, and narrating the feature's removal and re-add is barred by the editorial rule settled in issue Docs audit — message recording — 2026-08-06 (mnfst/manifest PRs #2565–#2662) #45. - The ephemeral-filesystem list at
request-logs.mdx:122: unchanged on the platform side, still open as Docs audit — 2026-08-07 (PRs #2693–#2697) #48 finding 6. - The storage variable at
request-logs.mdx:67, outside a toggle block: it self-scopes to self-hosted in its own sentence. - The volume name at
request-logs.mdx:84: verified correct against the shipped compose file. - Non-streamed responses store the upstream body rather than the converted one the client received:
request-logs.mdx:24already frames the pair as provider-facing. - Fallback hops skipped on cooldown appear with no log tabs: covered by the page's existing rule that the tabs appear when the attempt has a body stored.
- The field count on the telemetry page: 15 fields, none renamed, none missing.
Reply with the finding number and the chosen option
(e.g. "3 → option 2", or "3: other idea, let's discuss").
PRs are then made manually with the agent, never by the harness.
Rejected findings: reply "N → reject" and they will never be re-raised.
Activity
docs-sync: scoped audit — request logs (7 findings, issue #69), topic…
Merge origin/main into fix/issue-53-llm-gateway
SebConejo commentedon Aug 18, 2026
Traitement en lot (2026-08-18) : les sept findings → option recommandée chacun, avec les rédactions proposées par l'audit. Le finding 7 est rempli : capture du tab Messages faite sur une instance locale seedée, livrée dans la PR.
Merge pull request #75 from mnfst/fix/issue-69-request-logs