{"id":6681,"date":"2026-07-27T14:24:48","date_gmt":"2026-07-27T14:24:48","guid":{"rendered":"https:\/\/dialnexa.com\/blogs\/?p=6681"},"modified":"2026-07-27T14:48:58","modified_gmt":"2026-07-27T14:48:58","slug":"production-ai-agent-reliability","status":"publish","type":"post","link":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/","title":{"rendered":"The Production Engineering Behind an Agent That Never Just Stops"},"content":{"rendered":"<nav class=\"alfred-series-nav\" style=\"margin:0 0 2.25rem;padding:1.25rem 1.5rem;border:1px solid #e0e7ff;border-left:4px solid #6366f1;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .6rem;font-size:.78rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#4f46e5;\">Building Alfred \u00b7 a six part series<\/p>\n<ol style=\"margin:0;padding-left:1.25rem;font-size:.95rem;line-height:1.75;\">\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/company-second-brain-architecture\/\">Why a second brain is a system, not a chat box<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/grounding-ai-agent-company-data\/\">Grounding: accessing company reality safely<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/\">Memory as a governance problem<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-graduated-agency\/\">Graduated agency<\/a><\/li>\n<li><strong>Production reliability<\/strong> <span style=\"color:#6b7280;font-size:.85rem;\">\u00b7 you are here<\/span><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-learning-loop\/\">The learning loop<\/a><\/li>\n<\/ol>\n<\/nav>\n<p><em>Building Alfred, Part 5 of 6. <a href=\"https:\/\/dialnexa.com\/blogs\/company-second-brain-architecture\/\">Parts 1 to 4 covered architecture, grounding, memory and agency<\/a>. This one is about the code that exists purely because production is hostile.<\/em><\/p>\n<p>A user starts a difficult investigation, watches the first few steps, and closes the laptop.<\/p>\n<p>The browser connection disappears. The work should not.<\/p>\n<p>That expectation is easy to underestimate, because almost every AI demo is synchronous. A request enters, tokens stream, an answer appears, the browser stayed open, every tool behaved.<\/p>\n<p>Production is a different place. Networks drop. Deployments restart processes. Tools time out. Users send two requests in three seconds. Context windows fill. A model requests a tool one instant before the process dies. Scheduled work wakes up after two days of downtime.<\/p>\n<p>The parts of Alfred that made early demos exciting were the tools and the answers. The parts that made people depend on it are locks, replay, repair, compaction and honest state.<\/p>\n<figure class=\"alfred-diagram\" style=\"margin:2.5rem 0;\">\n<svg viewBox=\"0 0 760 320\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:100%;height:auto;display:block;font-family:Inter,system-ui,sans-serif;\" role=\"img\" aria-label=\"Timeline showing a detached agent run surviving a client disconnect and a process restart, with unpaired tool calls repaired before resuming\">\n<rect x=\"1\" y=\"1\" width=\"758\" height=\"318\" rx=\"12\" fill=\"#f8f7ff\" stroke=\"#e0e7ff\"\/>\n<text x=\"24\" y=\"34\" fill=\"#4f46e5\" font-size=\"12.5\" font-weight=\"700\">RUN, a detached task that owns the session lock<\/text>\n<rect x=\"24\" y=\"48\" width=\"712\" height=\"46\" rx=\"8\" fill=\"#4f46e5\"\/>\n<text x=\"40\" y=\"76\" fill=\"#c7d2fe\" font-size=\"11\">tool<\/text>\n<text x=\"130\" y=\"76\" fill=\"#c7d2fe\" font-size=\"11\">tool<\/text>\n<text x=\"228\" y=\"76\" fill=\"#c7d2fe\" font-size=\"11\">tool_use persisted<\/text>\n<text x=\"400\" y=\"76\" fill=\"#c7d2fe\" font-size=\"11\">repair history<\/text>\n<text x=\"560\" y=\"76\" fill=\"#c7d2fe\" font-size=\"11\">answer delivered<\/text>\n<g stroke=\"#ffffff\" stroke-opacity=\"0.4\" stroke-width=\"1\">\n<line x1=\"118\" y1=\"48\" x2=\"118\" y2=\"94\"\/><line x1=\"216\" y1=\"48\" x2=\"216\" y2=\"94\"\/>\n<line x1=\"388\" y1=\"48\" x2=\"388\" y2=\"94\"\/><line x1=\"548\" y1=\"48\" x2=\"548\" y2=\"94\"\/>\n<\/g>\n<text x=\"24\" y=\"130\" fill=\"#6b7280\" font-size=\"12.5\" font-weight=\"700\">CLIENT, a follower that is never the owner<\/text>\n<rect x=\"24\" y=\"144\" width=\"124\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"86\" y=\"166\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"11.5\">SSE attached<\/text>\n<rect x=\"160\" y=\"144\" width=\"196\" height=\"34\" rx=\"7\" fill=\"#fef2f2\" stroke=\"#fecaca\"\/>\n<text x=\"258\" y=\"166\" text-anchor=\"middle\" fill=\"#b91c1c\" font-size=\"11.5\">tab closed, run continues<\/text>\n<rect x=\"384\" y=\"144\" width=\"200\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"484\" y=\"166\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"11.5\">reattach from last event id<\/text>\n<rect x=\"596\" y=\"144\" width=\"140\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"666\" y=\"166\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"11.5\">buffer replayed<\/text>\n<line x1=\"370\" y1=\"42\" x2=\"370\" y2=\"186\" stroke=\"#ef4444\" stroke-width=\"2\" stroke-dasharray=\"6 4\"\/>\n<text x=\"370\" y=\"204\" text-anchor=\"middle\" fill=\"#ef4444\" font-size=\"11.5\" font-weight=\"700\">process restart<\/text>\n<rect x=\"24\" y=\"222\" width=\"712\" height=\"76\" rx=\"8\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"40\" y=\"244\" fill=\"#1f2937\" font-size=\"12\" font-weight=\"700\">On resume<\/text>\n<text x=\"40\" y=\"264\" fill=\"#6b7280\" font-size=\"11.5\">heal_stale_runs clears the lock, then an unpaired tool_use gets a synthetic error<\/text>\n<text x=\"40\" y=\"281\" fill=\"#6b7280\" font-size=\"11.5\">result in memory. Persisted history is untouched, and a turn that already streamed<\/text>\n<text x=\"40\" y=\"296\" fill=\"#6b7280\" font-size=\"11.5\">text to a human is never replayed.<\/text>\n<\/svg><figcaption style=\"margin-top:.75rem;font-size:.9rem;color:#6b7280;text-align:center;\">The run outlives the connection. A restart mid tool call leaves an orphaned block, which is repaired in memory rather than in the persisted history.<\/figcaption><\/figure>\n<h2>Separate the run from the connection<\/h2>\n<p>The first architectural decision: the work is not the request.<\/p>\n<p>When a message arrives, <code>RunRegistry<\/code> creates a detached task:<\/p>\n<pre class=\"wp-block-code\"><code>asyncio.create_task(self._drive(handle, events), name=f\"alfred-run-{session_id}\")<\/code><\/pre>\n<p>The registry owns the run. Server-sent events stream to whoever is connected, and connected clients are followers, not owners. Closing a tab becomes a presentation event rather than an execution failure.<\/p>\n<p><code>_drive<\/code> guarantees a terminal event on every path. An exception emits an error event then a done event. Cancellation emits a done event with a cancelled reason. The comment explains why this is not optional: without a terminal event, followers reattach forever.<\/p>\n<p>Events land in an in-memory buffer, and followers iterate by index waiting on an <code>asyncio.Condition<\/code>, so any number of connections can attach and detach mid-run. Every SSE frame carries <code>id: {buffer index}<\/code>, and reattaching is a query parameter:<\/p>\n<pre class=\"wp-block-code\"><code>GET \/alfred\/sessions\/{session_id}\/stream?after=&lt;last id&gt;<\/code><\/pre>\n<p>Heartbeats every 15 seconds keep idle connections alive, and the response sets <code>X-Accel-Buffering: no<\/code> so nginx does not helpfully buffer a stream into uselessness.<\/p>\n<p>Finished handles linger for 120 seconds via <code>loop.call_later<\/code>, so a client that drops during the final token can still drain the tail.<\/p>\n<p>Two honest notes. The buffer cap is 50,000 events, at which point <code>overflowed<\/code> is set and appending stops. Nothing reads that flag, so the documented &#8220;follower refetches the persisted transcript&#8221; fallback is not actually implemented. And using <code>?after=<\/code> rather than the standard <code>Last-Event-ID<\/code> header means a browser&#8217;s native EventSource auto-reconnect resumes from scratch unless the client tracks the id itself.<\/p>\n<h2>The lock, and its fallback<\/h2>\n<p>Two simultaneous requests in one conversation corrupt ordering. So there is a per-session lock:<\/p>\n<pre class=\"wp-block-code\"><code>redis.set(f\"alfred:run:{session_id}\", \"1\", nx=True, ex=self._ttl)   # ttl 900s<\/code><\/pre>\n<p>On any Redis exception it falls through to a database compare-and-set:<\/p>\n<pre class=\"wp-block-code\"><code>UPDATE chat_sessions SET status = 'running' ... WHERE id = %s AND status = 'idle' RETURNING id<\/code><\/pre>\n<p>The TTL matters because a dead worker must not own a lock forever. At startup, <code>heal_stale_runs<\/code> flips every <code>running<\/code> session back to <code>idle<\/code> and scans and deletes every <code>alfred:run:*<\/code> key.<\/p>\n<p>Both of those docstrings say the same thing: this is only correct because the deployment is single instance. That is worth stating rather than hiding. The fallback is per call, not sticky, so a flapping Redis means some sessions hold a Redis lock and others hold a database lock, and a session locked only in Postgres is still acquirable via Redis. On one process, benign. On three, a bug waiting for traffic.<\/p>\n<p>Above the session lock sits a per-user cap of three parallel runs, and the code is candid about its precision:<\/p>\n<blockquote>\n<p>a race between simultaneous sends can briefly overshoot by one; the cap is a guardrail against runaway spend, not a hard scheduler.<\/p>\n<\/blockquote>\n<h2>Cancellation is not the same as a restart<\/h2>\n<p>Users need a real stop button. Cancellation cancels the driving task, the loop catches <code>CancelledError<\/code>, and the partial assistant text is persisted with a visible marker appended.<\/p>\n<p>But a deployment restart must not be recorded as a user cancellation. So there is a module-level flag:<\/p>\n<pre class=\"wp-block-code\"><code>_PROCESS_SHUTTING_DOWN = False\n\ndef mark_process_shutting_down() -&gt; None: ...<\/code><\/pre>\n<p>When it is set, a cancelled loop does <strong>not<\/strong> persist partial text as the final assistant message, which leaves the history at a resumable point instead of a finished-looking one.<\/p>\n<p>Shutdown is ordered deliberately: Repeat worker, then Discord, then a 3 second grace for live runs, then the relay consumer, then Redis, then the digest and background workers. Stop scheduling new work before tearing down delivery.<\/p>\n<p>Four distinct states, four different meanings: user stop, model failure, connector failure, service restart. Collapsing them into &#8220;agent failed&#8221; is how operational trust dies.<\/p>\n<h2>Keeping tool calls paired<\/h2>\n<p>The Messages API expects a <code>tool_use<\/code> block to be followed by a matching <code>tool_result<\/code>. An interrupted run can leave an orphan, which makes the next API call structurally invalid. Your conversation is now permanently broken, and the user just sees an error.<\/p>\n<p>Three repair functions handle this, and the distinction between them is the interesting part.<\/p>\n<p><code>repair_unpaired_tool_uses<\/code> injects a synthetic error result so the history is valid, with text that tells the model the truth:<\/p>\n<blockquote>\n<p>This tool call was interrupted by a server restart before it finished.<\/p>\n<\/blockquote>\n<p><code>repair_unpaired_mcp_tool_uses<\/code> does the same for server-side connector blocks. <code>downgrade_unavailable_mcp_blocks<\/code> rewrites blocks referencing a connector that has since been revoked.<\/p>\n<p>All three operate <strong>in memory only<\/strong>. The persisted history stays untouched. A valid paused turn is not rewritten just because it looks incomplete in isolation, and a resumable server-side turn continues unchanged.<\/p>\n<p>That distinction is the whole design. Recovery code should fix broken history without destroying a legitimate protocol state, and the only way to tell those apart is to understand the protocol rather than pattern-match on shape.<\/p>\n<p>There is a matching rule at the stream layer:<\/p>\n<pre class=\"wp-block-code\"><code>if produced_output or not retryable:\n    raise<\/code><\/pre>\n<p>Up to four retries on <code>{408, 409, 429, 500, 502, 503, 504, 529}<\/code> with exponential backoff capped at 20 seconds, and a hard refusal to replay a turn that already streamed text to a human. Duplicate partial output is worse than a clean error.<\/p>\n<h2>Compaction has to respect the protocol<\/h2>\n<p>Eventually a useful conversation exceeds the model&#8217;s comfortable context.<\/p>\n<p>The trigger is measured after each response as <code>input_tokens + cache_read_input_tokens + cache_creation_input_tokens<\/code>, against a threshold of 150,000. It sets a flag, and compaction runs <strong>after<\/strong> the turn ends rather than mid-turn.<\/p>\n<p>The boundary is the part that took thought. Compaction keeps the four most recent user turns verbatim and summarises everything older. The boundary must be a real user turn, defined as:<\/p>\n<pre class=\"wp-block-code\"><code>role == \"user\" and any(block.get(\"type\") == \"text\" for block in blocks)<\/code><\/pre>\n<p>That text-block requirement is doing real work. In the Anthropic API, <code>tool_result<\/code> messages also carry <code>role: \"user\"<\/code>. Splitting on those would separate a <code>tool_use<\/code> from its result and the API would reject the replayed history. Naive history trimming produces exactly this bug, and it appears as a mysterious 400 several turns later.<\/p>\n<p>Compaction also refuses to move the boundary backwards. Tool results are rendered at 1,500 characters each, the transcript is tail-clipped at 60,000, and the summariser is <code>claude-haiku-4-5<\/code> with a 2,000 token cap, billed to the same session and user under <code>purpose = 'compact'<\/code> so it does not hide inside the answer&#8217;s cost.<\/p>\n<p>The whole compaction body is wrapped in a try\/except that only warns. A failed compaction silently leaves the session uncompacted and retriggers next turn, which is the correct failure mode: degraded, not broken.<\/p>\n<p>One real bug worth naming: compaction is skipped for any budgeted scheduled run, and budgeted scheduled runs are exactly the longest-running sessions in the system.<\/p>\n<h2>Caching and compaction solve different problems<\/h2>\n<p>These get conflated constantly, so, plainly: compaction controls how much history remains. Caching controls how often you pay full price for the history that stays.<\/p>\n<p>The stable system prompt gets a cache breakpoint. The volatile tail (identity, date, per-query memory, connector lines, skills) deliberately does not, because content that changes every turn pays the 1.25x write premium every time and never collects the 0.1x read discount.<\/p>\n<p>The usage ledger tracks <code>input_tokens<\/code>, <code>output_tokens<\/code>, <code>cache_creation_input_tokens<\/code> and <code>cache_read_input_tokens<\/code> as separate columns, which is what makes that trade measurable rather than a matter of opinion.<\/p>\n<h2>Scheduled autonomy: Alfred Repeat<\/h2>\n<p>A Repeat task stores a schedule, a timezone, instructions and run state. This is where the boring correctness problems live.<\/p>\n<p>The schedule shape is enforced in SQL, not just Python:<\/p>\n<pre class=\"wp-block-code\"><code>CHECK (\n  (\"frequency\" = 'daily'   AND \"day_of_week\" IS NULL     AND \"day_of_month\" IS NULL)\n  OR (\"frequency\" = 'weekly'  AND \"day_of_week\" IS NOT NULL AND \"day_of_month\" IS NULL)\n  OR (\"frequency\" = 'monthly' AND \"day_of_week\" IS NULL     AND \"day_of_month\" IS NOT NULL)\n)<\/code><\/pre>\n<p>Storage is <code>TIMESTAMPTZ<\/code> in UTC. Recurrence is computed in the task&#8217;s zone from a local <code>TIME<\/code> plus a <code>ZoneInfo<\/code>, defaulting to <code>Asia\/Kolkata<\/code>. Two classic bugs are handled explicitly.<\/p>\n<p><strong>Nonexistent local times.<\/strong> A daylight saving gap means the naive local datetime you constructed does not exist. The fix is a round trip: build naive in zone, then <code>astimezone(utc).astimezone(zone)<\/code>, which normalises forward out of the gap. IST has no DST, so this path only matters for non-IST tasks, which is precisely why it would have rotted unnoticed if it were not written down.<\/p>\n<p><strong>Months that do not have your date.<\/strong> The occurrence is clamped with <code>min(day_of_month, calendar.monthrange(year, month)[1])<\/code>, and critically, advancing to the next month re-clamps from the <strong>original<\/strong> <code>day_of_month<\/code>. A task set for the 31st fires on 28 February and then goes back to 31 March, instead of silently drifting to the 28th forever. Almost every hand-rolled monthly scheduler gets this wrong.<\/p>\n<p><strong>Downtime does not replay a backlog.<\/strong> Advancing loops <code>while candidate &lt;= local_now<\/code>, so a process offline for three days runs the oldest due occurrence once and jumps to the next future slot. No thundering herd of missed runs.<\/p>\n<h3>Claiming work exactly once<\/h3>\n<p>Enqueue is a compare-and-set on the task row:<\/p>\n<pre class=\"wp-block-code\"><code>UPDATE ... WHERE id = %s AND enabled AND deleted_at IS NULL AND next_run_at = %s RETURNING id<\/code><\/pre>\n<p>Losing the CAS inserts nothing. It then refuses to enqueue if the task already has a queued or running occurrence, so a slow run never overlaps its successor. The final structural guard is a unique constraint, <code>UNIQUE (task_id, scheduled_for)<\/code>, paired with <code>ON CONFLICT DO NOTHING<\/code>.<\/p>\n<p>Claiming is the standard pattern:<\/p>\n<pre class=\"wp-block-code\"><code>... FOR UPDATE SKIP LOCKED ...\nUPDATE ... SET status = 'running',\n               attempts = attempts + 1,\n               started_at = COALESCE(started_at, NOW()),\n               error = NULL\nRETURNING ...<\/code><\/pre>\n<p>Concurrency is an in-process budget of two, but safety comes from the database claim, not the counter. And on startup, every row left <code>running<\/code> by a dead process is flipped back to <code>queued<\/code> with <code>error = 'Recovered after a Batman restart'<\/code>.<\/p>\n<p>Occurrence rows denormalise the folder name, task name, persona, instructions and timezone at enqueue time, so editing a task does not rewrite history for a run already in flight.<\/p>\n<h3>Budget as a first-class scheduling concern<\/h3>\n<p>Each occurrence can carry a rupee budget, converted once against a configured rate, with the current session spend as a starting point so a continued occurrence resumes against cumulative spend rather than zero. If spend already exceeds the budget, the occurrence finishes <code>budget_exhausted<\/code> before a single model call.<\/p>\n<p>In-loop enforcement is pre-emptive rather than reactive. Before each model call it counts tokens, derives an affordable output ceiling, and shrinks <code>max_tokens<\/code> so the call cannot breach the cap. If real billing still beats the estimate, unexecuted tool calls receive:<\/p>\n<blockquote>\n<p>Not executed because the Repeat run budget was exceeded.<\/p>\n<\/blockquote>\n<p>An explicit no-side-effects-after-the-breach path.<\/p>\n<p>Hitting <code>max_iterations<\/code> inside a scheduled run is not a failure. It raises a continuation, and the occurrence is requeued one second later to continue in the next segment.<\/p>\n<h3>Notifications that cannot be lost<\/h3>\n<p>The terminal status and the notification intent are written in the <strong>same transaction<\/strong>, so a completed occurrence cannot exist without a pending notification. Delivery then uses a lease: claim with <code>FOR UPDATE SKIP LOCKED<\/code>, push the next attempt time forward, deliver, mark done. A crash mid-delivery self-heals when the lease expires.<\/p>\n<p>Two honest gaps: neither occurrence retries nor notification retries are capped, and backoff is a fixed 60 seconds rather than exponential. A permanently broken dependency will retry forever at one minute intervals. Notification text also defends against mention injection by zero-width-joining <code>@<\/code> in user-supplied task and folder names, which is the kind of detail you only add after someone names a task <code>@everyone<\/code>.<\/p>\n<h2>Discord, where idempotency gets serious<\/h2>\n<p>Discord is the surface that taught us the most about duplicate delivery.<\/p>\n<p>A mention creates a row keyed by <code>trigger_message_id<\/code> with <code>ON CONFLICT DO NOTHING<\/code>; losing the race re-selects the existing row and returns <code>created=False<\/code>, so a gateway redelivery cannot produce a second invocation. The progress message id is written in the same INSERT, with a comment recording why:<\/p>\n<blockquote>\n<p>the worker can never claim an invocation whose Discord progress message is still unknown (that race produced duplicate progress messages)<\/p>\n<\/blockquote>\n<p>Per-session serialisation is in the claim query rather than in application logic:<\/p>\n<pre class=\"wp-block-code\"><code>AND NOT EXISTS (\n  SELECT 1 FROM discord_alfred_invocations active\n  WHERE active.session_id = i.session_id AND active.state = 'running'\n)<\/code><\/pre>\n<p>Parallelism across sessions, strict single-file within one.<\/p>\n<p>Final delivery is a high-water mark. The answer is chunked, the already-sent id list is re-read from the database, and anything below that index is skipped. The id list is persisted after <strong>each<\/strong> individual send, so a crash between chunk three and chunk four resumes at chunk four. PDF attachments share the same index space. Only then is the progress message deleted and the invocation marked complete.<\/p>\n<p>Image inputs get four layers: content type allowlist at snapshot time, a host allowlist of <code>cdn.discordapp.com<\/code> and <code>media.discordapp.net<\/code> at download time, a streaming abort on both declared and actual size, and a re-sniff of the media type from the actual bytes. That last one exists because Discord guesses content type from the filename extension and the Messages API rejects blocks whose bytes disagree. Any per-image failure degrades to a text block, <code>[Image skipped: {filename} \u2014 {reason}]<\/code>, so the model can tell the user what was dropped and why.<\/p>\n<p>Recovery flips every <code>running<\/code> invocation to <code>retryable<\/code>, then decides per invocation between continuing, repairing, or skipping the model entirely. That last case is the one I like: if the assistant&#8217;s answer already reached Postgres but delivery did not, it sends the persisted text without paying for the turn again. The answer survived; only the delivery failed.<\/p>\n<h2>Optional integrations should fail small<\/h2>\n<p>An internal AI depends on systems with different uptime and different deployment timelines. A missing browser connector or an unconfigured repository token should not take down chat for everyone.<\/p>\n<p>Optional subsystems load through <code>asyncio.gather(..., return_exceptions=True)<\/code> and each failure downgrades to &#8220;that feature is off.&#8221; Startup logs one line naming exactly what is missing:<\/p>\n<pre class=\"wp-block-code\"><code>[alfred] anthropic=... | web_search=... | code_tools=... | pdf_reports=OFF (weasyprint\/pango missing) | personal_gmail=...<\/code><\/pre>\n<p>The PDF tool is the cleanest example. <code>pdf_generation_available()<\/code> is a cached import probe, and if WeasyPrint or its Pango system libraries are absent, the tool simply disappears from the advertised schema list. The model never learns about a capability that would fail.<\/p>\n<h2>The ops story is thinner than the runtime story<\/h2>\n<p>Being consistent about honesty means saying where the engineering stops.<\/p>\n<p>The runtime has locks, leases, compare-and-sets, repair, replay and graceful degradation. The deployment pipeline has two stages: SSH and deploy, then post to Discord. No build stage, no test stage, no lint, no image registry.<\/p>\n<p>There is a substantial unit suite covering the repeat worker, the schedule maths, the run registry, compaction and the Discord worker. It never runs on deploy.<\/p>\n<p><code>\/health<\/code> returns a static literal. Neither compose file defines a <code>healthcheck<\/code>, and nothing curls <code>\/health<\/code> after deploying. There is no deploy verification gate.<\/p>\n<p>Startup migrations run under a Postgres advisory lock, which is right, and then swallow failure with <code>[migrations] Startup migration failed; continuing without it.<\/code> The app will boot against an un-migrated schema.<\/p>\n<p>There is one piece of migration code I would keep as a teaching example. A branch collision left some databases stamped with revision ids that no longer existed, which made every alembic command fail before doing anything. The repair back-stamps to the last common ancestor and re-walks, which is only safe because the intervening migrations are idempotent. The comment records the failure that made it necessary: leaving the implicit transaction open made alembic join it and silently roll the whole migration back at connection close, exiting zero with nothing applied. A migration runner that succeeds loudly and does nothing is worse than one that fails.<\/p>\n<p>And there is a diagnostic still live in a hot path:<\/p>\n<pre class=\"wp-block-code\"><code># DIAGNOSTIC: isolate connection-checkout cost (cold connect + check_connection)\n# from query time. ... Watch connections_num: if it climbs by 1 per request,\n# connections are being churned, not reused.\nlogger.info(\"write-pool acquire=%.1fms stats=%s\", acquire_ms, stats)<\/code><\/pre>\n<p>One INFO line per write connection acquisition. It was added to chase a real problem, where NAT and pooler timeouts silently killed idle connections and the first request after a quiet period hung for a minute and a half on a dead socket. That is why the pool now sets <code>check=AsyncConnectionPool.check_connection<\/code>, <code>max_idle=60<\/code>, <code>max_lifetime=600<\/code> and explicit TCP keepalives. The instrumentation should come out now that the fix is in.<\/p>\n<h2>The production lesson<\/h2>\n<p>Reliability is not a wrapper around an intelligent agent. It is what allows intelligence to become a habit.<\/p>\n<p>The failure modes that destroy trust are not wrong answers. They are a lost investigation, a duplicate email, a scheduled task that ran twice, and a system that cannot tell you which of those happened.<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-learning-loop\/\">Next, in Part 6<\/a>: the learning loop, and an uncomfortable finding about which parts of it actually close.<\/p>\n<p><em>Which failure mode would most quickly destroy trust in an agent used by your team?<\/em><\/p>\n<hr style=\"margin:2.5rem 0 1.5rem;border:0;border-top:1px solid #e5e7eb;\" \/>\n<div class=\"alfred-series-pager\" style=\"display:flex;flex-wrap:wrap;gap:1rem;\">\n<div style=\"flex:1 1 240px;padding:1rem 1.25rem;border:1px solid #e0e7ff;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .3rem;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#6b7280;\">Previous<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-graduated-agency\/\" style=\"font-weight:600;\">Graduated agency<\/a>\n<\/div>\n<div style=\"flex:1 1 240px;padding:1rem 1.25rem;border:1px solid #e0e7ff;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .3rem;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#6b7280;\">Next in the series<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-learning-loop\/\" style=\"font-weight:600;\">The learning loop<\/a>\n<\/div>\n<\/div>\n<p style=\"margin-top:1.5rem;font-size:.95rem;color:#4b5563;\">Alfred runs on <a href=\"https:\/\/dialnexa.com\/\">DialNexa<\/a>, our voice AI platform for businesses. If you are building internal agents and want to compare notes, the comments are open.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.<\/p>\n","protected":false},"author":1,"featured_media":6709,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[717],"tags":[46,5,3],"class_list":["post-6681","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-business-automation","tag-conversational-ai","tag-voice-ai"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Production AI Agents: Run Locks, Event Replay, History Repair and Compaction<\/title>\n<meta name=\"description\" content=\"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Production AI Agents: Run Locks, Event Replay, History Repair and Compaction\" \/>\n<meta property=\"og:description\" content=\"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/\" \/>\n<meta property=\"og:site_name\" content=\"DialNexa\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-27T14:24:48+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-27T14:48:58+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Aditya Kamat\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Aditya Kamat\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/\"},\"author\":{\"name\":\"Aditya Kamat\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/person\\\/1af38c86cbe30b471e5c350bfb15926c\"},\"headline\":\"The Production Engineering Behind an Agent That Never Just Stops\",\"datePublished\":\"2026-07-27T14:24:48+00:00\",\"dateModified\":\"2026-07-27T14:48:58+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/\"},\"wordCount\":2768,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-5-reliability-1.png\",\"keywords\":[\"business automation\",\"conversational ai\",\"Voice AI\"],\"articleSection\":[\"Engineering\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/\",\"name\":\"Production AI Agents: Run Locks, Event Replay, History Repair and Compaction\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-5-reliability-1.png\",\"datePublished\":\"2026-07-27T14:24:48+00:00\",\"dateModified\":\"2026-07-27T14:48:58+00:00\",\"description\":\"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#primaryimage\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-5-reliability-1.png\",\"contentUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-5-reliability-1.png\",\"width\":1200,\"height\":630,\"caption\":\"Building Alfred part 5: production reliability engineering for AI agents\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/production-ai-agent-reliability\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"The Production Engineering Behind an Agent That Never Just Stops\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#website\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/\",\"name\":\"DialNexa Blog\",\"description\":\"Voice AI insights, customer communication playbooks, sales automation guides, and contact center operations advice from DialNexa.\",\"publisher\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\",\"name\":\"DialNexa\",\"url\":\"https:\\\/\\\/dialnexa.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/cropped-cropped-favicon-300x300-1.png\",\"caption\":\"DialNexa\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/person\\\/1af38c86cbe30b471e5c350bfb15926c\",\"name\":\"Aditya Kamat\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"caption\":\"Aditya Kamat\"},\"description\":\"Co-Founder of DialNexa. Expert in voice AI, conversational technology, and enterprise telephony. Building the future of AI-powered customer engagement.\",\"sameAs\":[\"https:\\\/\\\/dialnexa.com\"],\"jobTitle\":\"Co-Founder\",\"url\":\"https:\\\/\\\/dialnexa.com\",\"worksFor\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Production AI Agents: Run Locks, Event Replay, History Repair and Compaction","description":"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/","og_locale":"en_US","og_type":"article","og_title":"Production AI Agents: Run Locks, Event Replay, History Repair and Compaction","og_description":"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.","og_url":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/","og_site_name":"DialNexa","article_published_time":"2026-07-27T14:24:48+00:00","article_modified_time":"2026-07-27T14:48:58+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png","type":"image\/png"}],"author":"Aditya Kamat","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Aditya Kamat","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#article","isPartOf":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/"},"author":{"name":"Aditya Kamat","@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/person\/1af38c86cbe30b471e5c350bfb15926c"},"headline":"The Production Engineering Behind an Agent That Never Just Stops","datePublished":"2026-07-27T14:24:48+00:00","dateModified":"2026-07-27T14:48:58+00:00","mainEntityOfPage":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/"},"wordCount":2768,"commentCount":0,"publisher":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#primaryimage"},"thumbnailUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png","keywords":["business automation","conversational ai","Voice AI"],"articleSection":["Engineering"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/","url":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/","name":"Production AI Agents: Run Locks, Event Replay, History Repair and Compaction","isPartOf":{"@id":"https:\/\/dialnexa.com\/blogs\/#website"},"primaryImageOfPage":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#primaryimage"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#primaryimage"},"thumbnailUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png","datePublished":"2026-07-27T14:24:48+00:00","dateModified":"2026-07-27T14:48:58+00:00","description":"Detached runs, Redis session locks with database fallback, SSE replay buffers, repairing unpaired tool calls after a restart, protocol-safe compaction, and timezone-correct scheduled autonomy.","breadcrumb":{"@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#primaryimage","url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png","contentUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-5-reliability-1.png","width":1200,"height":630,"caption":"Building Alfred part 5: production reliability engineering for AI agents"},{"@type":"BreadcrumbList","@id":"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/dialnexa.com\/blogs\/"},{"@type":"ListItem","position":2,"name":"The Production Engineering Behind an Agent That Never Just Stops"}]},{"@type":"WebSite","@id":"https:\/\/dialnexa.com\/blogs\/#website","url":"https:\/\/dialnexa.com\/blogs\/","name":"DialNexa Blog","description":"Voice AI insights, customer communication playbooks, sales automation guides, and contact center operations advice from DialNexa.","publisher":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/dialnexa.com\/blogs\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/dialnexa.com\/blogs\/#organization","name":"DialNexa","url":"https:\/\/dialnexa.com","logo":{"@type":"ImageObject","url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2025\/10\/cropped-cropped-favicon-300x300-1.png","caption":"DialNexa"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/person\/1af38c86cbe30b471e5c350bfb15926c","name":"Aditya Kamat","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","caption":"Aditya Kamat"},"description":"Co-Founder of DialNexa. Expert in voice AI, conversational technology, and enterprise telephony. Building the future of AI-powered customer engagement.","sameAs":["https:\/\/dialnexa.com"],"jobTitle":"Co-Founder","url":"https:\/\/dialnexa.com","worksFor":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"}}]}},"_links":{"self":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6681","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/comments?post=6681"}],"version-history":[{"count":4,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6681\/revisions"}],"predecessor-version":[{"id":6711,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6681\/revisions\/6711"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/media\/6709"}],"wp:attachment":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/media?parent=6681"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/categories?post=6681"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/tags?post=6681"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}