State Management
State management is the layer that survives every interruption: laptop close, model upgrade, developer turnover, parallel sessions, machine swaps, crashes. The design exists because "trust whichever copy I happen to see first" is exactly how state corruption happens.
This page captures the design. The full strategic case is in ai-engineering-strategy.md ยง14.
Built: the state-sync skill (write side โ AI Context block to the GitHub Issue + condensed mirror to the ClickUp ai_context field + bidirectional link), the AI Context block convention, and the reconciliation algorithm (read side, in /continue steps 2โ3). Summaries over the mirrored state are produced by /daily-summary.
Still deferred: the per-machine local cache (.claude/session/active-context.json) and the working-state lease (ai-engineering-strategy.md ยง15). Until the cache exists, reconciliation reads the GitHub Issue body directly; until the lease exists, the "ClickUp assignee = task owner" convention (autonomy-model.md) plus the worktree pattern cover concurrent-session safety.
Re-read the deferred sections against reality before relying on them.
Three storage layers, ordered by authorityโ
The authoritative state for an in-progress task lives in the GitHub Issue body. The two other layers (ClickUp mirror, local cache) serve specific roles and never win a tie.
| Layer | Where | Authority | Why this layer exists |
|---|---|---|---|
| Issue body "AI Context" block | bewith-dev/<consumer-repo> GitHub Issue | Authoritative | One place, version-controlled by GitHub, accessible from any device, readable by every actor (humans + AI sessions). |
ClickUp custom field ai_context | The ClickUp task | Management mirror | Lets the PM/management dashboards see in-progress state without granting GitHub Issue read access. Written by the AI whenever the Issue body is updated; never the source of truth. |
| Local cache | .claude/session/active-context.json per machine | Convenience | Sub-second resume in the same session. Reconciled against the Issue body on every session start. |
The split exists because the audiences differ. A management dashboard cares about "what's in progress, who's on it, what's the gist" โ that's the ClickUp mirror's job. An AI session resuming after a 10-minute coffee break cares about "what was I doing exactly" โ that's the local cache's job. The authoritative state needs a single home that survives both audiences' needs โ that's the GitHub Issue body.
Setup โ the ClickUp
ai_contextfield (one-time, per workspace). The mirror layer needs a custom field to write to. An admin creates one field, type Text, named exactlyai_context(lowercase, underscore), at the R&D space scope (covers every sprint/folder list where dev tasks live) โ ClickUp UI: space โ Settings โ Custom Fields โ + Text โai_context. The exact name matters:state-syncresolves the field by name viaclickup_get_custom_fields, never a hardcoded id, so it stays portable. There is no MCP/API tool that creates the field, so this step is manual. If the field is absent,state-syncskips the mirror gracefully (one-line note) and the GitHub Issue body remains authoritative โ nothing breaks; PMs just lose the ClickUp-side view until the field exists.
The AI Context blockโ
A structured markdown block at the top of the GitHub Issue body. Structured enough for the AI to parse it; readable enough that a human (manager, coworker, the author six weeks later) can absorb the situation in 30 seconds.
## AI Context (updated by Claude โ do not edit manually)
- **Active actor:** cc-ariel-laptop (Claude Code on Ariel's laptop)
- **Phase:** implementing (chunk 3 of 5)
- **Last updated:** 2026-05-26T14:32:00Z
- **Plan:**
1. Add UserBookingRepository (done)
2. Wire into BookingService (done)
3. Add unit tests for repository (in progress)
4. Add integration test (pending)
5. Open PR (pending)
- **Files touched:** apps/api/src/booking/{repository,service,booking.module}.ts, plus 4 test files
- **Decisions:**
- Chose Mongo over Aurora for booking history (volume + write patterns)
- Single-document booking model with embedded changes array
- **Blockers:** none
- **Next step:** Finish unit tests in booking.repository.spec.ts
The fields are not arbitrary. Each addresses a specific failure mode the AI produces when the field is absent:
- Active actor prevents the claim race (someone resumes the task without realising another machine is on it).
- Phase lets a resuming session skip directly to the right step of the autonomous loop.
- Last updated is the tiebreaker for reconciliation.
- Plan survives the AI's context-window limit; without it, the AI re-derives the plan and often gets a different one.
- Files touched lets the resuming session re-orient without re-grepping.
- Decisions are the load-bearing details that would be lost in a model swap or a coworker handoff.
- Blockers + Next step together answer "what should I do right now."
The block is owned by the AI. Humans don't edit it manually โ they correct via conversation, and the AI updates the block.
The reconciliation algorithmโ
Mobile resume โ and more generally, any session start where the AI may have stale local state โ runs through a deterministic reconciliation algorithm. The algorithm is the whole protection against multi-actor state corruption; it must be deterministic, exhaustive about its branches, and conservative when it can't decide.
Step 1: Identify the candidate taskโ
The AI determines which task is active. Three sources, in order of preference:
- The local cache
.claude/session/active-context.jsonif it exists and was modified within the last 24 hours. - The most recently updated open GitHub Issue assigned to the current actor with label
in-progress. - If neither, the AI presents the open ClickUp tasks and asks the user to pick.
Step 2: Fetch both copiesโ
If a candidate task exists, the AI fetches:
- The local cache (if present):
local_copywithlocal_updated_attimestamp. - The GitHub Issue body's "AI Context" block:
remote_copywithremote_updated_atparsed from theLast updatedfield inside the block.
Step 3: Apply the reconciliation rulesโ
if remote_copy does not exist:
# New task, no remote state yet.
state = local_copy or initialize_empty()
elif local_copy does not exist:
# Fresh machine, new session, or wiped cache.
state = remote_copy
write_local_cache(remote_copy)
elif remote_updated_at > local_updated_at:
# Remote is newer โ someone updated from another machine
# (mobile resume, coworker handoff, or different actor).
state = remote_copy
write_local_cache(remote_copy)
elif local_updated_at > remote_updated_at:
# Local is newer โ likely a crash before state-sync flushed.
if remote_actor != local_actor:
# Different actor wrote remote; we cannot blindly overwrite.
raise ReconciliationConflict(local_copy, remote_copy)
else:
# Same actor; safe to push local.
write_remote(local_copy)
state = local_copy
else:
# Timestamps equal, take either (they should be identical).
state = remote_copy
Step 4: Handle reconciliation conflictโ
If different actors hold conflicting state, the AI does not silently merge. It stops and presents both copies to the user, asking which to take as canonical. The chosen copy becomes the new authoritative state. The conflict is logged for retrospective analysis โ these should be rare; if they happen often, the lease mechanism is failing (see ai-engineering-strategy.md ยง15).
Step 5: Validate completenessโ
Before proceeding to step 4 of the autonomous loop (Evaluate Policies), the AI checks the reconciled state for required fields: Active actor, Phase, Plan, Files touched, Next step. If any are missing, the AI asks the user to fill the gap before continuing. A half-state is worse than no state because it gives false confidence to the next session.
Step 6: Persist and proceedโ
The reconciled state is written to both the local cache and the remote Issue body (if the remote was outdated). The AI then proceeds with the autonomous loop, starting from the Next step.
Handoff scenarios this design supportsโ
Same person, mobile resume. Phone Claude opens the Issue, reads the context, runs /continue. Identical experience to the laptop. The local cache on the laptop is older than the Issue body once the phone session flushes; on next laptop start, reconciliation step 3 (remote_updated_at > local_updated_at) pulls remote.
Sick coworker handoff. Original developer runs /handoff. The AI updates the Issue body with current state and tags the next actor in the Active actor field. The next actor opens the Issue, runs /continue, and resumes. Step 4 of the loop loads the same policies and agents the original session would have.
Cross-machine same person. Identical to mobile resume. Different machines, same actor โ step 3 pulls remote when one is newer.
Task complete. On /done, the AI proposes a final "AI Context" update that summarises what shipped, links the PR, and lists any followups. This is the version managers and teammates see going forward.
The state-sync skill (the write side)โ
The state-sync skill persists the Issue body's "AI Context" and mirrors a condensed copy to the ClickUp ai_context field, keeping the bidirectional GitHub Issue โ ClickUp link intact so any session can answer "which ClickUp task does this Issue belong to." Target cadence is every 30 minutes or every 3 commits, whichever comes first; until a true background runner exists, it fires at task checkpoints (invoked from /continue steps 8 and 12 โ phase change, ~3 commits, before context pressure, on pause/handoff//done).
The skill belongs to the "always silent" autonomy category. Persisting state is never gated โ pausing for approval here would defeat the recoverability the design exists for.
Producing summaries from the mirrorโ
Because every in-progress task carries a current AI Context block (authoritative on the GitHub Issue, mirrored to ClickUp), a progress summary needs no extra digging. /daily-summary reads the mirrored state to produce either a cross-task standup digest (shipped / in-review / in-progress / stuck) for the daily, or a single-task progress summary (/daily-summary <task>) for a task-review discussion. /status is the lighter "what's on my plate, by urgency" cousin. This closes the loop: every task updates both trackers, and both summary views read straight from that state.
Why this beats simpler alternativesโ
Why not just a local file? Local files don't survive laptop close + phone resume. They don't survive coworker handoff. They don't give management dashboards a view.
Why not just a ClickUp custom field? ClickUp's writes are slower than GitHub's, the field is rate-limited, and PMs accidentally edit it. The field is fine as a mirror; it can't be the source of truth.
Why not just the GitHub Issue body? Every session start would round-trip to GitHub before the AI did anything. The local cache eliminates that latency for the common case (same session resuming).
Why a reconciliation algorithm instead of "last write wins"? Multi-actor: two machines, one task, parallel updates. Last-write-wins silently destroys the loser's work. Conservative conflict (step 4) makes the user pick. That's the only safe default at this scale.
See alsoโ
state-syncskill โ the write side: AI Context to the GitHub Issue + ClickUp mirror + bidirectional link./daily-summaryโ standup / per-task progress summary over the mirrored state;/statusโ work grouped by urgency.- ai-engineering-strategy.md ยง14 โ strategic case, with the multi-storage hierarchy diagram.
- deterministic-ai.md principle 5 โ State is cloud-first, local-cached.
- autonomy-model.md โ
state_sync_updatelives innever_ask; this design relies on that exemption. - ai-engineering-strategy.md ยง15 โ the lease mechanism (deferred) that prevents the Actor mismatch case in step 4 from happening in the first place.