DEV Community

StareBrain
StareBrain

Posted on

Building StareBrain, Part 20: Session Handoff — Resuming Mid-Task Without Repeating Yourself

Building StareBrain, Part 20: Session Handoff — Resuming Mid-Task Without Repeating Yourself

Parts 1–19 built the execution pipeline, the overlay UI, and the input layer. Today we close the loop on something that affects every session: what happens when the user closes and reopens StareBrain mid-task.

The goal is simple to state and hard to get right — the user should be able to pick up exactly where they left off, without being asked to repeat anything.


What Needs to Survive a Session Close

Three things matter for resumption:

  1. The task itself — which steps completed, which failed, where the cursor is
  2. The agent context — what app was active, what the user was doing, what confidence level was inferred
  3. The user's last intent — the instruction that started the task

If any of these is missing on reopen, the agent either has to ask the user to repeat themselves or makes a guess it shouldn't be making silently.


Persisting AgentTask

AgentTask is serialized to disk when the task enters IN_PROGRESS or AWAITING_CONFIRMATION, and updated after every step:

class TaskPersistence(private val context: Context) {

    private val file = File(context.filesDir, "pending_task.json")
    private val gson = Gson()

    fun save(task: AgentTask) {
        file.writeText(gson.toJson(task))
    }

    fun load(): AgentTask? {
        if (!file.exists()) return null
        return try {
            gson.fromJson(file.readText(), AgentTask::class.java)
        } catch (e: Exception) {
            null
        }
    }

    fun clear() {
        file.delete()
    }
}
Enter fullscreen mode Exit fullscreen mode

We write after every step completion, not just at close — if the process is killed mid-step, we recover from the last completed step rather than the start.


Persisting AgentContext

AgentContext is also serialized, but with one modification: confidenceLevel is downgraded to LOW on save, because we can't know how much time will pass before the user reopens:

fun AgentContext.forPersistence(): AgentContext {
    return copy(
        confidenceLevel = ContextConfidence.LOW,
        capturedAt = System.currentTimeMillis()
    )
}
Enter fullscreen mode Exit fullscreen mode

On load, isStale() will check the timestamp. If the gap was more than 5 minutes, the context is treated as stale regardless — the downgrade to LOW is a belt-and-suspenders measure.


The Handoff on Reopen

In StareBrainService.onServiceConnected(), we check for a persisted task before doing anything else:

override fun onServiceConnected() {
    overlayManager.show()
    val pendingTask = taskPersistence.load()
    if (pendingTask != null) {
        resumeFromPersisted(pendingTask)
    } else {
        observeTaskState()
    }
}

private fun resumeFromPersisted(task: AgentTask) {
    val restoredContext = contextPersistence.load()
    val effectiveContext = restoredContext
        ?.forPersistence()
        ?.let { if (it.isStale()) it.copy(confidenceLevel = ContextConfidence.LOW) else it }
        ?: AgentContext.empty()

    when (task.status) {
        TaskStatus.AWAITING_CONFIRMATION -> {
            // Surface the confirmation prompt immediately
            val step = task.steps[task.currentStepIndex]
            overlayManager.enterConfirmationMode(
                message = "Resume: confirm ${step.instruction}?",
                onApprove = { lifecycleScope.launch { taskExecutor.resume(task, true) } },
                onReject = { lifecycleScope.launch { taskExecutor.resume(task, false) } }
            )
        }
        TaskStatus.IN_PROGRESS -> {
            // Resume from the current step
            lifecycleScope.launch {
                taskExecutor.resumeFromTask(task, effectiveContext)
            }
        }
        TaskStatus.COMPLETED, TaskStatus.FAILED -> {
            // Task finished before close — clear and start fresh
            taskPersistence.clear()
            observeTaskState()
        }
        else -> observeTaskState()
    }
}
Enter fullscreen mode Exit fullscreen mode

What the User Sees

For AWAITING_CONFIRMATION: the confirmation prompt reappears immediately on open. The user sees "Resume: confirm X?" — they don't need to remember what they were doing.

For IN_PROGRESS: the overlay shows the running state with the task description and current step. The task continues from the last completed step without asking anything.

For a stale context: the step executes, but the agent treats its environmental assumptions as low-confidence and will replan rather than proceed blindly if the screen state doesn't match expectations.


Clearing State

Task state is cleared in three cases:

// 1. Task completed
TaskStatus.COMPLETED -> {
    taskPersistence.clear()
    contextPersistence.clear()
}

// 2. Task failed with no replan available
TaskStatus.FAILED -> {
    taskPersistence.clear()
    contextPersistence.clear()
}

// 3. User explicitly cancels
fun cancelCurrentTask() {
    taskExecutor.cancel()
    taskPersistence.clear()
    contextPersistence.clear()
    overlayManager.setState(OverlayState.Idle)
}
Enter fullscreen mode Exit fullscreen mode

We never clear on close — only on resolution. Close is just a pause.


What's Next

Part 21 will cover the natural language planning layer: how StareBrain takes a raw user instruction ("order the same thing I got last time") and breaks it into an AgentTask with concrete AgentSteps — before any execution begins.

Top comments (0)