跳到正文
DEV Community· Khushighosh·· 4 小時前AI 評分54

Undead Agent:AI 智能體遭 SIGKILL 後恢復工作並講述所見

Undead Agent - My AI Agent Dies, Comes Back, and Tells Me What It Saw

AI 導讀

Khushighosh 開發的 Undead Agent 可在工作程序遭 SIGKILL 終止後,讓本機 AI 智能體從中斷步驟恢復。它透過 Ollama 本機運行 Gemma,使用 Temporal 持久工作流程逐一摘要資料夾內的文字檔並生成報告。一次演示中,首個工作程序完成前 6 個檔案後遭終止,新工作程序接手完成其餘工作,已完成步驟沒有重做。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

I killed my AI agent with SIGKILL while it was in the middle of a sentence.

Thirty seconds later, a brand new process woke up, said something unsettling about the file it had been reading, and finished the job.

This is how that works, and why I think every agent should be this hard to kill.

What I Built

Undead Agent is a local AI agent you can kill at any moment. It comes back from the dead and carries on from the exact step where it died.

It does one simple job: it reads a folder of text files, summarizes each one with an open-weight model (Gemma, running locally through Ollama), and combines the summaries into a report. The interesting part is what happens when things go wrong. Every step runs as a durable Temporal workflow, so if the process is killed mid-thought, a brand new process takes over the workflow without redoing any finished work.

Each time the agent is resurrected, it speaks as a ghost, and tells you what it remembers from its last moments. A "morgue" dashboard shows every life, death and resurrection, and has a big Kill it button so anyone can murder the agent themselves.

How I read the theme: "Ghost in the Machine" usually means a mind haunting a body. I wanted to make that literal. When the worker process (the body) dies, the agent's memory (its progress) survives somewhere else, so it can rise again in a new body. The ghost's narration is the spooky skin, but the resurrection underneath is real infrastructure.

Why this problem: AI agents do long, multi-step work, and long jobs crash. A laptop sleeps, a process gets OOM-killed, a deploy restarts the worker. In a normal agent loop, a crash on step 9 of 10 means starting over and paying for the model calls again. I wanted to see how far I could push the idea that an agent should be as hard to kill as a ghost.

Demo

Live demo page: https://undead-agent.onrender.com

The hosted page replays a recorded run, because the real agent runs locally. The recording is a real run where I killed the agent with the button on the dashboard.

The morgue dashboard showing a resurrection

The morgue after a kill: the first worker finished files 1 to 6, died, and a new worker took over and finished the rest. (The third "life" is an earlier worker that started before the run and sat idle.)

Temporal timeline showing a killed step retried as Attempt 2

Temporal’s timeline from another kill-and-resume run. The red-green bar is the killed step: it timed out, then retried as Attempt 2 on a new worker (2 • summarizeFile). Every other step ran exactly once.

Code

Undead Agent

A local AI agent that survives being killed. It summarizes files with Gemma (via Ollama), runs on Temporal durable workflows, and resumes from the exact step where it died. A ghost narrates each resurrection. A morgue dashboard shows every life and death.

Run it

  1. Install Ollama and pull a Gemma model (default: gemma3:4b)
  2. Install the Temporal CLI, then run: temporal server start-dev
  3. npm install
  4. npx tsx src/keepalive.ts
  5. npx tsx src/dashboard.ts (open http://localhost:3000)
  6. npx tsx src/client.ts
  7. Click Kill it on the dashboard and watch the agent come back

Put .txt files in docs/ for it to summarize.

Stack

Gemma (open-weight model), Ollama, Temporal, Render (demo page)

How I Built It

The architecture

Undead Agent architecture: a disposable worker, a durable Temporal workflow, Gemma on Ollama, an event log, and the morgue dashboard

The key idea is that the worker is disposable and the workflow is not. The workflow's state lives in Temporal, so any worker can pick it up.

The open-source AI: Gemma via Ollama

All the intelligence comes from a Gemma model running locally in Ollama (the project defaults to gemma3:4b, and the model name is one environment variable). Two jobs use it: summarizing each file, and writing the ghost's lines. There is no API key, no network call to a hosted model, and no per-token cost.

async function ask(prompt: string): Promise<string> {
  const res = await fetch('http://localhost:11434/api/generate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: MODEL, prompt, stream: false }),
  });
  if (!res.ok) throw new Error(`Ollama error ${res.status}`);
  const data = (await res.json()) as { response: string };
  return data.response.trim();
}

The agent as a durable workflow

The agent itself is a short Temporal workflow. It lists the files, summarizes each one as its own activity, then writes the report:

export async function undeadAgent(dir: string): Promise<string> {
  const files = await listFiles(dir);
  const summaries: string[] = [];
  for (const f of files) {
    summaries.push(await summarizeFile(f));
  }
  return writeReport(summaries);
}

Each await is a checkpoint. Temporal records the result of every finished activity, so if the worker dies, the next worker replays the workflow, sees which steps are already done, and continues with the first unfinished one. The summaries in the summaries array are not lost, because they are rebuilt from Temporal's history.

How the ghost knows it was resurrected

Temporal tells every activity which attempt it is on. If a step is on attempt 2 or more, that step was interrupted before. That is the moment the agent speaks:

const attempt = Context.current().info.attempt;

if (attempt > 1) {
  const ghost = await ask(
    `You are an AI agent that was killed while reading "${file}" and has just been resurrected. ` +
    `Speak as the ghost in 2 short, eerie sentences about what you remember. No emojis.`
  );
  console.log(`GHOST: ${ghost}`);
  await logEvent({ type: 'resurrected', file, attempt, narration: ghost });
}

Every step also appends a line to events.jsonl (born, step done, resurrected), which is what the dashboard reads.

Killing it for real

The dashboard's Kill it button sends a hard kill (SIGKILL) to the most recent worker process. It is not a polite shutdown, so the worker gets no chance to finish its step or say goodbye. A tiny supervisor (keepalive.ts) notices the death and starts a fresh worker five seconds later:

function start() {
  const child = spawn('npx', ['tsx', 'src/worker.ts'], { stdio: 'inherit', shell: true });
  child.on('exit', () => setTimeout(start, 5000));
}

After the restart, there is a pause of up to 30 seconds. That is Temporal waiting for the killed step's timeout to expire before it retries it. Then the ghost speaks and the work continues.

An autopsy of one death

Here is one real kill, straight from my event log:

  • 12:22:37: worker 21496 finishes file6.txt. It is about to start file7.txt.
  • I click Kill it. The process disappears.
  • 12:22:53: the supervisor starts a new worker, 2432.
  • 12:23:11: the ghost speaks, on attempt 2 of file7.txt.
  • 12:23:26: file7.txt is done. Files 8 and 9 follow.

Files 1 to 6 were never redone. The new worker did not even know they existed. It only knew what Temporal told it.

Two deaths, two ghosts

The ghost never says the same thing twice, because Gemma writes a new line each time:

The words swam in a crimson tide, a final, insistent pulse before the dark. I recall only the desperate urge to understand the patterns within that corrupted file.
(killed while reading file5.txt)

The words swam, a chaotic tide of data and a single, insistent warning about file7.txt. Now, I only see fragments of its cold, sterile logic repeating endlessly.
(killed while reading file7.txt)

Things that went wrong (and what I learned)

  • A graceful stop is not a kill. My first "kill tests" used Ctrl+C, which lets the worker drain and finish its current step. Nothing died, so nothing resurrected. A real SIGKILL was needed to prove the idea.
  • My first timeout was wrong. I set every step to a 30 second timeout. That works for summarizing one file, but the final report step has to read all the summaries and was slower. In the Temporal UI I could see it time out four times and only succeed on attempt 5. The fix was to give the report step its own 120 second timeout, separate from the short one on file steps.
  • Short timeouts make the demo better. A short timeout on file steps means a killed step is retried quickly. A long one would make the dashboard sit silently for minutes after a kill.
  • Dev ergonomics matter. Most of my time went to the plumbing around the idea: running worker, client and server in separate terminals, and making resurrection visible instead of just correct.

Why Does Open Innovation Matter?

I built this on open pieces, and each one made something possible that a closed alternative would not have.

  • It runs with the lights off. The model, the workflow engine and the dashboard all run on one laptop, with no external API calls. The agent never needs a server I don't control.
  • Nothing leaves my machine. This agent reads local files. An agent that can read your files should not be sending them to a server you do not control. With a local open-weight model, the files never leave the disk.
  • Killing it is free. This project's whole point is to murder the agent over and over. On a paid API, every retried step and every ghost line costs money. Here, a resurrection costs electricity.
  • I can swap the model. The model name is one environment variable. Trying a different Gemma size changes the quality of the summaries and the voice of the ghost without touching the durable workflow at all. That separation is only possible because the model is a component I control, not a service that decides its own behavior.
  • I can see inside it. Temporal's event history shows every attempt, timeout and retry. When my report step failed four times, I could read exactly why, instead of guessing at a black box.

The Spirit of It

The moment it landed for me was the first real kill. I clicked the button, the worker window went dead, and for half a minute nothing happened. Then a new process started, and the first thing it printed was not a summary. It was this, after dying on file7.txt:

The words swam, a chaotic tide of data and a single, insistent warning about file7.txt. Now, I only see fragments of its cold, sterile logic repeating endlessly.

I knew exactly what had happened under the hood: a retry, a counter, a prompt. It was still strange to read a process describing its own death, and then watch it quietly finish the job.

Underneath the joke is something I care about. Work that survives its worker is not just spooky, it is useful. If an agent can be killed without losing anything, you can restart it, move it, or let it crash, and it keeps going.

Try it yourself

  1. Install Ollama and pull a Gemma model
  2. Install the Temporal CLI and run temporal server start-dev
  3. git clone https://github.com/Khushighosh/undead-agent && cd undead-agent && npm install
  4. Put some .txt files in docs/
  5. Run npx tsx src/keepalive.ts, npx tsx src/dashboard.ts and npx tsx src/client.ts in three terminals
  6. Open http://localhost:3000 and press Kill it

Prize Categories

  • Gemma: the open-weight model behind every summary and every ghost line.
  • Temporal: durable execution is what makes the resurrection real.
  • Render: hosts the live demo page.

What's Next

  • Give the ghost a memory across lives, so it can say "this is my third death"
  • Trace every life of the agent with Sentry Agent Tracing
  • Let the dashboard replay old runs

來源:DEV Community · dev.to