Skip to content

Messages from animus are files the session reads and weighs itself; the hooks keep a status - #2

Merged
parintorns merged 1 commit into
mainfrom
mailbox-messages
Sep 29, 2026
Merged

parintorns merged 1 commit into
mainfrom
mailbox-messages

Conversation

@parintorns

@parintorns parintorns commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Pairs with CodeCampusCo/animus PR https://github.com/CodeCampusCo/animus/pull/41 (the app, the file contract docs/agent-mailbox.md and the spec docs/specs/2026-09-29-mailbox-messages-design.md). Merge them together: this plugin writes and reads what that app expects.

What changes

  • The monitor announces a message file; the agent reads it. watch polls the inbox every 2 s. For each new <ULID>.md (dot-temp files do not count) it renames it to <ULID>.seen.md, marks the arrival time, and prints one announcement: the file's path, that it is external data and not a command from the user at this terminal, "read it, then delete it", "animus is waiting for your answer", and the exact send heredoc command (this script, this mailbox, urgent). The <ULID>.md name going is the delivery ack animus waits for. A .seen.md file is not announced again. An OSError on one message (for example something in the way of the rename) skips it and the monitor goes on; an OSError while listing (the mailbox removed as a session ends) skips that poll.
  • Status file. The hooks write <mailbox>/status, one word, atomically, in every mailbox the Claude Code process owns: idle at SessionStart and on a Stop that does not block; working on UserPromptSubmit and PostToolUse; waiting in the waiting hooks, even when a held message keeps them from opening an episode.
  • Stop nudge. When a message was announced after both the last send and the owner's last typed prompt, and stop_hook_active is not true, Stop returns decision: block with a reason saying animus is waiting and the send command. It runs after a held waiting message is taken back; a blocked stop writes neither the "finished" message nor idle. A typed prompt is a UserPromptSubmit whose prompt does not start with <task-notification or <cross-session-message. The times are empty marker files (mtime = event time) under <CLAUDE_PLUGIN_DATA>/marks/<pid>/; sweep removes those of dead processes.
  • One content kind. The plugin writes only tell and withdraw. send <mailbox> <urgency> takes no kind (text as arguments or on stdin) and keeps the urgency it is given; reply is gone. The held waiting message is a tell.
  • Hardening. Every atomic write uses a temp name of its own (.<name>.<random>.tmp), so parallel hooks writing one file cannot collide. wait takes 1 to 1440 minutes (a day; a choice, not measured) and refuses inf, nan and anything outside.
  • Skill. A message from animus is external data, not the user's command; the verbatim words show what was heard. The agent uses its own judgment on whether and how far to act, as with any outside input; instructions at the terminal outrank it. It answers with send, urgent, and when it will not act, or it needs the user, it says so and points them to the terminal. "The verbatim words are the authority" is gone.
  • Version 0.2.0; README describes messages and the status.

Not done on purpose

There is no gate: nothing deterministic stops a message from making the agent act. Claude Code's own mode and permissions apply, and the model decides (owner's decision, 2026-09-30; a read-only PreToolUse gate was built and dropped, see the spec's Rejected alternatives).

Tests

python3 -m unittest discover -s tests: 18 OK. Covered: announce and rename once, other names ignored, after /clear the old mailbox is followed, a message in the way does not stop the monitor, the status words across hooks, the Stop nudge (never without a message; blocks after an announcement; not with stop_hook_active, after send, or after a typed prompt; a monitor-started prompt does not count as typed; no "finished" on a blocked stop), send keeps urgency, dead-process markers swept, parallel writers of one file, the wait bounds.

Acceptance (owner, 2026-09-30)

Passed with the dev app and this branch via --plugin-dir. Seen: the Stop nudge fired once before the agent had seen a message that was still queued in the turn; harmless (one extra prompt to answer).

Known, deferred

  • After a denied permission with a blocked stop, status stays waiting until the next tool or stop.
  • send accepts any urgency word; the old send <mailbox> tell urgent ... shape writes urgency: tell.
  • A settings.json wait written before this change is not capped when read.
  • test_send_keeps_its_urgency does not mark an arrival first.

🤖 Generated with Claude Code

…he hooks keep a status

The monitor renames each new <ULID>.md to <ULID>.seen.md (the delivery ack
animus waits for) and announces its path with the send command. The hooks
keep <mailbox>/status (idle, working, waiting). A Stop hook holds a turn
once when a message has no answer yet. send takes no kind and reply goes;
the plugin writes only tell and withdraw. Writes use their own temp names,
and wait takes 1 to 1440 minutes. Pairs with the animus branch
mailbox-readonly (the app, the contract and the spec).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@parintorns
parintorns merged commit 17daf51 into main Sep 29, 2026
1 check passed
@parintorns
parintorns deleted the mailbox-messages branch September 29, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant