Skip to content

"Closing time" should skip checkpoint for idle agents with clean working trees #59

@billrehm

Description

@billrehm

Problem/Motivation

When the user presses "closing time," ClosingTimeController.start() (in src/main/closingTime.ts) builds a workers set from getLiveAgentIds() and sends GOD a shutdown brief listing every live worker. GOD then broadcasts to all workers, each worker runs a full LLM turn to process the closing-time instruction, commits/parks WIP, writes to memory.md, and sends a CLOSING-TIME-ACK. GOD collects every ACK, saves its own state, and sends CLOSING-TIME-COMPLETE. Only then does the app quit.

There is no idle-state check anywhere in this flow. An agent that has been sitting idle for 30 minutes with nothing to commit still goes through the full cycle: receive the broadcast, wake up, run an LLM turn to decide there's nothing to save, write to memory.md anyway, and ACK. GOD also burns tokens processing each ACK.

For a common scenario (three idle agents, end of day), this means four full Claude turns (three workers + GOD's ACK processing and conclusion) generating tokens purely to confirm there's nothing to do. The 6-minute TIMEOUT_MS compounds the pain if any agent is slow to drain its inbox.

Proposed solution

In ClosingTimeController.start(), before adding an agent to the workers set, check whether it's idle with nothing to save. The main process already has the data:

telemetry.snapshot() provides per-agent ts (last activity timestamp) used to compute lastActiveSecAgo in writeFleetSnapshot() (line ~480 of index.ts). hive.inboxBacklog(id) gives unread inbox count. The git:status IPC handler (line ~1252 of index.ts) can check working tree state.

The check would go in the filter at lines 99-103 of closingTime.
// Current:
this.workers = new Set(
[...live].filter((id) => {
const a = reg.agents[id];
return id !== this.godId && !!a && !a.isGod;
})
);

// Proposed: add idle detection
this.workers = new Set(
[...live].filter((id) => {
const a = reg.agents[id];
if (!a || a.isGod || id === this.godId) return false;
// Fast-track idle agents: no save cycle needed
if (isIdleAndClean(id)) {
this.fastTracked.add(id); // track for UI progress
return false; // exclude from ACK roster
}
return true;
})
);

Where isIdleAndClean(id) checks:

  1. lastActiveSecAgo > threshold (e.g. 120s) from the telemetry snapshot
  2. inboxBacklog === 0 (no unread messages to process)
  3. git status --porcelain is empty for the agent's cwd (no uncommitted changes)

Fast-tracked agents would be killed immediately with the teardown rather than waiting for ACKs. If all workers are idle, GOD gets the zero-worker brief ('There are no workers on the floor right now -- do steps 3 and 4 immediately.', already handled at line 125) and only saves its own state before sending COMPLETE. Near-instant quit.

The ClosingTimeController constructor would need two additional dependencies: access to the telemetry snapshot and a way to run a synchronous git-status check. Both already exist in the main process scope where the controller is instantiated (lines 1356-1367 of index.ts).

Alternatives considered

  • The simplest workaround today is to click "kill all & quit" when you know your agents are idle. This works, but it requires the user to make a judgment call about whether agents have unsaved state. The point of "closing time" is to remove that judgment call. If closing time is slow enough that users learn to avoid it in favor of kill-all, the feature loses its value as the safe default.
  • A simpler implementation: skip the git-status check entirely and use only lastActiveSecAgo > threshold + inboxBacklog === 0. Cheaper (no filesystem call per agent) and covers the majority case. The risk is an agent that finished work but didn't commit, then went idle. In practice this is rare because Claude Code's normal flow includes committing as part of task completion, but it's not guaranteed.
  • Another option: keep the current protocol but have GOD skip the broadcast-and-wait for agents it can see are idle in fleet.json. This keeps the optimization in the LLM layer rather than the harness, but it's less reliable (GOD might not check fleet.json) and still burns GOD's tokens to make the determination.

Area

Terminals / PTY

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions