Reliable local delivery

Target reader: developers verifying offline, restart and partial attachment outcomes.

The Godot SDK uses one bounded local queue. When queueing is enabled, it atomically saves the frozen report before its first HTTP request. A disk or capacity failure stops that submission; it is never described as successfully queued. An explicit local offline_queue: false or a verified project setting of false prevents new feedback from being saved to disk. Screenshots then remain in memory and process exit cannot recover that submission.

Minimal example

EchoNebulaFeedback.configure({
  "project_key": "enb_pk_xxx",
  "public_api_base_url": "http://localhost:4000",
  "offline_queue": true,
  "max_offline_queue_items": 100,
  "max_offline_queue_attempts": 3,
  "max_offline_queue_bytes": 67108864
})

Defaults are 100 pending records, 100 dead letters, 64 MiB across managed records and attachments, and 3 attempts per report or attachment. Local limits are bounded by 100 records per store, 10 attempts per stage, and 256 MiB; the byte limit has an 8 KiB minimum. The queue reserves space for atomic checkpoints. A full queue rejects a new submission instead of evicting a pending report. Old dead letters may be pruned. Lowering the byte budget below existing usage can stop delivery until space or the budget is restored.

Confirmation and restart

A submit action freezes its report ID, description, context, locale and consents before screenshot capture yields. Closing during capture cancels the unfinished action. Once saved, closing the panel or exiting the game does not create a new report identity.

The report receipt (feedback_id) is saved before uploading attachments. Each attachment keeps its own idempotency key, byte digest, attempt count and confirmed receipt. A restart skips confirmed stages and resends an unconfirmed request with its original content and key. A lost response can therefore cause another HTTP request, while server idempotency keeps one report or attachment. No service runs outside the game process.

feedback_payload_ready only reports prepared intent. Use the payload-free feedback_delivery_changed(result) notification for delivery facts:

State Meaning
sending An eligible request is being dispatched; receipt is not yet confirmed.
queued Saved locally, waiting for eligibility or retry.
partial The report was accepted, but some attachments remain unconfirmed. retryable says whether automatic recovery continues. Do not create another report to retry them.
succeeded The report and all selected attachments have confirmed receipts.
failed Delivery stopped without a complete confirmed result. This does not prove the server received nothing.

The result contains client_report_id, feedback_id, state, retryable, reason, attachments_total, attachments_accepted, and attachments_failed. It excludes feedback content, contact, logs, keys, local paths and signed URLs. Avoid logging full payloads or queue JSON. The team workflow remains new, reviewing, resolved, ignored, archived; Console shows only server-confirmed records and attachments.

Network errors, 408, 429 and 5xx can retry within the persisted budget. Delta and HTTP-date Retry-After are respected. Other 4xx, missing/empty/changed local files and exhausted budgets stop automatically; partial receipt information is retained. A checkpoint write failure stops dispatch until SDK reconfiguration or restart; it does not erase the last successfully saved record. Config lookup retries are capped at 10; call refresh_sdk_config() after resolving a persistent configuration failure. Refresh can return false while dispatch is busy.

Project ownership

Records bind a normalized endpoint, a server-verified project ID when known, and an irreversible Project Key fingerprint. The key itself is never written to the queue. The same project's replacement key can resume known records only after config verifies that project at the same endpoint. Different projects or endpoints cannot silently adopt old payloads.

If the first config request is offline, the local queue option determines whether a minimal record can be saved. Optional controls remain unavailable until project policy is verified. Such a record has no guessed project ID: only the original key fingerprint and endpoint may bind it after successful config. Replacing an unverified key cannot migrate that record, even if the replacement later proves to belong to the same project. HTTP redirects are not followed with credentials.

Seven-day local limit and files

Unsent or failed local reports, dead letters and managed attachments are retained for at most 7 days (604800 seconds) from first local persistence. A previously saved attachment can make the deadline earlier. Retrying, requeueing, changing keys or moving to dead letters never renews it. Expired items are ineligible before requests. Startup and active maintenance remove expired data, including while the game tree is paused or time scale is zero. Closing the game stops execution, so physical cleanup waits for the next startup; there is no background cleanup service.

Invalid timestamps and backwards clock movement fail closed instead of granting a new lifetime. If storage denies removal, the SDK reports cleanup_failed, keeps expired content ineligible and retries local cleanup when active. It does not claim the files were deleted. This limit does not delete or change data already received by the server; server retention is a separate policy.

The SDK owns user://echo_nebula_feedback/queue/, dead_letter/ and attachments/. Do not use these directories for host-game files. Only validated direct file references are managed; traversal and symbolic links are rejected. Startup removes orphaned managed files; active maintenance also cleans abandoned temporary files. Current v3 records use the same frozen intents, receipts, project binding and seven-day limits. Obsolete v1/v2 records are removed from validated SDK-owned paths without migration or sending. If deletion is denied, they remain ineligible and report cleanup_failed until local cleanup succeeds. Unreadable data may leave only a bounded diagnostic without raw payload.

Verification

In the SDK repository, run ./scripts/check-sdk-alpha.sh and GODOT_BIN=/path/to/godot ./scripts/run-godot-headless.sh. The Python tool constructs only current payload fixtures; real Godot tests cover queue and crash recovery.

From Infra, node scripts/e2e-worktree-isolated.mjs --g2-sdk creates an owned temporary API/PostgreSQL/MinIO/Console stack and exercises the actual addon with lost responses, process termination, attachment recovery, project isolation, expiry and disk failures. --g2-native adds native-window scenarios requiring mouse interaction. Committed release acceptance remains ./scripts/e2e-fresh-stack.sh, with its clean gate unchanged. Runtime evidence currently covers the tested desktop engine; it is not a cross-platform or exported-game support promise.

Common mistakes

Next step

See privacy and consent and troubleshooting.