<?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: Jason Shen</title>
    <description>The latest articles on DEV Community by Jason Shen (@timetxt).</description>
    <link>https://dev.to/timetxt</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%2F1129362%2F553de093-d6bc-4223-9f8e-3b0b449ed0d5.jpg</url>
      <title>DEV Community: Jason Shen</title>
      <link>https://dev.to/timetxt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timetxt"/>
    <language>en</language>
    <item>
      <title>When Your Coding Agent Needs a Scribe, Not a Memory Engine</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Fri, 26 Jun 2026 04:58:25 +0000</pubDate>
      <link>https://dev.to/timetxt/when-your-coding-agent-needs-a-scribe-not-a-memory-engine-3bj</link>
      <guid>https://dev.to/timetxt/when-your-coding-agent-needs-a-scribe-not-a-memory-engine-3bj</guid>
      <description>&lt;p&gt;Over the past few weeks, I have had several conversations about the right way to give AI coding agents persistent memory.&lt;/p&gt;

&lt;p&gt;Some developers ask about AgentMemory. Others ask about Qiju, which I built and maintain.&lt;/p&gt;

&lt;p&gt;My usual response is: they are not solving the same problem. But I have not written that distinction down clearly.&lt;/p&gt;

&lt;p&gt;This post is my attempt to do that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same surface problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both tools address the same frustration.&lt;/p&gt;

&lt;p&gt;Every new coding session starts without context. The agent does not know which file is authoritative. It does not know what was decided in the previous session. It does not know which approaches have already failed or which assumptions have been verified.&lt;/p&gt;

&lt;p&gt;The developer spends the opening minutes of every session reconstructing what the previous session established.&lt;/p&gt;

&lt;p&gt;Both tools try to reduce that cost. But they approach it differently, and the difference matters in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What AgentMemory does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AgentMemory runs as a local server and hooks into the lifecycle events of your coding session automatically.&lt;/p&gt;

&lt;p&gt;With Claude Code, Codex, or another supported agent, it captures:&lt;/p&gt;

&lt;p&gt;every prompt submitted;&lt;br&gt;
every tool call the agent makes;&lt;br&gt;
every result the agent receives;&lt;br&gt;
session start and session end.&lt;br&gt;
Those observations are compressed using a language model, embedded using a local or cloud embedding model, and indexed with BM25 and vector search. At the start of the next session, the most relevant memories are retrieved automatically and injected into the conversation. You do not need to do anything.&lt;/p&gt;

&lt;p&gt;AgentMemory has published reproducible benchmark results: 95.2% recall accuracy on LongMemEval-S. Semantic retrieval can find "database performance" when you actually wrote "N+1 query fix." Keyword search cannot do that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Qiju does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Qiju does not capture anything automatically.&lt;/p&gt;

&lt;p&gt;When I want a record to persist, I or my agent intentionally invokes &lt;code&gt;/qiju-log&lt;/code&gt; inside Claude Code, or &lt;code&gt;$qiju-log&lt;/code&gt; inside Codex.&lt;/p&gt;

&lt;p&gt;That produces a structured handoff record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s"&gt;Selected token refresh strategy&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="s"&gt;auth, architecture&lt;/span&gt;
&lt;span class="na"&gt;search_terms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;refresh token, 401 retry&lt;/span&gt;
&lt;span class="na"&gt;next_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;implement bounded retry, add expiry regression test&lt;/span&gt;
&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="s"&gt;Decision, evidence, rejected alternatives, and handoff notes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The record is stored in plain JSONL files on the developer's machine. Nothing is embedded. Nothing leaves the machine. Retrieval is deterministic keyword and tag search.&lt;/p&gt;

&lt;p&gt;In the next session, the agent searches for what it needs. The developer sees exactly what was recorded and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real distinction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is tempting to describe AgentMemory as "automatic Qiju," or Qiju as "manual AgentMemory." That is not quite accurate.&lt;/p&gt;

&lt;p&gt;The two tools are answering different questions.&lt;/p&gt;

&lt;p&gt;AgentMemory asks: what did the agent observe during previous sessions?&lt;/p&gt;

&lt;p&gt;Qiju asks: what did the developer judge worth preserving about this project?&lt;/p&gt;

&lt;p&gt;An internal log of every tool call is not the same as a structured record of why a decision was made, what evidence supported it, and which approaches were already rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A memory engine accumulates what happened.
A record layer captures what matters.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the distinction is practical&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine a session in which I investigated an authentication bug, tried three approaches, found that the second one caused a regression, and decided to roll back the change and record a specific boundary condition in the architecture notes.&lt;/p&gt;

&lt;p&gt;An automatic memory capture would produce:&lt;/p&gt;

&lt;p&gt;observations of every file read;&lt;br&gt;
observations of every command run;&lt;br&gt;
observations of every test failure;&lt;br&gt;
a compressed session summary.&lt;br&gt;
That is useful background.&lt;/p&gt;

&lt;p&gt;A deliberate Qiju record would produce:&lt;/p&gt;

&lt;p&gt;the specific file that is the current ground truth;&lt;br&gt;
the decision and the reason for it;&lt;br&gt;
the two rejected approaches and why they failed;&lt;br&gt;
the next step for the following agent.&lt;br&gt;
The next agent does not need the full observational trace. It needs to know what was decided, where to find the evidence, and what not to repeat.&lt;/p&gt;

&lt;p&gt;Those are different artefacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A concrete example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the post I wrote about Codex's SQLite logging problem, I described this kind of record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ground truth:  design/pypi-packaging-execution-plan.md
Decision:      Use one canonical src/qiju package for source and PyPI installs.
Verified:      Wheel and sdist passed artifact inspection and installed-wheel smoke tests.
Rejected:      Renaming scripts to qiju only during the staging build.
Next:          Publish to TestPyPI and repeat the clean-install lifecycle test.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete session transcript might contain tens of thousands of words around those conclusions. The durable project record is much smaller.&lt;/p&gt;

&lt;p&gt;The next agent does not need the whole session. It needs the right record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The operational differences&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beyond philosophy, the two tools make different operational assumptions.&lt;/p&gt;

&lt;p&gt;AgentMemory requires:&lt;/p&gt;

&lt;p&gt;a Rust binary (iii-engine) running as a background daemon;&lt;br&gt;
Node.js installed;&lt;br&gt;
an embedding model, either local or from a configured API provider.&lt;br&gt;
If an external embedding provider is configured, content leaves the machine. The local &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; model avoids this but adds setup steps. AgentMemory supports over twenty agents including Claude Code, Codex, Cursor, Cline, Gemini CLI, and others. It runs on macOS, Linux, and Windows.&lt;/p&gt;

&lt;p&gt;Qiju requires:&lt;/p&gt;

&lt;p&gt;Python 3.11 or later;&lt;br&gt;
nothing else.&lt;br&gt;
Records stay in plain JSONL files on the developer's machine. They can be committed to git alongside the code. Qiju supports Claude Code, Codex, Kiro, and Cursor. It runs on macOS and Linux. Windows is not yet supported.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where they compose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I do not think of these tools as competitors.&lt;/p&gt;

&lt;p&gt;A team could run AgentMemory for automatic background capture — what the agent did, when, and in which files — and run Qiju for the intentional layer — what was decided and why.&lt;/p&gt;

&lt;p&gt;AgentMemory recalls what the agent did.&lt;/p&gt;

&lt;p&gt;Qiju records what the developer decided.&lt;/p&gt;

&lt;p&gt;That is a natural division of responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current limits of both&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AgentMemory's automation is valuable precisely because most developers will not log anything if the effort is nonzero. But automatic capture at PostToolUse granularity produces high volume. The compression step reduces it, but the signal-to-noise ratio depends on how well the LLM identifies what was important. The daemon and embedding pipeline add operational complexity.&lt;/p&gt;

&lt;p&gt;Qiju's intentional capture is a feature to some developers and a friction point to others. If you forget to log something, it is not recorded — though a Stop hook in your agent configuration can instruct the agent to run &lt;code&gt;qiju-log&lt;/code&gt; before the session closes, which reduces the chance of an unrecorded session. There is no semantic search. Retrieval depends on using good keywords and tags at log time. The record is only as good as the effort put into writing it.&lt;/p&gt;

&lt;p&gt;Neither tool solves all the problems. Both have been honest about their current limits.&lt;/p&gt;

&lt;p&gt;Neither tool solves all the problems. Both have been honest about their current limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The question of agent memory is not settled. The tooling is early and changing quickly.&lt;/p&gt;

&lt;p&gt;If zero-friction automatic capture matters most, and you are comfortable with the operational requirements, AgentMemory is worth exploring.&lt;/p&gt;

&lt;p&gt;If deliberate, auditable, file-based records matter most, and you want something with no background services and no external dependencies, Qiju fits that model better.&lt;/p&gt;

&lt;p&gt;If both matter — the tools compose.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>If You Disable Codex’s SQLite Logs, What Keeps Your Project Memory Alive?</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Tue, 23 Jun 2026 05:45:05 +0000</pubDate>
      <link>https://dev.to/timetxt/if-you-disable-codexs-sqlite-logs-what-keeps-your-project-memory-alive-4dfj</link>
      <guid>https://dev.to/timetxt/if-you-disable-codexs-sqlite-logs-what-keeps-your-project-memory-alive-4dfj</guid>
      <description>&lt;p&gt;Developers have recently been discussing Codex continuously writing large amounts of diagnostic data into:&lt;/p&gt;

&lt;p&gt;~/.codex/logs_2.sqlite&lt;/p&gt;

&lt;p&gt;The visible database may remain relatively small while its SQLite WAL file keeps receiving frequent writes in the background.&lt;/p&gt;

&lt;p&gt;Some users have reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;excessive SSD writes;&lt;/li&gt;
&lt;li&gt;rapidly growing WAL files;&lt;/li&gt;
&lt;li&gt;slower responses;&lt;/li&gt;
&lt;li&gt;lock contention;&lt;/li&gt;
&lt;li&gt;startup or session reliability problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One unofficial workaround is to add a SQLite trigger that ignores new inserts into the logging table.&lt;/p&gt;

&lt;p&gt;That may reduce the disk-writing problem.&lt;/p&gt;

&lt;p&gt;But it also exposes a deeper architectural question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you disable an agent’s internal logs, what preserves the memory required to continue the project?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Diagnostic logs and project memory are not the same thing&lt;/p&gt;

&lt;p&gt;An agent’s internal logs are primarily designed for the agent application itself.&lt;/p&gt;

&lt;p&gt;They may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network events;&lt;/li&gt;
&lt;li&gt;trace output;&lt;/li&gt;
&lt;li&gt;runtime state transitions;&lt;/li&gt;
&lt;li&gt;low-level debugging information;&lt;/li&gt;
&lt;li&gt;internal operational details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those logs can be useful for diagnosing Codex.&lt;/p&gt;

&lt;p&gt;But they are not necessarily what the next coding agent needs when it resumes your project tomorrow.&lt;/p&gt;

&lt;p&gt;The next agent usually needs something much smaller and more meaningful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which file is the current ground truth?&lt;/li&gt;
&lt;li&gt;What decision was made?&lt;/li&gt;
&lt;li&gt;Why was that decision made?&lt;/li&gt;
&lt;li&gt;Which approaches have already failed?&lt;/li&gt;
&lt;li&gt;What evidence has been verified?&lt;/li&gt;
&lt;li&gt;What remains unfinished?&lt;/li&gt;
&lt;li&gt;What should happen next?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is project continuity.&lt;/p&gt;

&lt;p&gt;It is not diagnostic telemetry.&lt;/p&gt;

&lt;p&gt;What happens when you block the logs?&lt;/p&gt;

&lt;p&gt;The trigger workaround attempts to stop new rows from being inserted into Codex’s local logging database.&lt;/p&gt;

&lt;p&gt;The intended operational effect is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce repeated SQLite writes;&lt;/li&gt;
&lt;li&gt;slow or stop WAL growth;&lt;/li&gt;
&lt;li&gt;reduce SSD pressure;&lt;/li&gt;
&lt;li&gt;potentially improve responsiveness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it is still an unofficial workaround that modifies Codex’s internal database behavior.&lt;/p&gt;

&lt;p&gt;It may reduce diagnostic visibility.&lt;/p&gt;

&lt;p&gt;It may behave differently after a future Codex schema migration.&lt;/p&gt;

&lt;p&gt;It may interfere with debugging.&lt;/p&gt;

&lt;p&gt;It should not be presented as a guaranteed or permanent fix.&lt;/p&gt;

&lt;p&gt;More importantly, it should not be treated as a project-memory strategy.&lt;/p&gt;

&lt;p&gt;Even if Codex logging worked perfectly, an internal diagnostic database would still be the wrong place to depend on for durable project continuity.&lt;/p&gt;

&lt;p&gt;Its schema belongs to Codex.&lt;/p&gt;

&lt;p&gt;Its retention policy belongs to Codex.&lt;/p&gt;

&lt;p&gt;Its format can change with Codex.&lt;/p&gt;

&lt;p&gt;Its purpose is to help Codex operate and diagnose itself—not to preserve the smallest verified context required by the next agent.&lt;/p&gt;

&lt;p&gt;The real performance gap is continuity performance&lt;/p&gt;

&lt;p&gt;When developers hear “agent performance,” they usually think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model quality;&lt;/li&gt;
&lt;li&gt;response speed;&lt;/li&gt;
&lt;li&gt;token usage;&lt;/li&gt;
&lt;li&gt;tool execution;&lt;/li&gt;
&lt;li&gt;latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is another kind of performance that matters just as much:&lt;/p&gt;

&lt;p&gt;How quickly can a new session recover the correct context and continue working?&lt;/p&gt;

&lt;p&gt;I call this continuity performance.&lt;/p&gt;

&lt;p&gt;A fast model that spends twenty minutes rediscovering decisions, reopening the wrong files, repeating failed experiments, or misunderstanding the project’s current state is not performing well at the project level.&lt;/p&gt;

&lt;p&gt;This becomes especially visible when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original session is gone;&lt;/li&gt;
&lt;li&gt;context has been compacted;&lt;/li&gt;
&lt;li&gt;the transcript is too large;&lt;/li&gt;
&lt;li&gt;the agent changes;&lt;/li&gt;
&lt;li&gt;local logs are cleared;&lt;/li&gt;
&lt;li&gt;an internal database is disabled;&lt;/li&gt;
&lt;li&gt;a tool changes its storage format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is not that every trace event disappeared.&lt;/p&gt;

&lt;p&gt;The problem is that the next agent no longer knows what matters.&lt;/p&gt;

&lt;p&gt;Where QiJu fits&lt;/p&gt;

&lt;p&gt;QiJu is now publicly available on PyPI:&lt;/p&gt;

&lt;p&gt;uv tool install qiju&lt;/p&gt;

&lt;p&gt;It is a local-first, lossless record layer for AI coding agents.&lt;/p&gt;

&lt;p&gt;It works with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code;&lt;/li&gt;
&lt;li&gt;Codex;&lt;/li&gt;
&lt;li&gt;Kiro;&lt;/li&gt;
&lt;li&gt;Cursor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;QiJu does not try to preserve every conversation line or every tool event.&lt;/p&gt;

&lt;p&gt;It intentionally records the smaller set of facts that allow work to continue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the ground-truth file or artifact;&lt;/li&gt;
&lt;li&gt;the decision that was made;&lt;/li&gt;
&lt;li&gt;the evidence supporting it;&lt;/li&gt;
&lt;li&gt;the result that was verified;&lt;/li&gt;
&lt;li&gt;the unresolved risk;&lt;/li&gt;
&lt;li&gt;the next action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A later Codex session—or Claude Code, Kiro, or Cursor—can search those records and continue from the correct state.&lt;/p&gt;

&lt;p&gt;The goal is not to reconstruct the entire old session.&lt;/p&gt;

&lt;p&gt;The goal is to help the next agent continue correctly.&lt;/p&gt;

&lt;p&gt;QiJu is not “memory”&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;QiJu does not make a model remember more.&lt;/p&gt;

&lt;p&gt;It creates an explicit record layer.&lt;/p&gt;

&lt;p&gt;Ordinary agent memory tries to answer:&lt;/p&gt;

&lt;p&gt;What does the model remember?&lt;/p&gt;

&lt;p&gt;QiJu tries to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why do we know this?&lt;/li&gt;
&lt;li&gt;Where is the proof?&lt;/li&gt;
&lt;li&gt;Who produced the result?&lt;/li&gt;
&lt;li&gt;Was it verified?&lt;/li&gt;
&lt;li&gt;What should happen next?&lt;/li&gt;
&lt;li&gt;Which agent should continue?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the work auditable and handoffable instead of trapped inside one tool’s internal recall.&lt;/p&gt;

&lt;p&gt;A useful way to think about it is:&lt;/p&gt;

&lt;p&gt;Codex logs tell Codex what happened internally.&lt;br&gt;
QiJu tells the next agent what matters for continuing the work.&lt;/p&gt;

&lt;p&gt;A practical example&lt;/p&gt;

&lt;p&gt;Imagine a Codex session has just completed a packaging migration.&lt;/p&gt;

&lt;p&gt;During the work, the agent discovered that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the public package must move to src/qiju/;&lt;/li&gt;
&lt;li&gt;pyproject.toml must remain the version authority;&lt;/li&gt;
&lt;li&gt;runtime JSON must be loaded through package resources;&lt;/li&gt;
&lt;li&gt;uninstalling the CLI must preserve user records;&lt;/li&gt;
&lt;li&gt;all supported coding-agent hosts must pass installed-wheel testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A complete transcript may contain tens of thousands of words around those conclusions.&lt;/p&gt;

&lt;p&gt;The durable project record can be much smaller:&lt;/p&gt;

&lt;p&gt;Ground truth:&lt;br&gt;
design/pypi-packaging-execution-plan.md&lt;br&gt;
Decision:&lt;br&gt;
Use one canonical src/qiju package for source and PyPI installs.&lt;br&gt;
Verified:&lt;br&gt;
Wheel and sdist passed artifact inspection and installed-wheel smoke tests.&lt;br&gt;
Rejected:&lt;br&gt;
Renaming scripts to qiju only during the staging build.&lt;br&gt;
Next:&lt;br&gt;
Publish to TestPyPI and repeat the clean-install lifecycle test.&lt;/p&gt;

&lt;p&gt;That is enough for the next agent to find the right file, understand the decision, and continue from the correct point.&lt;/p&gt;

&lt;p&gt;It does not need every trace message.&lt;/p&gt;

&lt;p&gt;It needs the right record.&lt;/p&gt;

&lt;p&gt;The new update problem&lt;/p&gt;

&lt;p&gt;Once QiJu is installed through PyPI, users can upgrade the CLI with:&lt;/p&gt;

&lt;p&gt;uv tool upgrade qiju&lt;/p&gt;

&lt;p&gt;But that creates another subtle problem.&lt;/p&gt;

&lt;p&gt;The CLI may be updated while the copied SKILL.md files inside existing projects remain stale.&lt;/p&gt;

&lt;p&gt;Those skills tell Claude Code, Codex, Kiro, and Cursor how to call QiJu.&lt;/p&gt;

&lt;p&gt;So upgrading only the executable is not enough.&lt;/p&gt;

&lt;p&gt;QiJu 0.5.3 adds:&lt;/p&gt;

&lt;p&gt;qiju update&lt;/p&gt;

&lt;p&gt;This refreshes the installed QiJu skills across registered projects.&lt;/p&gt;

&lt;p&gt;Typical usage:&lt;/p&gt;

&lt;p&gt;uv tool upgrade qiju&lt;br&gt;
qiju update&lt;/p&gt;

&lt;p&gt;You can also preview changes:&lt;/p&gt;

&lt;p&gt;qiju update --dry-run&lt;/p&gt;

&lt;p&gt;Refresh only one host:&lt;/p&gt;

&lt;p&gt;qiju update --host codex&lt;/p&gt;

&lt;p&gt;Or scan and backfill older projects that were created before the registry existed:&lt;/p&gt;

&lt;p&gt;qiju update --scan-projects&lt;/p&gt;

&lt;p&gt;This matters because continuity depends not only on preserving records, but also on keeping the agent integration itself current.&lt;/p&gt;

&lt;p&gt;How project registration works&lt;/p&gt;

&lt;p&gt;When you initialize QiJu in a project:&lt;/p&gt;

&lt;p&gt;cd /path/to/project&lt;br&gt;
qiju init --host codex&lt;/p&gt;

&lt;p&gt;QiJu registers the project location.&lt;/p&gt;

&lt;p&gt;Each project’s own:&lt;/p&gt;

&lt;p&gt;.qiju/config.json&lt;/p&gt;

&lt;p&gt;remains the source of truth for which hosts are enabled.&lt;/p&gt;

&lt;p&gt;The registry tells QiJu where projects are.&lt;/p&gt;

&lt;p&gt;The project config tells QiJu how each project is wired.&lt;/p&gt;

&lt;p&gt;That separation allows qiju update to refresh projects without relying only on heuristic filesystem scans.&lt;/p&gt;

&lt;p&gt;It also means projects outside standard search roots can still be updated, as long as they were registered during initialization.&lt;/p&gt;

&lt;p&gt;For older installs, one migration command is enough:&lt;/p&gt;

&lt;p&gt;qiju update --scan-projects&lt;/p&gt;

&lt;p&gt;After that, ordinary updates can remain fast and deterministic.&lt;/p&gt;

&lt;p&gt;What QiJu does not do&lt;/p&gt;

&lt;p&gt;QiJu does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fix Codex’s SQLite logging behavior;&lt;/li&gt;
&lt;li&gt;stop SSD writes by itself;&lt;/li&gt;
&lt;li&gt;replace Codex diagnostics;&lt;/li&gt;
&lt;li&gt;guarantee that the unofficial database trigger is safe;&lt;/li&gt;
&lt;li&gt;preserve the Codex conversation interface;&lt;/li&gt;
&lt;li&gt;reproduce every internal event from the previous session;&lt;/li&gt;
&lt;li&gt;silently ingest all transcripts or prompts;&lt;/li&gt;
&lt;li&gt;use embeddings or vector memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Capture is intentional.&lt;/p&gt;

&lt;p&gt;You—or the agent under your instruction—decide what is worth recording.&lt;/p&gt;

&lt;p&gt;That boundary is important.&lt;/p&gt;

&lt;p&gt;QiJu is not another background logger.&lt;/p&gt;

&lt;p&gt;It is the opposite: a deliberate, inspectable record of what matters.&lt;/p&gt;

&lt;p&gt;A real workflow&lt;/p&gt;

&lt;p&gt;Install QiJu:&lt;/p&gt;

&lt;p&gt;uv tool install qiju&lt;/p&gt;

&lt;p&gt;Connect it to a project:&lt;/p&gt;

&lt;p&gt;cd /path/to/project&lt;br&gt;
qiju init --host codex&lt;/p&gt;

&lt;p&gt;During the session, ask the agent to record what matters:&lt;/p&gt;

&lt;p&gt;$qiju-log&lt;/p&gt;

&lt;p&gt;Later, in another session or another agent:&lt;/p&gt;

&lt;p&gt;$qiju-search the last packaging decision&lt;/p&gt;

&lt;p&gt;After upgrading QiJu:&lt;/p&gt;

&lt;p&gt;uv tool upgrade qiju&lt;br&gt;
qiju update&lt;/p&gt;

&lt;p&gt;That keeps both the CLI and the installed agent skills current.&lt;/p&gt;

&lt;p&gt;Why this matters after disabling logs&lt;/p&gt;

&lt;p&gt;Suppose you apply the SQLite trigger workaround and Codex stops writing most of its local diagnostic events.&lt;/p&gt;

&lt;p&gt;Operationally, that may reduce disk pressure.&lt;/p&gt;

&lt;p&gt;But now the separation becomes clear:&lt;/p&gt;

&lt;p&gt;Codex diagnostics&lt;br&gt;
→ reduced or unavailable&lt;br&gt;
QiJu project records&lt;br&gt;
→ still available&lt;br&gt;
Ground-truth files&lt;br&gt;
→ still referenced&lt;br&gt;
Decisions&lt;br&gt;
→ still searchable&lt;br&gt;
Next steps&lt;br&gt;
→ still recoverable&lt;/p&gt;

&lt;p&gt;The next agent may not know every internal event from the previous Codex run.&lt;/p&gt;

&lt;p&gt;It does not need to.&lt;/p&gt;

&lt;p&gt;It needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what was decided;&lt;/li&gt;
&lt;li&gt;why;&lt;/li&gt;
&lt;li&gt;where the evidence is;&lt;/li&gt;
&lt;li&gt;what was verified;&lt;/li&gt;
&lt;li&gt;what to do next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how QiJu helps maintain continuity performance even when Codex’s internal logging is limited or cleared.&lt;/p&gt;

&lt;p&gt;The broader design lesson&lt;/p&gt;

&lt;p&gt;The Codex logging issue is a reminder that developers often mix several different kinds of memory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Diagnostics — what happened inside the application.&lt;/li&gt;
&lt;li&gt;Conversation history — what the user and agent said.&lt;/li&gt;
&lt;li&gt;Repository state — what currently exists in the codebase.&lt;/li&gt;
&lt;li&gt;Decision history — why the project reached its current state.&lt;/li&gt;
&lt;li&gt;Continuation context — what the next agent needs to do next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No single SQLite log, transcript, or Git repository fully represents all five.&lt;/p&gt;

&lt;p&gt;Git can show what changed, but not always why.&lt;/p&gt;

&lt;p&gt;A transcript may show why, but it can be too large and tied to one tool.&lt;/p&gt;

&lt;p&gt;Diagnostic logs show runtime behavior, but mostly contain noise from the project’s perspective.&lt;/p&gt;

&lt;p&gt;A durable agent workflow needs an explicit project record layer.&lt;/p&gt;

&lt;p&gt;Current limits&lt;/p&gt;

&lt;p&gt;QiJu is still in developer preview.&lt;/p&gt;

&lt;p&gt;At the time of writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it supports macOS and Linux;&lt;/li&gt;
&lt;li&gt;Windows is not yet supported or tested;&lt;/li&gt;
&lt;li&gt;capture is intentional rather than automatic;&lt;/li&gt;
&lt;li&gt;retrieval is deterministic keyword, tag, and regex search;&lt;/li&gt;
&lt;li&gt;there are no embeddings or semantic ranking;&lt;/li&gt;
&lt;li&gt;the CLI and host wiring may still evolve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those limits are deliberate.&lt;/p&gt;

&lt;p&gt;The current priority is reliability, inspectability, and ownership.&lt;/p&gt;

&lt;p&gt;My conclusion&lt;/p&gt;

&lt;p&gt;Blocking noisy Codex logs may be a reasonable temporary response to excessive disk writes, provided users understand that it is unofficial and may affect diagnostics.&lt;/p&gt;

&lt;p&gt;But the deeper conclusion is more important:&lt;/p&gt;

&lt;p&gt;Your project memory should not disappear when an agent’s internal logs are disabled, cleared, corrupted, or redesigned.&lt;/p&gt;

&lt;p&gt;Codex logs tell Codex what happened internally.&lt;/p&gt;

&lt;p&gt;QiJu records the ground truth, decisions, evidence, and next steps that another agent needs to continue.&lt;/p&gt;

&lt;p&gt;That separation lets you reduce dependence on internal diagnostic logs without sacrificing project continuity.&lt;/p&gt;

&lt;p&gt;QiJu is available now:&lt;/p&gt;

&lt;p&gt;uv tool install qiju&lt;/p&gt;

&lt;p&gt;Project page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/qiju/" rel="noopener noreferrer"&gt;https://pypi.org/project/qiju/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jasonshrepo/qiju" rel="noopener noreferrer"&gt;https://github.com/jasonshrepo/qiju&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star if you like QiJu(起居).&lt;/p&gt;

&lt;p&gt;The question I am most interested in is:&lt;/p&gt;

&lt;p&gt;If you stopped trusting your coding agent’s internal history tomorrow, what would you preserve so another agent could continue correctly?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>python</category>
    </item>
    <item>
      <title>From Prompting ChatGPT to Orchestrating AI Agents: Two Years as an Ordinary Engineer</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Sun, 21 Jun 2026 05:41:51 +0000</pubDate>
      <link>https://dev.to/timetxt/from-prompting-chatgpt-to-orchestrating-ai-agents-two-years-as-an-ordinary-engineer-1li7</link>
      <guid>https://dev.to/timetxt/from-prompting-chatgpt-to-orchestrating-ai-agents-two-years-as-an-ordinary-engineer-1li7</guid>
      <description>&lt;p&gt;Two years ago, my interaction with AI was mostly limited to asking ChatGPT questions and checking its knowledge cutoff.&lt;/p&gt;

&lt;p&gt;Today, I use multiple coding agents, connect company knowledge systems through MCP, run local models inside iOS applications, and maintain a project memory layer so different agents can continue each other’s work.&lt;/p&gt;

&lt;p&gt;I am not an AI researcher.&lt;/p&gt;

&lt;p&gt;I am an ordinary engineer who kept experimenting until AI gradually became part of both my development workflow and my daily life.&lt;/p&gt;

&lt;p&gt;This is not a history of the AI industry. It is a personal engineering retrospective: what changed, what did not, and what I learned while moving from chat-based assistance to agent-driven development.&lt;/p&gt;

&lt;p&gt;Stage 1: AI Was Impressive, but Unreliable&lt;/p&gt;

&lt;p&gt;My first experience with ChatGPT was similar to that of many people.&lt;/p&gt;

&lt;p&gt;The initial response was amazement.&lt;/p&gt;

&lt;p&gt;The second response was scepticism.&lt;/p&gt;

&lt;p&gt;It did not take long to discover that a language model could present incorrect information with extraordinary confidence. I learned the term “hallucination,” and asking about the model’s knowledge cutoff became part of my normal usage.&lt;/p&gt;

&lt;p&gt;I later completed introductory AI courses from Andrew Ng and Microsoft.&lt;/p&gt;

&lt;p&gt;I did not understand every technical detail, but those courses gave me enough context to recognise an important limitation:&lt;/p&gt;

&lt;p&gt;A model producing a plausible answer is not the same as a system producing a reliable result.&lt;/p&gt;

&lt;p&gt;That distinction became more important as I moved from asking questions to using AI to modify software.&lt;/p&gt;

&lt;p&gt;Stage 2: Cursor Turned AI Into a Development Partner&lt;/p&gt;

&lt;p&gt;For me, the real shift began with Cursor.&lt;/p&gt;

&lt;p&gt;Before Cursor, AI was primarily a place I went to ask for help.&lt;/p&gt;

&lt;p&gt;With Cursor, AI entered the development environment itself.&lt;/p&gt;

&lt;p&gt;One Christmas, I spent three days almost entirely at home, building with Cursor. At the end of those three days, I had created my first iOS application.&lt;/p&gt;

&lt;p&gt;Then I built another one.&lt;/p&gt;

&lt;p&gt;And another.&lt;/p&gt;

&lt;p&gt;Cursor made the feedback loop unusually short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Describe an idea.&lt;/li&gt;
&lt;li&gt;Generate an implementation.&lt;/li&gt;
&lt;li&gt;Run it.&lt;/li&gt;
&lt;li&gt;Observe the failure.&lt;/li&gt;
&lt;li&gt;Ask for a correction.&lt;/li&gt;
&lt;li&gt;Repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was my introduction to what later became widely described as Vibe Coding.&lt;/p&gt;

&lt;p&gt;The most important change was not that the model wrote code faster than I did.&lt;/p&gt;

&lt;p&gt;It was that ideas remained alive long enough to become prototypes.&lt;/p&gt;

&lt;p&gt;Before AI-assisted development, a small idea often died during setup, documentation searches, dependency configuration, or framework research.&lt;/p&gt;

&lt;p&gt;With AI, the cost of reaching the first working version dropped dramatically.&lt;/p&gt;

&lt;p&gt;Rules Were an Early Form of Context Engineering&lt;/p&gt;

&lt;p&gt;The early experience was also frustrating.&lt;/p&gt;

&lt;p&gt;The agent forgot previous decisions.&lt;/p&gt;

&lt;p&gt;It changed files outside the intended scope.&lt;/p&gt;

&lt;p&gt;It solved local problems by creating architectural ones.&lt;/p&gt;

&lt;p&gt;It occasionally behaved as if it had never seen the project before.&lt;/p&gt;

&lt;p&gt;The community’s response was to rediscover software engineering discipline.&lt;/p&gt;

&lt;p&gt;We began writing project rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the instructions before changing code.&lt;/li&gt;
&lt;li&gt;Explain the implementation plan first.&lt;/li&gt;
&lt;li&gt;Do not delete files without confirmation.&lt;/li&gt;
&lt;li&gt;Follow the existing architecture.&lt;/li&gt;
&lt;li&gt;Verify the result after making changes.&lt;/li&gt;
&lt;li&gt;Ask questions when requirements are ambiguous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time, this was often described as prompt engineering.&lt;/p&gt;

&lt;p&gt;Looking back, it was an early form of context engineering.&lt;/p&gt;

&lt;p&gt;We were not merely trying to write better sentences. We were trying to create a stable operating environment for a probabilistic system.&lt;/p&gt;

&lt;p&gt;Limited context made this harder. Long conversations regularly lost important information, and opening a new chat was part of the workflow.&lt;/p&gt;

&lt;p&gt;I even required the agent to refer to itself using a fixed name in every response. It was an imperfect but useful signal that the original instructions were still present.&lt;/p&gt;

&lt;p&gt;The implementation details have changed, but the underlying problem remains:&lt;/p&gt;

&lt;p&gt;An agent cannot reliably continue work unless the important context is explicit, accessible, and structured.&lt;/p&gt;

&lt;p&gt;AI Exposed My Missing Domain Knowledge&lt;/p&gt;

&lt;p&gt;While building my first applications, I noticed that my interfaces were consistently unattractive.&lt;/p&gt;

&lt;p&gt;Initially, I treated this as a model limitation.&lt;/p&gt;

&lt;p&gt;Later, I realised that the missing component was my own domain knowledge.&lt;/p&gt;

&lt;p&gt;AI could generate SwiftUI code, but it could not give me design judgement that I did not possess.&lt;/p&gt;

&lt;p&gt;It could implement a button, but I still needed to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why the button should be placed in a particular location.&lt;/li&gt;
&lt;li&gt;How much spacing creates visual hierarchy.&lt;/li&gt;
&lt;li&gt;What information a user needs before taking an action.&lt;/li&gt;
&lt;li&gt;Why an interface feels confusing even when every feature works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI reduced implementation friction, but it did not remove the need for expertise.&lt;/p&gt;

&lt;p&gt;In some ways, it made missing expertise more visible.&lt;/p&gt;

&lt;p&gt;When implementation is slow, it is easy to blame the tools.&lt;/p&gt;

&lt;p&gt;When implementation becomes fast, weak product decisions, unclear requirements, and poor design judgement become much harder to hide.&lt;/p&gt;

&lt;p&gt;Stage 3: Moving Beyond the Chat Interface&lt;/p&gt;

&lt;p&gt;As models and tools multiplied, I started using APIs and local inference rather than relying entirely on hosted chat products.&lt;/p&gt;

&lt;p&gt;I bought a reasonably powerful computer and began downloading models from Hugging Face.&lt;/p&gt;

&lt;p&gt;One model that surprised me was Kokoro TTS. Its relatively small footprint showed me how much useful capability could exist outside the largest hosted models.&lt;/p&gt;

&lt;p&gt;I later integrated text-to-speech into an iOS application using the Sherpa ecosystem.&lt;/p&gt;

&lt;p&gt;This introduced a new category of engineering problems.&lt;/p&gt;

&lt;p&gt;Running a model in a desktop experiment is very different from shipping it inside a mobile application.&lt;/p&gt;

&lt;p&gt;Suddenly, I had to care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model size.&lt;/li&gt;
&lt;li&gt;Memory usage.&lt;/li&gt;
&lt;li&gt;Startup latency.&lt;/li&gt;
&lt;li&gt;Packaging.&lt;/li&gt;
&lt;li&gt;Device compatibility.&lt;/li&gt;
&lt;li&gt;Offline execution.&lt;/li&gt;
&lt;li&gt;Failure handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This experience reinforced another lesson:&lt;/p&gt;

&lt;p&gt;A model demo is not a product.&lt;/p&gt;

&lt;p&gt;The engineering work begins when the model has to operate inside real constraints.&lt;/p&gt;

&lt;p&gt;Stage 4: Spec-Driven and Agent-Driven Development&lt;/p&gt;

&lt;p&gt;After spending a long time in free-form Vibe Coding loops, Kiro’s Spec mode stood out to me.&lt;/p&gt;

&lt;p&gt;It approached AI development through explicit requirements, design, tasks, and implementation steps.&lt;/p&gt;

&lt;p&gt;This felt important because code generation was no longer the main bottleneck.&lt;/p&gt;

&lt;p&gt;The real challenges had become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintaining the goal across a long-running task.&lt;/li&gt;
&lt;li&gt;Preserving constraints.&lt;/li&gt;
&lt;li&gt;Producing work that could be reviewed.&lt;/li&gt;
&lt;li&gt;Recording decisions.&lt;/li&gt;
&lt;li&gt;Enabling another developer or agent to continue.&lt;/li&gt;
&lt;li&gt;Recovering when the agent moved in the wrong direction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spec-driven development felt less magical than free-form prompting, but it felt more transferable to professional engineering.&lt;/p&gt;

&lt;p&gt;The same transition occurred with project instruction files, skills, goal modes, and agent loops.&lt;/p&gt;

&lt;p&gt;The industry moved from:&lt;/p&gt;

&lt;p&gt;“Generate this function.”&lt;/p&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;p&gt;“Understand this repository, follow these constraints, complete this goal, test the result, and report what changed.”&lt;/p&gt;

&lt;p&gt;That is a fundamentally different type of interaction.&lt;/p&gt;

&lt;p&gt;MCP Connected AI to Existing Systems&lt;/p&gt;

&lt;p&gt;The Model Context Protocol generated plenty of debate when it appeared.&lt;/p&gt;

&lt;p&gt;Some considered it a major architectural shift. Others saw it as an additional abstraction that might eventually be replaced.&lt;/p&gt;

&lt;p&gt;In my own work, MCP became useful for a practical reason:&lt;/p&gt;

&lt;p&gt;It offered a relatively lightweight way to connect agents to existing tools and data.&lt;/p&gt;

&lt;p&gt;I built an MCP integration for my company’s knowledge platform.&lt;/p&gt;

&lt;p&gt;The organisation already had nearly ten years of accumulated knowledge. The problem was not the absence of information. The problem was making that information available within an agent workflow.&lt;/p&gt;

&lt;p&gt;Once the knowledge platform was connected, the agent could retrieve information in the context of an actual task.&lt;/p&gt;

&lt;p&gt;This represented a major change in how I thought about AI.&lt;/p&gt;

&lt;p&gt;Two years earlier, I had asked a chatbot when its training knowledge ended.&lt;/p&gt;

&lt;p&gt;Now I was designing a system that gave an agent access to the organisation’s current knowledge.&lt;/p&gt;

&lt;p&gt;The focus had shifted from model intelligence to system design.&lt;/p&gt;

&lt;p&gt;Coding Is Easier, but Engineering Is Not&lt;/p&gt;

&lt;p&gt;Modern coding agents can produce remarkable amounts of working code.&lt;/p&gt;

&lt;p&gt;It is increasingly possible to define a goal, give an agent access to a repository, and let it iterate through implementation and testing.&lt;/p&gt;

&lt;p&gt;That can make programming appear almost trivial.&lt;/p&gt;

&lt;p&gt;My experience has been more complicated.&lt;/p&gt;

&lt;p&gt;The amount of manual typing has decreased.&lt;/p&gt;

&lt;p&gt;The need for engineering judgement has not.&lt;/p&gt;

&lt;p&gt;The work has moved toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defining goals precisely.&lt;/li&gt;
&lt;li&gt;Breaking large tasks into safe boundaries.&lt;/li&gt;
&lt;li&gt;Supplying relevant context.&lt;/li&gt;
&lt;li&gt;Preventing unnecessary changes.&lt;/li&gt;
&lt;li&gt;Reviewing architecture.&lt;/li&gt;
&lt;li&gt;Validating assumptions.&lt;/li&gt;
&lt;li&gt;Testing behaviour rather than trusting output.&lt;/li&gt;
&lt;li&gt;Diagnosing failures that cross multiple generated changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A successful build is not necessarily a correct product.&lt;/p&gt;

&lt;p&gt;A passing test suite is not proof that the requirement was understood.&lt;/p&gt;

&lt;p&gt;Generated code still becomes code that someone must own.&lt;/p&gt;

&lt;p&gt;AI has made implementation faster, but faster implementation increases the importance of deciding what should be implemented.&lt;/p&gt;

&lt;p&gt;Multi-Agent Development Created a Memory Problem&lt;/p&gt;

&lt;p&gt;I now regularly use more than one agent on a project.&lt;/p&gt;

&lt;p&gt;Claude may begin a task. Kiro or Codex may continue it. Another tool may take over when the first one reaches a limit or struggles with a particular type of work.&lt;/p&gt;

&lt;p&gt;This sounds like parallel engineering, but the handoff is often the weakest point.&lt;/p&gt;

&lt;p&gt;The next agent needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the original goal was.&lt;/li&gt;
&lt;li&gt;What has already been implemented.&lt;/li&gt;
&lt;li&gt;Which decisions were intentional.&lt;/li&gt;
&lt;li&gt;Which approaches failed.&lt;/li&gt;
&lt;li&gt;Which files are sensitive.&lt;/li&gt;
&lt;li&gt;What remains incomplete.&lt;/li&gt;
&lt;li&gt;What should not be changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that information, every handoff becomes a partial restart.&lt;/p&gt;

&lt;p&gt;To solve this problem for my own workflow, I built QiJu, written 起居 in Chinese.&lt;/p&gt;

&lt;p&gt;QiJu records project changes and provides a reusable memory layer for subsequent agent sessions.&lt;/p&gt;

&lt;p&gt;Without it, I spend significant time reconstructing context.&lt;/p&gt;

&lt;p&gt;With it, agent handoffs become closer to continuing a shared workflow rather than starting another isolated chat.&lt;/p&gt;

&lt;p&gt;I no longer think of multi-agent development as “using several AI tools.”&lt;/p&gt;

&lt;p&gt;The engineering challenge is creating continuity between them.&lt;/p&gt;

&lt;p&gt;The Most Important Change: Ideas Survive Longer&lt;/p&gt;

&lt;p&gt;A few days ago, I spent around thirty minutes building a floating stopwatch for macOS.&lt;/p&gt;

&lt;p&gt;It was a small tool I had wanted for a long time.&lt;/p&gt;

&lt;p&gt;The result was not technically significant, but the process represented something larger.&lt;/p&gt;

&lt;p&gt;Previously, that idea would probably have remained on a list.&lt;/p&gt;

&lt;p&gt;Building it would have required enough setup and research that the expected benefit did not justify the activation cost.&lt;/p&gt;

&lt;p&gt;With AI assistance, the distance between the idea and a usable first version was short enough that the idea survived.&lt;/p&gt;

&lt;p&gt;That is probably the biggest personal impact AI has had on me.&lt;/p&gt;

&lt;p&gt;It has not made every idea valuable.&lt;/p&gt;

&lt;p&gt;It has made experimentation cheap enough that more ideas get the opportunity to prove whether they are valuable.&lt;/p&gt;

&lt;p&gt;AI Expanded My Capabilities and Narrowed My Attention&lt;/p&gt;

&lt;p&gt;There has also been a personal cost.&lt;/p&gt;

&lt;p&gt;Because AI and software occupied so much of my attention, I realised that I was entering an information bubble.&lt;/p&gt;

&lt;p&gt;My daily reading became dominated by models, agents, context windows, IDEs, MCP servers, and product announcements.&lt;/p&gt;

&lt;p&gt;To counter that, I began using AI to generate a daily story about something outside my normal interests.&lt;/p&gt;

&lt;p&gt;History, art, society, individuals, and unfamiliar parts of the world became part of a collection I call “Stories Outside the Bubble.”&lt;/p&gt;

&lt;p&gt;The irony is obvious.&lt;/p&gt;

&lt;p&gt;AI expanded what I could build while narrowing what I noticed.&lt;/p&gt;

&lt;p&gt;Then I used AI to widen my attention again.&lt;/p&gt;

&lt;p&gt;What I Believe After Two Years&lt;/p&gt;

&lt;p&gt;Over these two years, my relationship with AI moved through several stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI as a chatbot.&lt;/li&gt;
&lt;li&gt;AI as a coding assistant.&lt;/li&gt;
&lt;li&gt;AI as an implementation agent.&lt;/li&gt;
&lt;li&gt;AI as a component connected to tools and knowledge.&lt;/li&gt;
&lt;li&gt;AI as part of a multi-agent engineering workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most valuable lesson is not that prompts are powerful or that coding agents are fast.&lt;/p&gt;

&lt;p&gt;It is that useful AI systems depend on everything around the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context.&lt;/li&gt;
&lt;li&gt;Domain knowledge.&lt;/li&gt;
&lt;li&gt;Memory.&lt;/li&gt;
&lt;li&gt;Tools.&lt;/li&gt;
&lt;li&gt;Constraints.&lt;/li&gt;
&lt;li&gt;Evaluation.&lt;/li&gt;
&lt;li&gt;Human judgement.&lt;/li&gt;
&lt;li&gt;Continuity between tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I remain an ordinary engineer.&lt;/p&gt;

&lt;p&gt;But AI is now embedded in how I build applications, use company knowledge, record project decisions, and explore new subjects.&lt;/p&gt;

&lt;p&gt;I do not know which models or development tools will still be dominant 24 months from now.&lt;/p&gt;

&lt;p&gt;I do expect the distance between an idea and an implementation to keep shrinking.&lt;/p&gt;

&lt;p&gt;The difficult part will remain deciding which ideas deserve to be implemented, designing systems that can be trusted, and taking responsibility for what the agents produce.&lt;/p&gt;

&lt;p&gt;After writing this retrospective by the ocean, I should probably continue taking the morning off.&lt;/p&gt;

&lt;p&gt;But I am already thinking about opening Claude Code again.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>Monitoring SES Email Events though Configuration Set with SNS, SQS, Lambda and CloudWatch Log Groups</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Fri, 01 Sep 2023 11:28:03 +0000</pubDate>
      <link>https://dev.to/timetxt/monitoring-ses-email-events-though-configuration-set-with-sns-sqs-lambda-and-cloudwatch-log-groups-3a8c</link>
      <guid>https://dev.to/timetxt/monitoring-ses-email-events-though-configuration-set-with-sns-sqs-lambda-and-cloudwatch-log-groups-3a8c</guid>
      <description>&lt;p&gt;When you are sending emails through Amazon SES service, you need to track sending events like Bounce and Complaint. You can use those events to adjust your mail list and eventually maintain reputation of your SES account, so you don't have to face unwanted status of your SES account like sending pause and review situation. &lt;/p&gt;

&lt;p&gt;Amazon SES provides sending events through email feedback and event notification. When Configuration Set feature is available, now you can publish more events to multiple destinations. For example, you can publish Subscription event to CloudWatch metric. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cPC3B52y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icafwroqvfkgb281wrns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cPC3B52y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icafwroqvfkgb281wrns.png" alt="SES Sending Notification" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will show you how to set up a serverless workflow to monitor sending events of your SES account by using the following AWS services.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon SES &lt;/li&gt;
&lt;li&gt;Amazon Simple Notification Service (SNS)&lt;/li&gt;
&lt;li&gt;Amazon Simple Queue Service (SQS)&lt;/li&gt;
&lt;li&gt;AWS Lambda&lt;/li&gt;
&lt;li&gt;Amazon CloudWatch log groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--w1JOAAs9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eoh317md7w9iu5pcs8fx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--w1JOAAs9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eoh317md7w9iu5pcs8fx.png" alt="SES Sending events through SNS and serverless" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;1#. Follow SES document "&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-email.html"&gt;Set up email sending with Amazon SES&lt;/a&gt; " and setup your SES account. If your SES account is in sandbox, you will need to verify both sender and recipient email addresses.&lt;/p&gt;

&lt;p&gt;2#. Follow SES document "&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/creating-configuration-sets.html"&gt;Creating configuration sets in SES&lt;/a&gt;" and create a Configuration Set.&lt;/p&gt;

&lt;p&gt;3#. Follow SQS document "&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/step-create-queue.html"&gt;Create a queue (console)&lt;/a&gt;" and create SQS queue with standard queue type.&lt;/p&gt;

&lt;p&gt;4#. Follow SNS document "&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-getting-started.html#step-create-topic"&gt;Getting started with Amazon SNS&lt;/a&gt;" and create SNS topic.&lt;/p&gt;

&lt;p&gt;5#. Create a SNS subscription with Protocol type "SQS", put SQS ARN of SQS queue created in step 3.&lt;/p&gt;

&lt;p&gt;6#. Update Access Policy of SQS queue to allow SNS topic to publish events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Id": "__default_policy_ID",
  "Statement": [
    {
      "Sid": "__owner_statement",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::&amp;lt;AWS_Account_ID&amp;gt;:root"
      },
      "Action": "SQS:*",
      "Resource": "arn:aws:sqs:us-west-2:&amp;lt;AWS_Account_ID&amp;gt;:&amp;lt;SQS_Queue_Name&amp;gt;"
    },
    {
      "Sid": "__sender_statement",
      "Effect": "Allow",
      "Principal": {
        "Service": "sns.amazonaws.com"
      },
      "Action": "SQS:SendMessage",
      "Resource": "arn:aws:sqs:us-west-2:&amp;lt;AWS_Account_ID&amp;gt;:&amp;lt;SQS_Queue_Name&amp;gt;"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7#. Create a Lambda function using Python version &amp;gt; 3.9&lt;/p&gt;

&lt;p&gt;The function will be triggered by SQS queue and store events into CloudWatch log groups based on SES message ID. The function will create CloudWatch log group if not existing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
import boto3
from datetime import datetime
from time import time
import random

def lambda_handler(event, context):
    #print(event)
    for record in event['Records']:
        print("record")
        payload = record["body"]
        print(payload)
        # get lambda running region
        region = context.invoked_function_arn.split(":")[3]
        # get lambda running version
        version = context.function_version
        # get lambda running name
        name = "ses-notification-" + region
        mail = json.loads(payload)['mail']
        #print("mail")
        #print(mail)
        messageID = mail['messageId']
        print(messageID)
        logGroupNamePrefix="/aws/lambda/" + name
        # define CloudWatch log group client in the region
        client = boto3.client('logs', region_name=region)
        # check cloudwatch log group with name "ses-notification-cw-logs" existence
        response = client.describe_log_groups(logGroupNamePrefix=logGroupNamePrefix)
        # if log group not found, create it
        if len(response["logGroups"]) == 0:
            client.create_log_group(logGroupName=logGroupNamePrefix)
            print("Log group created")
        # get current date in form as year/month/day
        currentDay = str(datetime.now().day)
        currentMonth = str(datetime.now().month)
        currentYear = str(datetime.now().year)
        date = currentYear + "/" + currentMonth + "/" + currentDay
        logStreamsName = date + "/[" + version + "]" + messageID
        # create CloudWatch log stream
        responseLogStream=client.describe_log_streams(logGroupName=logGroupNamePrefix,logStreamNamePrefix=logStreamsName)
        if len(responseLogStream["logStreams"]) == 0:
            client.create_log_stream(logGroupName=logGroupNamePrefix, logStreamName=logStreamsName)
            print("Log stream created")
        # put payload into CloudWatch log stream with current timestamp
        #payload = "this is test payload"
        client.put_log_events(logGroupName=logGroupNamePrefix, logStreamName=logStreamsName, logEvents=[{"timestamp": int(time() * 1000), "message": payload}])
        print("Payload put into log stream")


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8#. The IAM role of the Lambda function must have the following permission in IAM managed policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatchLogsFullAccess&lt;/li&gt;
&lt;li&gt;AWSLambdaSQSQueueExecutionRole&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;9#. In the SQS queue, you need to follow "&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html"&gt;Configuring a queue to trigger an AWS Lambda function (console)&lt;/a&gt;" and add the Lambda function as trigger.&lt;/p&gt;

&lt;p&gt;10#. Create Event Destination in Configuration Set created in step 2 by following "&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/event-publishing-add-event-destination-sns.html"&gt;Set up an Amazon SNS event destination for event publishing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;11#. Now you can send an email by using &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-email.html"&gt;SMTP, AWS CLI or API&lt;/a&gt;  through your SES account. Remember that you need to &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/using-configuration-sets-in-email.html"&gt;specify the Configuration Set in your sending action&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>sns</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Amazon S3 - Web Based Upload Object with POST request and Presigned URL in Python Boto3</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Tue, 22 Aug 2023 13:21:41 +0000</pubDate>
      <link>https://dev.to/timetxt/amazon-s3-web-based-upload-object-with-post-request-and-presigned-url-in-python-boto3-5be5</link>
      <guid>https://dev.to/timetxt/amazon-s3-web-based-upload-object-with-post-request-and-presigned-url-in-python-boto3-5be5</guid>
      <description>&lt;p&gt;In this article, I will show you how to generate S3 Presigned URL for HTTP POST request with AWS SDK for Boto3(Python). The unique part of this article is that I will show you how to apply Server Side Encryption with KMS key, Tagging objects, Updating Object Metadata and more with S3 Presigned URL for HTTP POST.&lt;/p&gt;

&lt;p&gt;When using S3, there is a scenario about &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html#sigv4-UsingHTTPPOST-how-to"&gt;"Broswer-Based Uploads Using HTTP POST"&lt;/a&gt;. However, it is required to calculate AWS SigV4 Signature to follow the section.&lt;/p&gt;

&lt;p&gt;Instead of calculating the signature by you own codes, you can actually use AWS Boto3 SDK with method &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_post.html"&gt;"generate_presigned_post"&lt;/a&gt; to generate S3 PreSigned URL. This is not only saving your time to debug "Signature Mismatch" error with your own codes, you don't have to figure out requirements of crypto modules used by your codes to generate right signature. It will all be handled by AWS SDK.&lt;/p&gt;

&lt;p&gt;For example, you owns an S3 bucket in your account. One customer of yours is running a business to allow users of the customer to upload images. The images will be directly uploaded from the customer's website into your S3 bucket. The customer is not familiar with Amazon S3 service and does not own an AWS account, so you need to provide your customer an easy method uploading objects from the customer website directly into your S3 bucket. At the meantime, you don't need to make your bucket public for uploading objects.&lt;/p&gt;

&lt;p&gt;This is where S3 Presigned URL is needed. You can generate the S3 Presigned URL for HTTP POST from AWS Lambda function by having these &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/security-overview-aws-lambda/benefits-of-lambda.html"&gt;benefits&lt;/a&gt;. Then you can provide the S3 Presigned URL with your customer to integrate into the customer's website.&lt;/p&gt;

&lt;p&gt;But you might ask this question:&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are you not using S3 Presigned URL for PutObject API call?
&lt;/h2&gt;

&lt;p&gt;S3 Presigned URL for HTTP POST from broswer-based uploads provides a unique feature. You can define "starts-with" condition in the policy. You and your customers can both have some controls on requirements of the uploaded objects. &lt;/p&gt;

&lt;p&gt;For example, you only want your customer to upload text files, so you can use the following "start-with" condition to restrict value of "Content-Type" starting with "plain" in uploading request. The uploading request is created from your customer's website. The value of "Content-Type" request header is set when a file is being uploaded from your customer's website by using your S3 Presigned URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;["starts-with", "$Content-Type", "plain"],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In document of AWS SDK for Boto3, it did not share much information regarding how to use "Fields" and "Conditions" parameters mentioned at &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_post.html"&gt;"generate_presigned_post"&lt;/a&gt;. It took me some time to figure it out, so I added my understanding in the code example.&lt;/p&gt;

&lt;p&gt;I hope they will save your time in your code development.&lt;/p&gt;

&lt;p&gt;Here is the Python Code Example. Before you test it, you will need to update the constants to match your resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import boto3
import requests
from botocore.config import Config

ACCESS_KEY="AKIAIOSFODNN7EXAMPLE"
SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

BUCKET_NAME="example-bucket-name"
OBJECT_NAME="example-key-name"
REGION_LOCATION="ap-southeast-2"

KMS_KEY_ARN="arn:aws:kms:&amp;lt;region&amp;gt;:&amp;lt;account-id&amp;gt;:key/&amp;lt;key-id&amp;gt;"

EXPIRATION_TIME = 60*60 * 12 # 12 hours

TEST_FILE_NAME="/Absolute/Path/To/Local/FileName"

my_config = Config(
    region_name = REGION_LOCATION,
    signature_version = 'v4',
    retries = {
        'max_attempts': 10,
        'mode': 'standard'
    }
)

# define S3 client in ap-southeast-2 region
s3=boto3.client('s3',
                 aws_access_key_id=ACCESS_KEY,
                 aws_secret_access_key=SECRET_ACCESS_KEY,
                 config=my_config)


fields={
    "tagging": "&amp;lt;Tagging&amp;gt;&amp;lt;TagSet&amp;gt;&amp;lt;Tag&amp;gt;&amp;lt;Key&amp;gt;type&amp;lt;/Key&amp;gt;&amp;lt;Value&amp;gt;test&amp;lt;/Value&amp;gt;&amp;lt;/Tag&amp;gt;&amp;lt;/TagSet&amp;gt;&amp;lt;/Tagging&amp;gt;",
    "x-amz-storage-class": "STANDARD_IA",
    "Cache-Control": "max-age=86400",
    "success_action_status": "200",
    "x-amz-server-side-encryption": "aws:kms",
    "x-amz-server-side-encryption-aws-kms-key-id": KMS_KEY_ARN,
    "x-amz-server-side-encryption-bucket-key-enabled": "True"
    # "acl": "public-read"
    }


conditions=[
    {
        "x-amz-storage-class": "STANDARD_IA"
    },
    ["starts-with", "$Content-Type", "plain"],
    {
        "tagging": "&amp;lt;Tagging&amp;gt;&amp;lt;TagSet&amp;gt;&amp;lt;Tag&amp;gt;&amp;lt;Key&amp;gt;type&amp;lt;/Key&amp;gt;&amp;lt;Value&amp;gt;test&amp;lt;/Value&amp;gt;&amp;lt;/Tag&amp;gt;&amp;lt;/TagSet&amp;gt;&amp;lt;/Tagging&amp;gt;"
    },
    {
        "Cache-Control": "max-age=86400"
    },
    {
        "success_action_status": "200"
    },
    {
        "x-amz-server-side-encryption": "aws:kms"
    },
    {
        "x-amz-server-side-encryption-aws-kms-key-id": KMS_KEY_ARN
    },
    # {
    #     "acl": "public-read"
    # },
    {
        "x-amz-server-side-encryption-bucket-key-enabled": "True"
    }
]

# generate S3 Presigned URL for HTTP POST Request
response_presigned_url_post=s3.generate_presigned_post(
    BUCKET_NAME,
    OBJECT_NAME,
    Fields=fields,
    Conditions=conditions,
    ExpiresIn=EXPIRATION_TIME
)
print(response_presigned_url_post)

# User requests.post to test the URL
post_fields=response_presigned_url_post['fields']

# files={'file': open(TEST_FILE_NAME, 'rb')}
# you will see 403 error
#comment the following line and uncomment the second following line, you will see 200 successful

post_fields["Content-Type"]="application/octet-stream"
#post_fields["Content-Type"]="plain/text"

# file key must be the last key in the "files" parameter(form)
# it is defined at https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html#RESTObjectPOST-requests-form-fields

post_fields["file"]=open(TEST_FILE_NAME, 'rb')
print(post_fields)

# making POST Request
response_post_request=requests.post(response_presigned_url_post['url'], files=post_fields)

# print response, by default status code is 204,
# "success_action_status": "200" change it to 200

print(f'Response Status of POST request with S3 Presigned URL: {response_post_request.status_code}')
print(f'Response Headers of POST request with S3 Presigned URL: {response_post_request.headers}')
print(f'Response Body of POST request with S3 Presigned URL: {response_post_request.text}')

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>aws</category>
      <category>boto3</category>
      <category>s3</category>
      <category>postobject</category>
    </item>
    <item>
      <title>CloudFront Edge Computing - Dynamic At Edge with CloudFront Functions</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Mon, 14 Aug 2023 06:53:53 +0000</pubDate>
      <link>https://dev.to/timetxt/cloudfront-edge-computing-dynamic-at-edge-with-cloudfront-functions-374k</link>
      <guid>https://dev.to/timetxt/cloudfront-edge-computing-dynamic-at-edge-with-cloudfront-functions-374k</guid>
      <description>&lt;p&gt;Edge Computing feature is definitely one of the things that I like the most with CloudFront service. When &lt;a href="https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/"&gt;CloudFront Functions&lt;/a&gt; is launched, it makes the feature even better. &lt;/p&gt;

&lt;p&gt;Recently I saw someone asked a question about the following scenario in &lt;a href="https://repost.aws/questions"&gt;re:Post&lt;/a&gt;. I think it is a perfect scenario for CloudFront Functions.&lt;/p&gt;

&lt;p&gt;Files in the S3 origin: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON files are under content folder(prefix), e.g. s3://example_bucket/content/a.json&lt;/li&gt;
&lt;li&gt;Image files are under a sub-folder of content folder (prefix) e.g. s3://example_bucket/content/image/a.png&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The files are requested by viewer request like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON file: &lt;a href="https://text.example.com/abc/a.json"&gt;https://text.example.com/abc/a.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PNG file: &lt;a href="https://image.example.com/a.png"&gt;https://image.example.com/a.png&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is going to happen when there was no edge computing in CloudFront service? &lt;/p&gt;

&lt;p&gt;For the JSON file, there might be no other options but move the objects under 'content/abc/' folder. Then origin path will be 'content/' in origin setting of the distribution.&lt;/p&gt;

&lt;p&gt;For the PNG file, it will require a separate origin setting in the distribution configuration other than the JSON file because they are not going to share the same origin path. The PNG file will need origin path as 'content/image'.&lt;/p&gt;

&lt;p&gt;The JSON file and PNG files will also need different behaviors because they need to call different origin settings.&lt;/p&gt;

&lt;p&gt;It looks like lots of work. &lt;/p&gt;

&lt;p&gt;With CloudFront Functions, a few lines of codes will resolve the problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function handler(event) {
    var request = event.request;
    var uri = request.uri;
    var file_name = uri.split('/').pop();

    // Check whether the URI is missing a file name.
    if ( file_name.endsWith('.json')) {
        request.uri = "/content/" + file_name;
        return request;
    } else if (file_name.endsWith('.png')) {
        request.uri = '/content/image/' + file_name;
        return request;
    }

    return request;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the behavior, the function will be added as Viewer Request trigger in Default behavior with an S3 origin pointing to "s3://example_bucket". &lt;/p&gt;

&lt;p&gt;That is it! &lt;/p&gt;

&lt;p&gt;And if any further requirement is required, e.g. changing path based on the requesting domain name, only a few modifications in codes will meet the requirement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function handler(event) {
    var request = event.request;
    var uri = request.uri;
    var file_name = uri.split('/').pop();
    var host = request.headers.host.value;

    // Check whether the URI is missing a file name.
    if ( host === "text.example.com" ) {
        request.uri = "/content/" + file_name;
        return request;
    } else if ( host === "image.example.com" )) {
        request.uri = '/content/image/' + file_name;
        return request;
    }

    return request;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudFront Functions are perfect to process those 'small' modifications in viewer requests and responses. &lt;/p&gt;

&lt;p&gt;The code I am using is modified from this &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-add-index.html"&gt;code example&lt;/a&gt; in CloudFront public document.&lt;/p&gt;

&lt;p&gt;I am seeing a trend that generative AI tools like ChatGPT or Amazon CodeWhisperer will help to writ those scripts much easier than ever! &lt;/p&gt;

&lt;p&gt;Like Lambda@Edge, there are some &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html"&gt;restrictions&lt;/a&gt; on runtime of CloudFront Functions that you should have a read before wring your codes. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>cdn</category>
    </item>
    <item>
      <title>The Differences In Sending Email Actions Between SES Version 1 and Version 2 APIs</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Mon, 07 Aug 2023 03:05:28 +0000</pubDate>
      <link>https://dev.to/timetxt/the-differences-in-sending-email-actions-between-ses-version-1-and-version-2-apis-2o8n</link>
      <guid>https://dev.to/timetxt/the-differences-in-sending-email-actions-between-ses-version-1-and-version-2-apis-2o8n</guid>
      <description>&lt;p&gt;If you have been using Amazon SES service for a while, it might not be new to you that Amazon SES is having two versions of API actions available at the moment, &lt;a href="https://docs.aws.amazon.com/ses/latest/APIReference/index.html"&gt;API Reference (version 1)&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/ses/latest/APIReference-V2/index.html"&gt;API v2 Reference&lt;/a&gt;. I tried to find the announcement of the API v2 in &lt;a href="https://aws.amazon.com/new/?whats-new-content-all.sort-by=item.additionalFields.postDateTime&amp;amp;whats-new-content-all.sort-order=desc&amp;amp;awsf.whats-new-categories=*all&amp;amp;whats-new-content-all.q=ses&amp;amp;whats-new-content-all.q_operator=AND&amp;amp;awsm.page-whats-new-content-all=3"&gt;What's New with AWS?&lt;/a&gt; page but I did not have the luck. &lt;/p&gt;

&lt;p&gt;Amazon SES service API calls can be separated into two types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Management API actions, which can be recorded in &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/logging-using-cloudtrail.html#service-name-info-in-cloudtrail"&gt;CloudTrail Event History&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sending API actions, which are not recorded in &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/logging-using-cloudtrail.html#service-name-info-in-cloudtrail"&gt;CloudTrail Event History&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, you will read about how I took a peek at the sending API actions in Amazon SES API v1 and v2.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What are these API actions?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I can find the API action names in SES API documents. But I find that I can also get the list of API calls by creating an example IAM policy from IAM console. &lt;/p&gt;

&lt;p&gt;Here is the list of actions I got when I was creating an IAM policy with service name 'SES' and 'SES v2' from IAM console.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;v1 IAM actions&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Action": [&lt;br&gt;
            "ses:SendBounce",&lt;br&gt;
            "ses:SendBulkTemplatedEmail",&lt;br&gt;
            "ses:SendCustomVerificationEmail",&lt;br&gt;
            "ses:SendEmail",&lt;br&gt;
            "ses:SendRawEmail",&lt;br&gt;
            "ses:SendTemplatedEmail"&lt;br&gt;
        ]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2 IAM actions&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Action": [&lt;br&gt;
            "ses:SendBulkEmail",&lt;br&gt;
            "ses:SendCustomVerificationEmail",&lt;br&gt;
            "ses:SendEmail"&lt;br&gt;
        ]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;What are the differences?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used SES SDK Boto3 Document in the following comparison.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To use Amazon SES v2 APIs, the service client must be

&lt;code&gt;boto3.client('sesv2')&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SES v2 API &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2/client/send_email.html"&gt;"SendEmail"&lt;/a&gt; provides the same functions in v1 APIs &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_email.html"&gt;"SendEmail"&lt;/a&gt;, &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_raw_email.html"&gt;"SendRawEmail"&lt;/a&gt; and &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_templated_email.html"&gt;"SendTemplatedEmail"&lt;/a&gt;. But only the API &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2/client/send_email.html"&gt;"SendEmail"&lt;/a&gt; in SES v2 supports the feature &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/sending-email-list-management.html"&gt;"list management"&lt;/a&gt;. List Management can help to reduce the chance of get complaints or even hard bounces by sending emails to recipients who have opted out of their previous subscriptions to some email services, e.g. newsletter email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SES v2 API &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2/client/send_bulk_email.html"&gt;"SendBulkEmail"&lt;/a&gt; has different names of parameters in sending requests with SES v1 &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_bulk_templated_email.html"&gt;"SendBulkTemplatedEmail"&lt;/a&gt;. But values of those parameters should be no difference. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SES v2 API action &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sesv2/client/send_custom_verification_email.html"&gt;"SendCustomVerificationEmail"&lt;/a&gt; has no difference with the &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses/client/send_custom_verification_email.html"&gt;action&lt;/a&gt; in SES v1 API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SES v2 API actions do not have a definition of action "SendBounce". The action &lt;a href="https://docs.aws.amazon.com/ses/latest/APIReference/API_SendBounce.html"&gt;"SendBounce"&lt;/a&gt; is used to generate and send a bounce message to the sender of an email which I received through Amazon SES. There is a restriction that we can only use this API action on an email up to 24 hours after receiving it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;How do I restrict usage in SES API v1 and v2? &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SES public document has given an example about &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/control-user-access.html#iam-and-ses-examples-access-specific-ses-api-version"&gt;"Allowing Access to only SES API version 2"&lt;/a&gt;. Relatively, I could modify the condition forcing to use SES API v1. &lt;/p&gt;

&lt;p&gt;Why would I want to do that? One thing in the same document could provide an answer:&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;The SES SMTP interface uses SES API version 2 of ses:SendRawEmail.&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;As an IAM user can &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html#smtp-credentials-convert"&gt;convert AWS credentials into SMTP username and password&lt;/a&gt;, I can use the trick to restrict the IAM user to use or not use SMTP client to send emails through Amazon SES.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One more difference is the &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/quotas.html#quotas-message"&gt;message size&lt;/a&gt; between using SES API v1 and v2. While size of message (after base64 encoding and including attachments) is 10 MB using SES API v1, the size is 40MB in SES API v2.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ses</category>
      <category>aws</category>
      <category>amazon</category>
    </item>
    <item>
      <title>SES BYODKIM - Streamline Private and Public Key by Python Script</title>
      <dc:creator>Jason Shen</dc:creator>
      <pubDate>Tue, 01 Aug 2023 05:26:28 +0000</pubDate>
      <link>https://dev.to/timetxt/ses-byodkim-streamline-private-and-public-key-by-python-script-dpj</link>
      <guid>https://dev.to/timetxt/ses-byodkim-streamline-private-and-public-key-by-python-script-dpj</guid>
      <description>&lt;p&gt;In Amazon SES document, it states the following: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You have to delete the first and last lines (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----, respectively) of the generated private(public) key. Additionally, you have to remove the line breaks in the generated private key. The resulting value is a string of characters with no spaces or line breaks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-email-authentication-dkim-bring-your-own.html#send-email-authentication-dkim-bring-your-own-configure-identity"&gt;source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the private or public is not streamlined, you won't be able to use them with SES BYODKIM.&lt;/p&gt;

&lt;p&gt;At the meantime, you also need to generate a random string as selector in TXT DNS record publishing the public key&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;selector._domainkey.example.com&lt;/td&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;p=yourPublicKey&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Replace selector with a unique name that identifies the key.&lt;br&gt;
Here is the Python Script to complete &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-email-authentication-dkim-bring-your-own.html#send-email-authentication-dkim-bring-your-own-update-dns"&gt;source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is python script can do the both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import sys, string, random

def streamline_key(keyLocation):
   keyLines=open(keyLocation).readlines()
   keyStream=[]
   for line in keyLines[1:-1]:
      keyStream.append(line.replace('\n', ''))
   key=''.join(keyStream)
   return key

print(sys.argv)

key1Location=sys.argv[-3]
print(key1Location)

key2Location=sys.argv[-2]
print(key2Location)

domain=sys.argv[-1]
print(domain)

key1Streamline=streamline_key(key1Location)
print(key1Location+" streamline:\n" + key1Streamline )
print("\n")
key2Streamline=streamline_key(key2Location)
print(key2Location+" streamline:\n" + key2Streamline)
print("\n")
selector = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(32))
print("public key TXT record name:\n" + selector+'._domainkey.'+domain)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the code with name Run the code 'remove-newline-in-key.py' in the same folder storing private key and public key. Then run the script as following format in command line. My public key name is public.key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% python remove-newline-in-key.py private.key public.key example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will get the following result&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;['remove-newline-in-key.py', 'private.key', 'public.key', 'example.com']&lt;br&gt;
private.key&lt;br&gt;
public.key&lt;br&gt;
example.com&lt;br&gt;
private.key streamline:&lt;br&gt;
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMdPWfzVMYahtkvTvBfXsVr52O2CHUok8JzeoCt1Ou3t1SmDmV3755+ztxGj7nFwUCVFmrT5ZmaaDZ5u7Jd856KmejtlIeuPHBt9wuoaiwI1IohXWZAMGLi+qo+FX1kHk+nKj5nMLNq9dSOE8xXXfmtPcz+B4LACpuQRXNGhqCLlAgMBAAECgYEAkXTq8qdQtrXMSfij3C6xI/kVhPihkZv18jZTZIPw1vXszJhbVIjkWNwarggam7Vg+GKc7pjZT+X8LHU9u60Pio22vi6ZNBQwqe0DlpMx1MtJIht4EwH63CZDSU6jijZUjvdTyKqtoqMHiqUaLz2Iom8LYikmrKImMr6S9PqgBsECQQDsJ+8N4asakc0uUKZkxgQNpoM7fykuFmF9TJcq3K9JHfx8HpvMN9UWNyGDfQqIo/4oFD3LxeheeyltETCNqE91AkEA2A7OE2r+D9uMpAnNyt3SmIRQZzVn+ZHB+0fICFB8L17rt6TnuH2AU6ceuoVzr8vtSWGZ+/sotUGvaIbZvwkHsQJAfoTafv5i8+YfHewZaS3pKAMIlcyHnGhjLITnDBCVXD/TcA/Z+iwDXlaE/vPzu8bYOFK31L8fwdaMGCG4eHwurQJAO2CeO/Hsjrkcxrw3BWi/BtFeM28W+xyWvhM1IyvTZUVl7JtyX16GVPcZ19LzPz4BIWikY/7baiz6IvTkhL7bkQJAZhwo33EVKRcDoavSOWshcWEsp6SNychsdT9R17uEzsZq1RgrB9XNqskTveJvzLeT/aRtuoqB+mpJ1R3ux3VLYw==&lt;/p&gt;

&lt;p&gt;public.key streamline:&lt;br&gt;
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHT1n81TGGobZL07wX17Fa+djtgh1KJPCc3qArdTrt7dUpg5ld++efs7cRo+5xcFAlRZq0+WZmmg2ebuyXfOeipno7ZSHrjxwbfcLqGosCNSKIV1mQDBi4vqqPhV9ZB5Ppyo+ZzCzavXUjhPMV135rT3M/geCwAqbkEVzRoagi5QIDAQAB&lt;/p&gt;

&lt;p&gt;public key TXT record name:&lt;br&gt;
w2hajm6q1zoe0gw1q993shhmqopj5auy._domainkey.example.com&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
  </channel>
</rss>
