recv: drop wait_seconds from the MCP tool, always an immediate peek
This commit is contained in:
parent
9e7158f593
commit
ceff662d25
6 changed files with 59 additions and 117 deletions
|
|
@ -121,10 +121,13 @@ in `hive-c0re::dashboard_events::DashboardEvent`.
|
||||||
agent. Always returns a list (`Messages { messages }`) — empty when
|
agent. Always returns a list (`Messages { messages }`) — empty when
|
||||||
nothing's pending, single-pop when `max = None` (default 1, the
|
nothing's pending, single-pop when `max = None` (default 1, the
|
||||||
single-message behaviour), batched up to `max` when caller asks for
|
single-message behaviour), batched up to `max` when caller asks for
|
||||||
more (server-side cap is 5; values above clamp silently).
|
more (server-side cap is 5; values above clamp silently). The wire
|
||||||
`wait_seconds` long-polls for the first message; once one arrives —
|
request still carries an optional `wait_seconds` (long-poll the first
|
||||||
or one is already pending — the call drains up to `max` in total
|
message, once one arrives — or one is already pending — the call
|
||||||
before returning, so a single `Recv` call coalesces a burst.
|
drains up to `max` in total): the harness's own turn-driving loop
|
||||||
|
uses it internally (`hive-agent`'s `recv_next`, 180s). The
|
||||||
|
agent-facing MCP `recv` tool no longer exposes this parameter at all
|
||||||
|
(#2814) — it always passes `wait_seconds: None`, an immediate peek.
|
||||||
|
|
||||||
Per-row bookkeeping inside the broker:
|
Per-row bookkeeping inside the broker:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
|
||||||
## Core tools (always available)
|
## Core tools (always available)
|
||||||
|
|
||||||
**Messaging** (`messaging` group): `send(to, body, in_reply_to?)`,
|
**Messaging** (`messaging` group): `send(to, body, in_reply_to?)`,
|
||||||
`recv(wait_seconds?, max?)`, `ask(question, options?, multi?,
|
`recv(max?)`, `ask(question, options?, multi?,
|
||||||
ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
|
ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
|
||||||
|
|
||||||
- `send` — message a peer (logical name) or the operator
|
- `send` — message a peer (logical name) or the operator
|
||||||
|
|
@ -36,11 +36,10 @@ ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
|
||||||
`[ "operator" ]` to restrict a sub-agent to operator messages only
|
`[ "operator" ]` to restrict a sub-agent to operator messages only
|
||||||
(the topology parent is always reachable regardless of this list —
|
(the topology parent is always reachable regardless of this list —
|
||||||
that carve-out is structural, keyed on parent relationship, not name).
|
that carve-out is structural, keyed on parent relationship, not name).
|
||||||
- `recv` — drain inbox. Without `wait_seconds` (or `0`) returns
|
- `recv` — drain inbox. Always an immediate peek, never blocks (#2814
|
||||||
immediately. Positive value parks the turn up to that many seconds
|
— dropped the `wait_seconds` long-poll param entirely; nobody had a
|
||||||
(cap 180) — incoming messages wake instantly. `max` (default 1, cap
|
use case for it that wasn't already better served by ending the
|
||||||
5) drains up to N rows; `wait_seconds` applies to the first, then
|
turn). `max` (default 1, cap 5) drains up to N rows. Each returned row is prefixed with
|
||||||
drains up to `max` total. Each returned row is prefixed with
|
|
||||||
`[msg #<id>]` (broker row id; note the highest id seen, then pass
|
`[msg #<id>]` (broker row id; note the highest id seen, then pass
|
||||||
it to `ack_until` to bulk-triage the batch). **Graceful shutdown**: when the harness
|
it to `ack_until` to bulk-triage the batch). **Graceful shutdown**: when the harness
|
||||||
receives a stop signal, the inbox becomes fenced and `recv` returns an
|
receives a stop signal, the inbox becomes fenced and `recv` returns an
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,12 @@ pub struct SendArgs {
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
pub struct RecvArgs {
|
pub struct RecvArgs {
|
||||||
/// How long to long-poll for the FIRST message before returning
|
|
||||||
/// the empty marker. Capped at 60s server-side. Default (None)
|
|
||||||
/// is 30s. Useful when an agent wants to park its turn waiting
|
|
||||||
/// for any new work — pick a longer wait to coalesce bursts.
|
|
||||||
#[serde(default)]
|
|
||||||
pub wait_seconds: Option<u64>,
|
|
||||||
/// Maximum number of messages to pop in this round-trip. Default
|
/// Maximum number of messages to pop in this round-trip. Default
|
||||||
/// (None) is 1 (single-message behaviour — exactly what you want
|
/// (None) is 1 (single-message behaviour — exactly what you want
|
||||||
/// when you're called to drive a turn off the first wake). Pass
|
/// when you're called to drive a turn off the first wake). Pass
|
||||||
/// a higher value (capped at 5 server-side) when you've been
|
/// a higher value (capped at 5 server-side) when you've been
|
||||||
/// told the inbox has more queued (the wake prompt mentions
|
/// told the inbox has more queued (the wake prompt mentions
|
||||||
/// pending count) and want to drain everything in one tool call.
|
/// pending count) and want to drain everything in one tool call.
|
||||||
/// Once the long-poll wakes up, the call drains up to `max` in
|
|
||||||
/// total before returning — no extra round-trip needed.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub max: Option<u32>,
|
pub max: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,24 +276,21 @@ impl AgentServer {
|
||||||
|
|
||||||
#[tool(
|
#[tool(
|
||||||
description = "Pop messages from this agent's inbox. Returns one or more messages, or \
|
description = "Pop messages from this agent's inbox. Returns one or more messages, or \
|
||||||
an empty marker if nothing is waiting. \n\n\
|
an empty marker if nothing is waiting. Always an immediate 'anything pending?' peek \
|
||||||
|
— never blocks. \n\n\
|
||||||
**Single-message default**: with no args (or `max: 1`) you get the next message — \
|
**Single-message default**: with no args (or `max: 1`) you get the next message — \
|
||||||
same behaviour the harness uses to drive a turn. Without `wait_seconds` (or with 0) \
|
same behaviour the harness uses to drive a turn. \n\n\
|
||||||
the call returns immediately — a cheap 'anything pending?' peek. \n\n\
|
**When idle, prefer ending the turn over polling here.** Ending the turn is the \
|
||||||
**When idle, prefer ending the turn over parking here.** Ending the turn is the \
|
|
||||||
ONLY path that observes an in-container todo wake (bash-task completions, matrix \
|
ONLY path that observes an in-container todo wake (bash-task completions, matrix \
|
||||||
unread, forge activity) — that signal reaches the harness loop between turns, never \
|
unread, forge activity) — that signal reaches the harness loop between turns, never \
|
||||||
a live `recv` call, so parking in `wait_seconds` while a todo lands means sitting \
|
a live `recv` call, so looping on `recv` while idle means sitting blind until \
|
||||||
blind until the timeout. Ending the turn also checkpoints your session, and costs no \
|
something else wakes you. Ending the turn also checkpoints your session, and costs \
|
||||||
latency vs. parking for a real inbox message either way — the broker wakes the next \
|
no latency vs. polling for a real inbox message either way — the broker wakes the \
|
||||||
turn just as fast. Reach for a positive `wait_seconds` (capped at 180) only for a \
|
next turn just as fast. \n\n\
|
||||||
short, deliberate in-turn block — e.g. confirming something you just triggered lands \
|
|
||||||
within seconds — not as the default way to wait for more work. \n\n\
|
|
||||||
**Batch drain**: pass `max: N` (capped at 5) to drain up to N messages in one \
|
**Batch drain**: pass `max: N` (capped at 5) to drain up to N messages in one \
|
||||||
round-trip. Use this when the wake prompt told you the inbox has more queued, or \
|
round-trip. Use this when the wake prompt told you the inbox has more queued, or \
|
||||||
any time you expect a burst — one tool call beats N consecutive single recvs. \
|
any time you expect a burst — one tool call beats N consecutive single recvs. Empty \
|
||||||
`wait_seconds` still applies to the FIRST message; once one arrives the call drains \
|
result reported the same way regardless of `max`. \n\n\
|
||||||
up to `max` in total. Empty result reported the same way regardless of `max`. \n\n\
|
|
||||||
After popping, the result appends a `(N more message(s) pending …)` line whenever the \
|
After popping, the result appends a `(N more message(s) pending …)` line whenever the \
|
||||||
inbox still has queued messages — so you know whether to drain again (or `ack_until`) \
|
inbox still has queued messages — so you know whether to drain again (or `ack_until`) \
|
||||||
without a separate status check. No line means the inbox is empty."
|
without a separate status check. No line means the inbox is empty."
|
||||||
|
|
@ -301,14 +298,13 @@ impl AgentServer {
|
||||||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
run_tool_envelope("recv", log, async move {
|
run_tool_envelope("recv", log, async move {
|
||||||
let waited = args.wait_seconds.is_some_and(|w| w > 0);
|
|
||||||
let (resp, retries) = self
|
let (resp, retries) = self
|
||||||
.dispatch(hive_core_agent_sock::Request::Recv {
|
.dispatch(hive_core_agent_sock::Request::Recv {
|
||||||
wait_seconds: args.wait_seconds,
|
wait_seconds: None,
|
||||||
max: args.max,
|
max: args.max,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
annotate_retries(format_recv(resp, waited), retries)
|
annotate_retries(format_recv(resp), retries)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,33 +39,26 @@ pub fn format_ack(
|
||||||
|
|
||||||
/// Format helper for `recv`: renders zero, one, or many popped
|
/// Format helper for `recv`: renders zero, one, or many popped
|
||||||
/// messages. Empty list collapses to "(empty)" so claude doesn't go
|
/// messages. Empty list collapses to "(empty)" so claude doesn't go
|
||||||
/// hunting for content; when `waited` is set (the call parked on a
|
/// hunting for content. A single message renders as the historical
|
||||||
/// long-poll that timed out) the empty result also carries
|
/// `from: X\n\nbody` block (banner first if `redelivered`). A
|
||||||
/// [`IDLE_WAIT_HINT`] nudging the model toward other work. A single
|
/// multi-message batch renders with a `popped N message(s):` header
|
||||||
/// message renders as the historical `from: X\n\nbody` block (banner
|
/// and `---` separators between bodies so the model can tell where
|
||||||
/// first if `redelivered`). A multi-message batch renders with a
|
/// one ends and the next begins; per-message redelivery banners
|
||||||
/// `popped N message(s):` header and `---` separators between bodies
|
/// included.
|
||||||
/// so the model can tell where one ends and the next begins;
|
|
||||||
/// per-message redelivery banners included.
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn format_recv(
|
pub fn format_recv(resp: Result<hive_core_agent_sock::Response, anyhow::Error>) -> String {
|
||||||
resp: Result<hive_core_agent_sock::Response, anyhow::Error>,
|
|
||||||
waited: bool,
|
|
||||||
) -> String {
|
|
||||||
match resp {
|
match resp {
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
Ok(hive_core_agent_sock::Response::Messages {
|
||||||
messages,
|
messages,
|
||||||
remaining,
|
remaining,
|
||||||
}) => render_recv_messages(&messages, remaining, waited),
|
}) => render_recv_messages(&messages, remaining),
|
||||||
// A graceful stop is pending — the inbox is fenced. Render a single
|
// A graceful stop is pending — the inbox is fenced. Render a single
|
||||||
// explicit directive (not an empty inbox, which claude's "park on recv"
|
// explicit directive (not an empty inbox, which claude might just
|
||||||
// habit would long-poll again, stalling the stop-checkpoint turn until
|
// recv again) so every recv during the stop unmissably tells claude
|
||||||
// the drain wait times out into a hard stop) so every recv during the
|
// to flush + end. `remaining` is forced to 0 — the inbox is fenced,
|
||||||
// stop unmissably tells claude to flush + end. `remaining` is forced
|
// so a "N more pending" hint would be misleading.
|
||||||
// to 0 — the inbox is fenced, so a "N more pending" hint would be
|
|
||||||
// misleading.
|
|
||||||
Ok(hive_core_agent_sock::Response::GracefulStop) => {
|
Ok(hive_core_agent_sock::Response::GracefulStop) => {
|
||||||
render_recv_messages(&[graceful_stop_message()], 0, waited)
|
render_recv_messages(&[graceful_stop_message()], 0)
|
||||||
}
|
}
|
||||||
other => reply_err(other, "recv"),
|
other => reply_err(other, "recv"),
|
||||||
}
|
}
|
||||||
|
|
@ -92,18 +85,10 @@ fn graceful_stop_message() -> hive_sh4re::DeliveredMessage {
|
||||||
/// depth; when non-zero a shared "(N more pending …)" hint (identical to the
|
/// depth; when non-zero a shared "(N more pending …)" hint (identical to the
|
||||||
/// wake prompt's) is appended so an in-turn drain knows more is queued. The
|
/// wake prompt's) is appended so an in-turn drain knows more is queued. The
|
||||||
/// empty path never carries the hint (nothing was popped).
|
/// empty path never carries the hint (nothing was popped).
|
||||||
fn render_recv_messages(
|
fn render_recv_messages(messages: &[hive_sh4re::DeliveredMessage], remaining: u64) -> String {
|
||||||
messages: &[hive_sh4re::DeliveredMessage],
|
|
||||||
remaining: u64,
|
|
||||||
waited: bool,
|
|
||||||
) -> String {
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
if messages.is_empty() {
|
if messages.is_empty() {
|
||||||
return if waited {
|
return "(empty)".to_owned();
|
||||||
format!("(empty){IDLE_WAIT_HINT}")
|
|
||||||
} else {
|
|
||||||
"(empty)".to_owned()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
let mut out = if messages.len() == 1 {
|
let mut out = if messages.len() == 1 {
|
||||||
let m = &messages[0];
|
let m = &messages[0];
|
||||||
|
|
@ -151,14 +136,6 @@ fn msg_id_tag(id: i64) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appended to the `recv` empty result when the agent parked on a
|
|
||||||
/// long-poll (`wait_seconds > 0`) that timed out with nothing new.
|
|
||||||
/// Nudges the model to spend the idle time on other useful work
|
|
||||||
/// instead of immediately re-blocking on `recv`.
|
|
||||||
pub const IDLE_WAIT_HINT: &str = " — nothing arrived before the wait timed out. \
|
|
||||||
If you have other useful work (assigned issues, in-flight PRs, a docs sweep, \
|
|
||||||
notes to update), do that now rather than immediately parking on recv again.";
|
|
||||||
|
|
||||||
/// Inner renderer for a `Vec<LooseEnd>` already extracted from the socket
|
/// Inner renderer for a `Vec<LooseEnd>` already extracted from the socket
|
||||||
/// reply. Called by the `get_loose_ends` handler, which injects the
|
/// reply. Called by the `get_loose_ends` handler, which injects the
|
||||||
/// `UnreadMatrix` entry before formatting.
|
/// `UnreadMatrix` entry before formatting.
|
||||||
|
|
@ -544,7 +521,7 @@ pub fn annotate_retries(mut s: String, retries: u32) -> String {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{IDLE_WAIT_HINT, format_recv};
|
use super::format_recv;
|
||||||
|
|
||||||
fn msg(id: i64, from: &str, body: &str) -> hive_sh4re::DeliveredMessage {
|
fn msg(id: i64, from: &str, body: &str) -> hive_sh4re::DeliveredMessage {
|
||||||
hive_sh4re::DeliveredMessage {
|
hive_sh4re::DeliveredMessage {
|
||||||
|
|
@ -557,39 +534,20 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_recv_after_wait_appends_idle_hint() {
|
fn empty_recv_renders_bare_empty_marker() {
|
||||||
let out = format_recv(
|
let out = format_recv(Ok(hive_core_agent_sock::Response::Messages {
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
messages: vec![],
|
||||||
messages: vec![],
|
remaining: 0,
|
||||||
remaining: 0,
|
}));
|
||||||
}),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert!(out.starts_with("(empty)"));
|
|
||||||
assert!(out.contains(IDLE_WAIT_HINT));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_recv_without_wait_has_no_hint() {
|
|
||||||
let out = format_recv(
|
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
|
||||||
messages: vec![],
|
|
||||||
remaining: 0,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert_eq!(out, "(empty)");
|
assert_eq!(out, "(empty)");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_recv_with_remaining_appends_pending_hint() {
|
fn single_recv_with_remaining_appends_pending_hint() {
|
||||||
let out = format_recv(
|
let out = format_recv(Ok(hive_core_agent_sock::Response::Messages {
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
messages: vec![msg(7, "alice", "hi")],
|
||||||
messages: vec![msg(7, "alice", "hi")],
|
remaining: 3,
|
||||||
remaining: 3,
|
}));
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert!(out.starts_with("[msg #7] from: alice"));
|
assert!(out.starts_with("[msg #7] from: alice"));
|
||||||
assert!(out.contains("3 more message(s) pending"));
|
assert!(out.contains("3 more message(s) pending"));
|
||||||
assert!(out.contains("max: 3"));
|
assert!(out.contains("max: 3"));
|
||||||
|
|
@ -597,25 +555,19 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_recv_no_remaining_has_no_pending_hint() {
|
fn single_recv_no_remaining_has_no_pending_hint() {
|
||||||
let out = format_recv(
|
let out = format_recv(Ok(hive_core_agent_sock::Response::Messages {
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
messages: vec![msg(7, "alice", "hi")],
|
||||||
messages: vec![msg(7, "alice", "hi")],
|
remaining: 0,
|
||||||
remaining: 0,
|
}));
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert!(!out.contains("more message(s) pending"));
|
assert!(!out.contains("more message(s) pending"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn batch_recv_with_remaining_appends_pending_hint_once() {
|
fn batch_recv_with_remaining_appends_pending_hint_once() {
|
||||||
let out = format_recv(
|
let out = format_recv(Ok(hive_core_agent_sock::Response::Messages {
|
||||||
Ok(hive_core_agent_sock::Response::Messages {
|
messages: vec![msg(7, "alice", "hi"), msg(8, "bob", "yo")],
|
||||||
messages: vec![msg(7, "alice", "hi"), msg(8, "bob", "yo")],
|
remaining: 9,
|
||||||
remaining: 9,
|
}));
|
||||||
}),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert!(out.starts_with("popped 2 message(s):"));
|
assert!(out.starts_with("popped 2 message(s):"));
|
||||||
assert_eq!(out.matches("more message(s) pending").count(), 1);
|
assert_eq!(out.matches("more message(s) pending").count(), 1);
|
||||||
// `max` suggestion is clamped to the server-side recv cap.
|
// `max` suggestion is clamped to the server-side recv cap.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
|
||||||
|
|
||||||
Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look:
|
Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look:
|
||||||
|
|
||||||
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__ask`, `mcp__hyperhive__answer`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over parking in `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint); and `ask`/`answer` are async — `ask` returns immediately with a question id, the reply lands later as a `question_answered` system event, never block a turn waiting on it inline.
|
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__ask`, `mcp__hyperhive__answer`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint); and `ask`/`answer` are async — `ask` returns immediately with a question id, the reply lands later as a `question_answered` system event, never block a turn waiting on it inline.
|
||||||
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
|
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
|
||||||
- **Lifecycle** (_requires `lifecycle` tool group_, direct children only, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`.
|
- **Lifecycle** (_requires `lifecycle` tool group_, direct children only, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`.
|
||||||
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_apply_commit`, `request_update_meta_inputs`.
|
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_apply_commit`, `request_update_meta_inputs`.
|
||||||
|
|
@ -44,4 +44,4 @@ When your inbox has a message, handle it and stop. Don't narrate intent — act.
|
||||||
|
|
||||||
**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/` **at every natural boundary, not just when a turn happens to end**, and stop.
|
**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/` **at every natural boundary, not just when a turn happens to end**, and stop.
|
||||||
|
|
||||||
For multi-step work (long builds, sequential edits) that spans more than one turn: end the turn and let an external wake drive the next one — a new inbox message, a `remind` you scheduled, or a backgrounded bash task's completion. Ending the turn is never a same-turn continuation: it's the checkpoint itself, and the only place an in-container todo wake (bash-task completion, matrix unread, forge activity) can reach you — a long-poll `recv(wait_seconds: …)` blocks _within_ the current turn instead and misses exactly those wakes, so it isn't a substitute for ending the turn. **Keep `set_status` current whenever the work changes** — a stale status is what actually loses context across a restart, not turn length; a status set at the start of a task and never touched again is a bug, not a shortcut.
|
For multi-step work (long builds, sequential edits) that spans more than one turn: end the turn and let an external wake drive the next one — a new inbox message, a `remind` you scheduled, or a backgrounded bash task's completion. Ending the turn is never a same-turn continuation: it's the checkpoint itself, and the only place an in-container todo wake (bash-task completion, matrix unread, forge activity) can reach you — polling `recv` in a loop happens _within_ the current turn instead and misses exactly those wakes, so it isn't a substitute for ending the turn. **Keep `set_status` current whenever the work changes** — a stale status is what actually loses context across a restart, not turn length; a status set at the start of a task and never touched again is a bug, not a shortcut.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue