{"slug": "the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed", "title": "The agent host didn't have the lifecycle boundaries I assumed", "summary": "A developer built Savepoints, an agent-memory system that adds explicit lifecycle boundaries around semantic review so that an agent's own review turn cannot recursively trigger another review. Live probes in the Cursor host showed that prompt-origin metadata from beforeSubmitPrompt was not reliable enough to distinguish review scaffolding from source work, so the system now marks review follow-ups explicitly with a SAVEPOINTS_CAPTURE_REVIEW_V1 opportunity_id token. Across four substantial agent sessions, observe hooks confirmed real activity but the emit-savepoint skill was consulted inconsistently and emit-learning never ran, making it impossible to tell \"reviewed the work and found nothing\" from \"the review never happened.", "body_md": "As I started handing agents longer pieces of work, I stopped being present at every lifecycle boundary.\n\nIn shorter copilot-style sessions I had been part of the control loop without really thinking about it. Longer agent runs broke that assumption: I was no longer present to know when work finished, what mattered, or what context should survive into the next task.\n\nOne place this surfaced was memory. I wanted an agent to review completed work and decide whether anything was worth carrying into future sessions. I call the system I built for that Savepoints.\n\nSavepoints already had the semantic path:\n\n```\nAgent work\n→ Observe\n→ emit-savepoint (worth keeping?)\n   → emit-learning / no_capture\n```\n\nObserve what happened, decide whether anything was worth keeping, then emit a learning or explicitly capture nothing.\n\nThe weak point was how that review started. It depended on the agent following instructions, but I had no reliable way to tell whether it had.\n\nAcross four substantial agent sessions, observe hooks confirmed real activity, but the `emit-savepoint` skill was consulted inconsistently and `emit-learning` never ran. I could not distinguish \"reviewed the work and found nothing\" from \"the review never happened.\"\n\nI added two boundaries around the semantic review:\n\n```\nAgent work\n→ Observe\n→ Review opportunity       ← new boundary\n→ emit-savepoint (worth keeping?)\n   → emit-learning / no_capture\n→ mark reviewed            ← new boundary\n```\n\nOpen a review opportunity when observed work needed review, then mark that evidence as reviewed once the opportunity had been handled.\n\n*Observation and semantic judgment stay separate.*\n\nI already had a suspected shape for the larger system, and checking half a dozen agent-memory systems reinforced it: opportunity IDs, idempotent review closure, fail-open hooks, semantic judgment left to the agent.\n\nWhat remained uncertain was whether those invariants had reliable lifecycle boundaries to attach to in the host.\n\nFor this implementation, the host was Cursor, which exposes lifecycle hooks such as `beforeSubmitPrompt`, `afterFileEdit`, and `stop`.\n\nThe host seemed to expose enough information to establish three boundaries: whether a turn was review scaffolding or source work, whether lifecycle events belonged to the same piece of work, and whether that work had actually affected this repository.\n\nIf those answers were reliable, the adapter was straightforward: register the work, watch its evidence, then open review when it completed.\n\nI expected the next pass to be production wiring.\n\nLive probes said otherwise.\n\nThe straightforward implementation used Cursor's `stop` hook as the completion signal. When ordinary work hit `stop`, Savepoints would open a review opportunity and send a follow-up asking the agent to review what had just happened.\n\nThat immediately created a recursion problem. **The review was itself another agent turn.** If that turn also ended in `stop`, Savepoints could mistake its own review for more completed work and open another review.\n\n```\nordinary work\n→ stop\n→ open review\n→ review follow-up\n→ stop? (could trigger another review)\n```\n\nThe natural first question was whether the host could tell me that this new turn was the review follow-up I had created.\n\nI tried making that distinction when `beforeSubmitPrompt` fired. If the host could tell me whether a prompt came from the user or from the follow-up I had generated, I could classify the turn before any work happened.\n\nLive probes showed that distinction was not reliable enough to build on.\n\nSo I stopped asking the host to infer an identity I controlled. When Savepoints creates a review follow-up, it now marks it explicitly:\n\n```\nSAVEPOINTS_CAPTURE_REVIEW_V1 opportunity_id=<uuid>\n```\n\nA prompt with that marker is review scaffolding. It does not register as new source work, so the review cannot recursively open another review.\n\nInstead of inferring scaffolding identity from host metadata, I made it part of the protocol I owned.\n\n`stop` did not establish which work had finished\nExplicitly marking the review turn solved one problem: Savepoints no longer had to infer whether a prompt was its own scaffolding.\n\nBut an earlier attempt to contain that recursion had exposed a different assumption about `stop`. Before I added the marker, I had tried a simpler guard: after opening review, suppress the next `stop`.\n\nThat assumed the next `stop` belonged to the review I had just opened. It didn't.\n\n```\nopen review\n→ expect review stop\n→ no stop arrives\n→ guard remains armed\n\n~100 seconds later\n→ unrelated work stops\n→ stale guard eats it\n```\n\nI tried several variations on the same idea. None gave me a reliable way to distinguish \"the review I just started has finished\" from \"some unrelated work has finished.\"\n\nThe stale guard had assumed an ordering relationship the host did not guarantee. A `stop` told me that something had ended. It did not prove that the thing ending was the review I had just opened.\n\n`generation_id` gave me a stronger relationship: events carrying the same ID could be connected to the same agent generation. I no longer had to assume that the next `stop` belonged to the work I was tracking.\n\nBut correlation only told me which events belonged together. It did not tell me whether that work had affected this repository.\n\nMulti-root workspaces exposed why that distinction mattered.\n\nA single agent generation can touch several repositories in one session. Session-wide hooks still run in each Savepoints-enabled repository, even when the edit happened somewhere else.\n\nThat meant `generation_id` could tell me that events belonged to the same agent generation, but not which repository that work had affected. A generation did not need a single repository owner. It could span several repositories.\n\n```\none agent generation\n│\n├─ edits repo A/\n│  └─ afterFileEdit under repo A's root → evidence for repo A\n│\n└─ edits repo B/\n   └─ afterFileEdit under repo B's root → evidence for repo B\n\nEach repo evaluates its own evidence independently.\n```\n\nSeeing the hook fire was therefore not evidence that this repository had been affected. Each repository asked a narrower question: **did this generation produce evidence here?**\n\nAn `afterFileEdit` event counted only when its `file_path` resolved under that repository's `repoRoot`. If the session touched another repository but none of those edits belong here, this repository does not open review.\n\nShell and MCP activity does not count toward this gate because I cannot reliably tie it to a repository. Savepoints can still learn from what happens around a tool call; it just does not try to observe what happens inside a tool boundary.\n\nBy this point, each failed assumption had removed an inference from the adapter. What remained were three boundaries that needed separate evidence:\n\n| Boundary I needed | Reliable signal | What I could not infer | \n|---|---|---|\n| Is this Savepoints' own review turn? | `SAVEPOINTS_CAPTURE_REVIEW_V1` | Scaffolding identity from host provenance | \n| Do these lifecycle events belong to the same agent generation? | `generation_id` | Which work had finished from event order alone | \n| Did this work affect this repository? | `afterFileEdit` under its`repoRoot` | Repository evidence from hook scope | \n\nOn Desktop, I could now follow one piece of work all the way through: it started, this repository produced evidence, the same work stopped, and Savepoints opened review.\n\nThen I ran the same design in Cloud.\n\nEvery piece seemed to be there. I could see work start, observe repository-local file edits, and see a final `stop`.\n\nThe individual lifecycle events existed. The boundary I needed between them did not.\n\n```\nthis work started\n→ this repository produced evidence\n→ this same work stopped\n→ open review\n```\n\nOn Desktop, `generation_id` connected that chain. In Cloud, the ID I saw when work started and while evidence was collected did not reliably match the one I saw at `stop`.\n\nI could have tried to guess which events belonged together using `conversation_id`, timing, or rules for normalizing the IDs.\n\nI didn't. Probe 2 had already shown the problem with guessing at correlation: seeing events in the expected sequence was not proof that they belonged to the same work.\n\nIf Savepoints could not reliably establish that the work it observed was the work that just stopped, it could not safely open review for it. So in Cloud, the adapter treats capture review that depends on lifecycle hooks alone as `unsupported`.\n\nThat does not mean Savepoints cannot run in Cloud, or that an agent cannot save a learning. It means this adapter cannot establish the evidence needed to guarantee this particular review path.\n\nI was also reluctant to build my own correlation scheme on top of a lifecycle surface that was still evolving. [A later public Cursor report](https://forum.cursor.com/t/generation-id-has-extra-suffix-for-afteragentthought/166275) documented different `generation_id` shapes across hook types. I would rather wait for a stable primitive I can verify than own an approximation that the host may eventually make unnecessary.\n\n**Takeaway:** An agent host can expose lifecycle events without exposing the lifecycle boundaries your system needs. I had to establish scaffolding identity, lifecycle correlation, and repository-local evidence separately instead of inferring them from host events.\n\nI could establish review identity myself, but correlation and repository evidence still needed independent proof. When the host could not provide that proof in Cloud, `unsupported` was more accurate than another heuristic.", "url": "https://wpnews.pro/news/the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed", "canonical_source": "https://dev.to/michaeltruong/the-agent-host-didnt-have-the-lifecycle-boundaries-i-assumed-5akh", "published_at": "2026-09-25 02:00:15+00:00", "updated_at": "2026-09-25 02:29:03.036417+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": ["Savepoints", "Cursor"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed", "markdown": "https://wpnews.pro/news/the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed.md", "text": "https://wpnews.pro/news/the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed.txt", "jsonld": "https://wpnews.pro/news/the-agent-host-didn-t-have-the-lifecycle-boundaries-i-assumed.jsonld"}}