<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: William Shi</title>
    <description>The latest articles on DEV Community by William Shi (@williamshi666).</description>
    <link>https://dev.to/williamshi666</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4233340%2Fcb04009f-c9ca-43ae-a24c-69d59dc880cf.jpg</url>
      <title>DEV Community: William Shi</title>
      <link>https://dev.to/williamshi666</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/williamshi666"/>
    <language>en</language>
    <item>
      <title>Agent memory: where it lives, when it expires, how it is deleted</title>
      <dc:creator>William Shi</dc:creator>
      <pubDate>Sun, 08 Nov 2026 12:35:30 +0000</pubDate>
      <link>https://dev.to/williamshi666/agent-memory-where-it-lives-when-it-expires-how-it-is-deleted-1pph</link>
      <guid>https://dev.to/williamshi666/agent-memory-where-it-lives-when-it-expires-how-it-is-deleted-1pph</guid>
      <description>&lt;p&gt;You deleted the memory file. The agent still remembers.&lt;/p&gt;

&lt;p&gt;That is the first thing worth knowing about agent memory: the file you can see is rarely the thing the model reads. Behind it sits an index, and an index outlives its source unless something deletes both.&lt;/p&gt;

&lt;p&gt;Most writing about agent memory argues about retrieval quality. The questions that decide whether you can operate the system are duller: which store does the memory live in, what writes to it, when does it expire, and what is actually gone after a delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things get called memory
&lt;/h2&gt;

&lt;p&gt;A context window, a session log and a knowledge base share one word and almost nothing else.&lt;/p&gt;

&lt;p&gt;The context window is assembled per turn and discarded after. A session log is append-only and exists to reconstruct what happened. A knowledge base is durable, indexed and queried. Conflating them is how retention questions go unanswered: you delete "the memory", and you have deleted one of three.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it is written, and what rebuilds it
&lt;/h2&gt;

&lt;p&gt;Saving the memory file rebuilds the index. In one agent workbench I maintain, the file API calls a rebuild when the path is the memory file, and the rebuild re-reads the file and re-embeds what it finds.&lt;/p&gt;

&lt;p&gt;The interesting part is the condition. The guard hashes the file first, and the hash helper returns an empty string when the file does not exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_file_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get MD5 hash of MEMORY.md.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_maybe_rebuild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rebuild index if MEMORY.md has changed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;current_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_file_hash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_stored_hash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_hash&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;current_hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rebuild_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the two branches together. Editing the file produces a different non-empty hash, so the index is rebuilt. &lt;strong&gt;Deleting the file produces an empty hash, so the rebuild never runs&lt;/strong&gt; — and the previous index stays exactly where it was.&lt;/p&gt;

&lt;p&gt;That single conditional is the difference between "the user deleted their memory" and "the user deleted the file we read memory from".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code says
&lt;/h2&gt;

&lt;p&gt;The same question applies to spending. A memory system that queries on every turn pays for retrieval it already did. One companion app keeps a session snapshot with a TTL, a turn budget and a capacity cap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_RECALL_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_RECALL_MAX_TURNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SNAPSHOT_MAX_MEMORIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RECALL_TRIGGER_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;记得|还记得|记不记得|上次|上回|之前|以前|曾经|我说过|跟你说过|那天|当时|早先&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four constants decide how much of the conversation the model remembers for free. The trigger regex is written against Chinese phrases — 你还记得 (do you remember), 上次 (last time), 之前 (before) — because that is the language the product ships in. The lesson is not the language: when the user explicitly points at the past, the snapshot is bypassed and a real recall runs. Caching recall silently is how an assistant ends up saying "I don't have that" about something it was told an hour ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting is a verification problem, not a delete call
&lt;/h2&gt;

&lt;p&gt;The interesting part of deletion is not the call, it is the report. Delete functions usually count successes as "the gateway returned". That number is a claim about your code, not about the database.&lt;/p&gt;

&lt;p&gt;The version I trust counts the state after deletion. It re-lists the source of the memory and reports how many rows are still visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listBySourceConversation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stillThere&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listBySourceConversation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;visitorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visitorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;companionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;companionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stillThere&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A delete that returned successfully but left the row behind counts as nothing. Because the whole operation is summed as &lt;code&gt;scanned = skipped + deleted + failed&lt;/code&gt;, the arithmetic itself catches a candidate that was neither deleted nor reported as failed.&lt;/p&gt;

&lt;p&gt;There is one more field worth copying: whether the candidate list was exhaustive. A search-based lister is truncated at some limit, and past that limit "not found" and "does not exist" are indistinguishable. If the backend cannot list by source, the report says partial instead of cleared.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should not have memory at all
&lt;/h2&gt;

&lt;p&gt;Memory is a liability with a feature attached: it is personal data you now store, expire and delete on request. Three cases where the honest answer is no memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The task is stateless.&lt;/strong&gt; A formatter, a converter, a reviewer that takes a diff and returns comments: none of them benefit from remembering last week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot name the deletion procedure.&lt;/strong&gt; If the answer to "delete this user's memory" is "run the delete endpoint and hope", you have a compliance problem rather than a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The memory would only restate the recent conversation.&lt;/strong&gt; Long-term memory earns its cost on cross-session, cross-time facts. Inside one session the transcript already has them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The reverse view
&lt;/h2&gt;

&lt;p&gt;The opposite design is defensible too: make memory explicit and user-visible, a file the person can open and edit, with the index treated as a cache that may be rebuilt at any time. That trades recall quality for inspectability, and for a personal tool it is often the better trade — the user can see what the agent knows and correct it in a text editor.&lt;/p&gt;

&lt;p&gt;What does not work is the middle: an opaque store with a visible file in front of it, where the two can disagree and nobody can tell which one answered the question.&lt;/p&gt;

&lt;p&gt;Both questions — where it lives, and what survives a delete — have to be answerable in one sentence each. If they are not, the memory design is not finished.&lt;/p&gt;

&lt;p&gt;The full implementations are in the open-source repositories: &lt;a href="https://github.com/WilliamShi666/Deep-Whisper" rel="noopener noreferrer"&gt;Deep-Whisper&lt;/a&gt; for the snapshot and the deletion path, and &lt;a href="https://github.com/endtoend-ai/Company-Brain" rel="noopener noreferrer"&gt;Company-Brain&lt;/a&gt; for the knowledge-base side. The longer version of this article, with a file and line number on every claim, is at &lt;a href="https://aiengstore.com/blog/agent-memory" rel="noopener noreferrer"&gt;aiengstore.com/blog/agent-memory&lt;/a&gt;. More of these systems are at &lt;a href="https://aiengstore.com" rel="noopener noreferrer"&gt;aiengstore.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Human-in-the-loop approval: where the gate actually lives</title>
      <dc:creator>William Shi</dc:creator>
      <pubDate>Sun, 08 Nov 2026 11:04:38 +0000</pubDate>
      <link>https://dev.to/williamshi666/human-in-the-loop-approval-where-the-gate-actually-lives-1eph</link>
      <guid>https://dev.to/williamshi666/human-in-the-loop-approval-where-the-gate-actually-lives-1eph</guid>
      <description>&lt;p&gt;An agent that runs unattended will eventually reach a step that cannot be undone. A post goes out, a message is sent, a record is deleted. Human-in-the-loop approval is not a UI feature bolted on top — it is a decision about &lt;strong&gt;where a person stands relative to that step&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There are three common placements. They differ in what a mistake costs and what every run costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three placements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Act first, review the log.&lt;/strong&gt; The agent executes everything and appends each step to an event log. A person reads it afterwards. Nothing blocks, so the run stays cheap. Fine for reversible actions — a draft, a branch, a staging change. It fails on irreversible ones, because a published post turns the review into a record rather than a decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Approve before the step runs.&lt;/strong&gt; The run stops and waits. Nothing happens until a decision arrives. This costs latency and someone on call. If &lt;em&gt;every&lt;/em&gt; step waits, the agent becomes a form and people start approving without reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Approve in a separate queue.&lt;/strong&gt; Risky steps are lifted out of the run into a queue a person works through. Scales better than blocking one run at a time, but costs a second state machine: the queue and the run have to agree on what is pending, and a resumed run may act on a world that moved while it waited.&lt;/p&gt;

&lt;p&gt;I built the second one, and the interesting part is not the approve button — it is what parks the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate is five pieces
&lt;/h2&gt;

&lt;p&gt;A flag on the plan, the rule that sets it, the promise that parks the run, the decision that resolves it, and the event log that records both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plan is data.&lt;/strong&gt; Every task carries a lane, a day range and an approval flag, so the gate is decided when the plan is made rather than improvised while it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;lane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;research&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// why this task exists, in the model's words&lt;/span&gt;
&lt;span class="nl"&gt;estimated_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;    &lt;span class="c1"&gt;// [start day, end day], e.g. [1, 3]&lt;/span&gt;
&lt;span class="nl"&gt;requires_approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule is in the normaliser, not the prompt.&lt;/strong&gt; Anything in the publish lane requires approval, whatever the model returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`t-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requires_approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lane&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requires_approval&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the difference between a policy and a suggestion. A prompt that says "please be careful" is not a gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wait is a promise, not a poll.&lt;/strong&gt; Nothing spins, no worker is held:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;waitForApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingApproval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A decision resolves that promise.&lt;/strong&gt; Returning &lt;code&gt;false&lt;/code&gt; when nothing was waiting is what makes a double-approve a no-op instead of a race:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApprovalDecision&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pendingApproval&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingApproval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this build stops being a reference
&lt;/h2&gt;

&lt;p&gt;The pause lands on a scripted day — the runner checks &lt;code&gt;day === 12&lt;/code&gt; and raises the request against a fixed task. The flag is real and travels with the plan, but the timeline that plays it is compressed and scripted. It is not a scheduler that scans every task for a flag.&lt;/p&gt;

&lt;p&gt;The promise lives inside the process. The store is an in-memory map, so an approval pending across a restart is lost rather than recovered. That is a boundary of the demo, not a property to build on.&lt;/p&gt;

&lt;p&gt;Two things I would keep: the decision is an HTTP call that returns &lt;strong&gt;409&lt;/strong&gt; when the run is not waiting, and every event — approvals included — is appended with a sequence number. That is what makes the run replayable afterwards, which matters more than the approval itself. An approval you cannot audit is just a click.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other direction: remove the capability
&lt;/h2&gt;

&lt;p&gt;Blocking the run is one answer. The other is removing the capability entirely: the agent may read the queue and recommend, but the tool that approves is not in its toolset.&lt;/p&gt;

&lt;p&gt;The approving tool sits in a separate high-risk list the surrounding code uses, and the agent's allowed set simply does not contain it. The capability is removed rather than discouraged.&lt;/p&gt;

&lt;p&gt;That is a stronger guarantee than any instruction in a system prompt, and it is cheaper — no state machine, no pending promises. It only works when the decision is easy to separate from the judgement. For "should this fact be approved", it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;Both systems I quoted are open source, so you can read the rest yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/endtoend-ai/deepseek-harness-desktop-plus" rel="noopener noreferrer"&gt;DeepSeek Harness Desktop&lt;/a&gt;&lt;/strong&gt; — the desktop agent runtime. Session-level permission presets (Read Only / Workspace Write / Full access) rather than a prompt asking the model to be careful, crash recovery around the loop, and the function-call wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/endtoend-ai/Company-Brain" rel="noopener noreferrer"&gt;Company Brain&lt;/a&gt;&lt;/strong&gt; — the knowledge-graph RAG stack the review tool comes from. Entities and relations in PostgreSQL + Apache AGE, three retrieval engines in one workspace, cited answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are part of a larger collection at &lt;a href="https://aiengstore.com" rel="noopener noreferrer"&gt;aiengstore.com&lt;/a&gt; — full source for the systems most teams end up rebuilding. Alongside the open-source ones it covers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents and orchestration&lt;/strong&gt; — goal-driven runners that plan, pause for human approval and replay every decision; multi-agent routing where the hard part is deciding which agent owns a task boundary, not making them talk; long-running operations agents with event-sourced logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrieval and knowledge&lt;/strong&gt; — hybrid retrieval that fuses dense and lexical ranking (and why BM25 tokenisation quietly breaks on product codes and non-Latin text); GraphRAG that survives a real corpus instead of a demo one; agent memory that addresses the two questions tutorials skip — when does it get rebuilt, and what actually remains after a delete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documents and extraction&lt;/strong&gt; — review that returns page coordinates rather than a summary, so a finding can be located; NL-to-SQL with the schema reasoning kept visible; document pipelines that handle scanned PDFs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics and internal tools&lt;/strong&gt; — operational dashboards over agent runs, knowledge bases that stay queryable as they grow.&lt;/p&gt;

&lt;p&gt;The written guides go deeper than this post in exactly one way: every claim carries a file and line number you can open. If you want the version of this article with all the citations, it is at &lt;a href="https://aiengstore.com/blog/human-approval" rel="noopener noreferrer"&gt;aiengstore.com/blog/human-approval&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
