---
summary: "POST/PATCH bypass `withRetry` — prevents double-apply when a successful upstream write loses its response. Adds a 13-test regression suite covering the retry policy across every method."
breaking: false
security: false
---

# 3.1.8 — 2026-05-11

`ObsidianService.#request` wrapped every call in `withRetry`. The framework's default `defaultIsTransient` predicate retries on 502/503/504/429 **and** raw network errors (`UND_ERR_SOCKET`, `ECONNRESET`, `ETIMEDOUT`, …) — fine for idempotent reads, but catastrophic for non-idempotent writes: an upstream that succeeded with a dropped response would double-apply on retry, duplicating an `append`, replaying a `patch` op, or re-running an arbitrary Obsidian command.

## Fixed

- **`ObsidianService.#request`** — POST/PATCH now bypass `withRetry` and propagate transient failures directly. Closes the data-corruption window on `obsidian_append_to_note`, `obsidian_patch_note` (`append`/`prepend`), `obsidian_execute_command`, `obsidian_open_in_ui`, and POST-mode `obsidian_search_notes`. Trade-off: those tools surface transient 5xx/network blips immediately rather than auto-retrying — agents must handle the first failure. Data integrity wins over single-call resilience for non-idempotent paths.

## Added

- **`RETRY_SAFE_METHODS`** — module-level `ReadonlySet<string>` of `{ GET, PUT, DELETE }`. Methods outside the set bypass the retry branch entirely.
- **`tests/services/obsidian-service.test.ts`** — 13 regression tests under `ObsidianService retry policy`, grouped into three describes:
  - **POST/PATCH never retry on transient failures** — `appendToNote` / `patchNote` / `executeCommand` / `openInUi` against 503/504 and raw network errors (`UND_ERR_SOCKET`, `ECONNRESET`).
  - **GET/PUT/DELETE retry on transient failures** — `getNoteContent` / `writeNote` / `deleteNote` against 503 and `ECONNRESET`, asserting the second attempt succeeds.
  - **Non-transient errors do not retry, regardless of method** — 404/400/500 surface immediately.
  - Helper `queueReplies(path, method, n, reply)` queues N identical intercepts so the attempt counter ticks per fetch (a single intercept would let retries fall through to "no intercept" and silently mask the bug).

## Changed

- **`tests/helpers.ts`** — exported `PathMatcher`, `DispatchOpts`, `DynamicReply`, `ReplyFn` (previously file-internal) so the retry-policy suite can type its intercept callbacks without redeclaring them.
- **`vitest` `^4.1.5` → `^4.1.6`** (devDependency).
