From f89b4a768baefb9eaa510ce91bf2fb6b00e32d37 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 05:25:35 +0000 Subject: [PATCH 1/5] Sonny triggers: a sent one-shot keeps its row until its turn ends well A one-off, or a `--runs` trigger's last run, deleted its state row as soon as its message went out, so a turn that failed afterwards (offline, a login expired) left nothing for /background, the footer's count or the triggers tool to show. Its row now stays, and whoever asks the daemon how its turn ended removes it once that turn completed with any answer passed on; a failed one stays until removed, as systemd keeps a failed unit until reset-failed. Fixes #422. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jkdGEbQZnXUy1kATtYcQ6 --- docs/SONNY.md | 7 +++- sonny/FEATURES.md | 2 +- sonny/src/main.rs | 4 +++ sonny/src/served.rs | 4 +++ sonny/src/trigger.rs | 79 ++++++++++++++++++++++++++++++++++++-------- 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/docs/SONNY.md b/docs/SONNY.md index 678cce5a7..9c1975d8d 100644 --- a/docs/SONNY.md +++ b/docs/SONNY.md @@ -210,7 +210,12 @@ anything else a failure can leave is listed and removable: gone, one that reached `--runs`), both under the lock and only while the plist is still the one it fired for: a trigger removed while its message went out is left as it now is. A one-off that - delivered leaves nothing. One that ends without delivering (its agent + delivered keeps its last result until its turn is known to have ended: + whoever asks the daemon how it ended (`/background`, the footer's count, + the `triggers` tool) removes the row once that turn completed with any + answer passed on, and one whose turn failed stays listed with + `last.turn_error` until it is removed, as systemd keeps a failed unit + until `reset-failed`. One that ends without delivering (its agent gone, the daemon unreachable, `missed`) loses its plist but keeps its last result, so `ls` lists it as not delivered, and why, until it is removed. When that result cannot be written, the plist stays, diff --git a/sonny/FEATURES.md b/sonny/FEATURES.md index 8cafd43a9..7062c0ac0 100644 --- a/sonny/FEATURES.md +++ b/sonny/FEATURES.md @@ -126,7 +126,7 @@ per feature; group small things into the row they belong to. | Lookout: tells a lead what its tasks did | shipped | added 10-26 #154, #161; one per daemon 10-27 #180; subscriptions 10-27 #195; subscribers only, leads subscribed 10-27 #212; news within a minute 10-27 #228, #232; a companion of the daemon 10-29 #370; a turn's end carries its reply 10-30 #378; stays light with thousands of bots 10-30 #398; a task's question is news 10-30 #395; follows only the event kinds it reads 10-30 #407 | A lead can rest while tasks work and hear the moment they need it | ? | | Lookout judge: a model decides what interrupts the lead (drift, interruptions, look-fors) | removed | added 10-26 #154; judge forks expire 10-27 #203; drift check 10-27 #204; interruptions only 10-27 #215; one judge with the approver 10-27 #217; picks 10-28 #239, #273; removed 10-29 #310 | Removed because the checks felt ad hoc and did not scale; the lookout now forwards everything and only picks the timing | ? | | Lookout sources: pollers the person sets up feed news to agents | experimental | added 10-29 #305 | Agents hear about the world, not only their tasks | ? | -| `/background`: triggers and lookout subscriptions, fire or remove | shipped | added 10-28 #267; in the pane 10-28 #283; what failed first, `✗ N failed` in the footer and the glance, turn failures in the triggers tool 10-30 | See and control what runs while nobody watches; a nightly run that failed while the laptop was offline or a login expired otherwise fails unseen | ? | +| `/background`: triggers and lookout subscriptions, fire or remove | shipped | added 10-28 #267; in the pane 10-28 #283; what failed first, `✗ N failed` in the footer and the glance, turn failures in the triggers tool 10-30; a one-off whose turn fails after it sent still shows 10-30 | See and control what runs while nobody watches; a nightly run that failed while the laptop was offline or a login expired otherwise fails unseen | ? | ## Approvals diff --git a/sonny/src/main.rs b/sonny/src/main.rs index 522a401b8..c5421ce9f 100644 --- a/sonny/src/main.rs +++ b/sonny/src/main.rs @@ -6182,6 +6182,10 @@ impl App { if !unended.is_empty() { let ended = trigger::ended(&self.client, unended).await; if !ended.is_empty() { + // A trigger that ended goes once its last turn did well. + if let (Some(store), Ok(places)) = (self.client.store(), trigger::Places::home()) { + trigger::forget_done(&places, store, &ended); + } self.turns.extend(ended); items = read(&self.turns); } diff --git a/sonny/src/served.rs b/sonny/src/served.rs index ba958cc80..6ab9e815d 100644 --- a/sonny/src/served.rs +++ b/sonny/src/served.rs @@ -633,6 +633,10 @@ async fn called(name: &str, arguments: &Value) -> Answer { for row in rows { crate::trigger::with_turn(row, &ended); } + if let (Some(store), Ok(places)) = (client.store(), crate::trigger::Places::home()) + { + crate::trigger::forget_done(&places, store, &ended); + } client.close().await; } Ok(said.to_string()) diff --git a/sonny/src/trigger.rs b/sonny/src/trigger.rs index 92a150920..9f9e1dedd 100644 --- a/sonny/src/trigger.rs +++ b/sonny/src/trigger.rs @@ -2165,11 +2165,11 @@ fn settle( || outcome["reply"]["gone"] == true || trigger.runs.is_some_and(|runs| kept.sent >= runs); // One that did not deliver ends only once why is on disk; else its plist - // stays, listed. - // An answer that did not get through keeps its row, saying so. - let kept_row = !sent || outcome["reply"]["outcome"] == "failed"; + // stays, listed. Its row stays whatever came of it: one that sent keeps + // it until its turn is known to have ended well (`forget_done`), so a + // turn that fails after the send still shows. if over && (sent || recorded.is_ok()) { - return retire(places, &trigger.name, kept_row, launchd).map_or_else(log, |_| true); + return retire(places, &trigger.name, true, launchd).map_or_else(log, |_| true); } recorded.is_ok() } @@ -3496,6 +3496,51 @@ pub async fn ended(client: &Client, handles: Vec) -> HashMap) { + let done = |name: &str| { + let row = read_json(&places.last(name))?; + let handle = sent(&row)?; + (row["name"] == name + && row["store_id"] == store_id + && row["last"]["reply"]["outcome"] != "failed" + && ended.get(&handle)?["status"] == "completed" + && !places.job(name).exists()) + .then_some(handle) + }; + let names: Vec = (std::fs::read_dir(&places.state) + .into_iter() + .flatten() + .flatten()) + .filter_map(|e| Some(e.file_name().to_str()?.strip_suffix(".json")?.to_owned())) + .filter(|name| valid_name(name).is_ok() && done(name).is_some()) + .collect(); + if names.is_empty() { + return; + } + let Ok(_lock) = Lock::take(places) else { + return; + }; + // Under the lock, only while it is still that row: a trigger made since + // under the name has a job, or a row of its own. + for name in names { + if done(&name).is_some() + && let Err(e) = std::fs::remove_file(places.last(&name)) + && e.kind() != std::io::ErrorKind::NotFound + { + eprintln!( + "{}", + error_json(&format!("{}: {e}", places.last(&name).display())) + ); + } + } +} + /// `row`, its `last` taking how the turn its fire sent ended, from /// `ended`: its `turn_status`, and `turn_error` when it did not complete, /// so a fire that went out and whose turn then failed, as when the machine @@ -3617,12 +3662,7 @@ fn fire(places: &Places, trigger: &Trigger, ask: Option<&Asked>) { // Its last run went out but its end did not (launchd would not unload // it): it ends now, sending nothing more. if trigger.runs.is_some_and(|runs| kept.sent >= runs) { - let last = &durable["last"]; - return ends( - places, - trigger, - last["outcome"] != "sent" || last["reply"]["outcome"] == "failed", - ); + return ends(places, trigger, true); } if !asked { // A one-off's calendar entry has no year: the same date a year early @@ -4770,9 +4810,22 @@ mod tests { fn a_fire_ends_only_the_trigger_it_ran_for() { let w = World::new("fire"); let one = once(); - // Delivered: nothing is left. + // Delivered: its row stays until its turn is known to have ended, + // and stays when that turn failed. w.install(&one).unwrap(); - w.settle(&one, json!({"outcome": "sent", "turn": 2})); + let handle = "turn:p.fix-login/2"; + w.settle( + &one, + json!({"outcome": "sent", "turn": 2, "handle": handle}), + ); + assert_eq!(w.state(&one.name), (false, false, true)); + let ended = |status: &str| HashMap::from([(handle.to_owned(), json!({"status": status}))]); + forget_done(&w.places, "00ab", &ended("failed")); + // Another daemon's turn of that handle is not its. + forget_done(&w.places, "ffff", &ended("completed")); + assert_eq!(w.state(&one.name), (false, false, true)); + // One that completed leaves nothing. + forget_done(&w.places, "00ab", &ended("completed")); assert_eq!(w.state(&one.name), (false, false, false)); // Not delivered: it ends, and its row says why until it is removed. w.install(&one).unwrap(); @@ -4911,7 +4964,7 @@ mod tests { w.settle(&s, json!({"outcome": "skipped"})); assert_eq!(w.state(&s.name), (true, true, true)); w.settle(&s, json!({"outcome": "sent", "turn": 5})); - assert_eq!(w.state(&s.name), (false, false, false)); + assert_eq!(w.state(&s.name), (false, false, true)); } #[test] From 885999ee403c3854e5d8827af915037e27b72d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 05:29:26 +0000 Subject: [PATCH 2/5] Sonny triggers: forget a done one-shot also once it ends after its turn Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jkdGEbQZnXUy1kATtYcQ6 --- sonny/src/main.rs | 13 +++++++++---- sonny/src/trigger.rs | 28 ++++++++++++++++------------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/sonny/src/main.rs b/sonny/src/main.rs index c5421ce9f..699470a23 100644 --- a/sonny/src/main.rs +++ b/sonny/src/main.rs @@ -6182,14 +6182,19 @@ impl App { if !unended.is_empty() { let ended = trigger::ended(&self.client, unended).await; if !ended.is_empty() { - // A trigger that ended goes once its last turn did well. - if let (Some(store), Ok(places)) = (self.client.store(), trigger::Places::home()) { - trigger::forget_done(&places, store, &ended); - } self.turns.extend(ended); items = read(&self.turns); } } + // A trigger that ended goes once its last turn did well, also one + // that ended after its turn did, as a `--reply-to` fire passing on + // the answer. + if !self.turns.is_empty() + && let (Some(store), Ok(places)) = (self.client.store(), trigger::Places::home()) + && trigger::forget_done(&places, store, &self.turns) > 0 + { + items = read(&self.turns); + } let fired: std::collections::HashSet<&String> = items.iter().filter_map(|i| i.handle.as_ref()).collect(); self.turns.retain(|handle, _| fired.contains(handle)); diff --git a/sonny/src/trigger.rs b/sonny/src/trigger.rs index 9f9e1dedd..3eb564cf0 100644 --- a/sonny/src/trigger.rs +++ b/sonny/src/trigger.rs @@ -3501,8 +3501,8 @@ pub async fn ended(client: &Client, handles: Vec) -> HashMap) { +/// Run by whoever asked the daemon how those turns ended. How many went. +pub fn forget_done(places: &Places, store_id: &str, ended: &HashMap) -> usize { let done = |name: &str| { let row = read_json(&places.last(name))?; let handle = sent(&row)?; @@ -3521,24 +3521,28 @@ pub fn forget_done(places: &Places, store_id: &str, ended: &HashMap gone += 1, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => eprintln!( "{}", error_json(&format!("{}: {e}", places.last(&name).display())) - ); + ), } } + gone } /// `row`, its `last` taking how the turn its fire sent ended, from @@ -4820,12 +4824,12 @@ mod tests { ); assert_eq!(w.state(&one.name), (false, false, true)); let ended = |status: &str| HashMap::from([(handle.to_owned(), json!({"status": status}))]); - forget_done(&w.places, "00ab", &ended("failed")); + assert_eq!(forget_done(&w.places, "00ab", &ended("failed")), 0); // Another daemon's turn of that handle is not its. forget_done(&w.places, "ffff", &ended("completed")); assert_eq!(w.state(&one.name), (false, false, true)); // One that completed leaves nothing. - forget_done(&w.places, "00ab", &ended("completed")); + assert_eq!(forget_done(&w.places, "00ab", &ended("completed")), 1); assert_eq!(w.state(&one.name), (false, false, false)); // Not delivered: it ends, and its row says why until it is removed. w.install(&one).unwrap(); From 52468d22a7b754e0d89dfce91bd05f685cdaf33b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 05:49:32 +0000 Subject: [PATCH 3/5] Runtime tests: a sent one-shot keeps its row until its turn ends well Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jkdGEbQZnXUy1kATtYcQ6 --- tests/test_trigger.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_trigger.py b/tests/test_trigger.py index e90abc56f..898ae470a 100644 --- a/tests/test_trigger.py +++ b/tests/test_trigger.py @@ -204,8 +204,10 @@ def test_a_one_off_waits_for_a_working_bot_instead_of_skipping(self): self.agent('run', '--store', str(self.store), '--bot', 'p.task', '--detach', 'gate') time.sleep(0.5) fired = self.fire('p.task', 'Look again.', at=int(time.time())) - # Delivered, it leaves no row behind. - self.assertIsNone(fired) + # Delivered, it ends, keeping its row until its turn is known to + # have ended well. + self.assertEqual(fired['last']['outcome'], 'sent', fired) + self.assertFalse(job(self.home, 'p.task').exists()) self.model.release_headers.set() self.settle('p.task') previews = [t['prompt_preview'] for t in self.turns('p.task')][1:] @@ -421,8 +423,9 @@ def test_a_trigger_at_its_run_limit_sends_nothing_more(self): first = self.fire('p.task', 'once', extra=['--runs', '1'], generation='g1') self.settle('p.task') self.assertEqual(len(self.turns('p.task')), 2) - # Delivered to its limit, it ends leaving nothing. - self.assertIsNone(first) + # Delivered to its limit, it ends, its row kept until its turn is + # known to have ended well. + self.assertEqual(first['last']['outcome'], 'sent', first) self.assertFalse(job(self.home, 'p.task').exists()) def test_a_fire_caps_the_turn_it_starts(self): From f124aa2c2fa9858441ec13541f423fa660362cd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 05:53:19 +0000 Subject: [PATCH 4/5] Sonny triggers: forgetting a done one-shot never waits for the lock Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jkdGEbQZnXUy1kATtYcQ6 --- sonny/src/trigger.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/sonny/src/trigger.rs b/sonny/src/trigger.rs index 3eb564cf0..a49c95691 100644 --- a/sonny/src/trigger.rs +++ b/sonny/src/trigger.rs @@ -1850,6 +1850,16 @@ struct Lock { impl Lock { fn take(places: &Places) -> Result { + Self::flock(places, libc::LOCK_EX) + } + + /// The lock now, or an error when another holds it, for work that can + /// wait for a later turn rather than hold up who asked. + fn try_take(places: &Places) -> Result { + Self::flock(places, libc::LOCK_EX | libc::LOCK_NB) + } + + fn flock(places: &Places, how: libc::c_int) -> Result { std::fs::create_dir_all(&places.state) .map_err(|e| format!("{}: {e}", places.state.display()))?; let path = places.state.join(".lock"); @@ -1861,7 +1871,7 @@ impl Lock { .map_err(|e| format!("{}: {e}", path.display()))?; use std::os::fd::AsRawFd; // SAFETY: flock on a descriptor this struct owns. - if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 { + if unsafe { libc::flock(file.as_raw_fd(), how) } != 0 { return Err(format!( "{}: {}", path.display(), @@ -3523,7 +3533,9 @@ pub fn forget_done(places: &Places, store_id: &str, ended: &HashMap Date: Fri, 30 Oct 2026 06:20:47 +0000 Subject: [PATCH 5/5] Sonny: the lead's next look repeats, with a ready check and a name example The eval of #433 showed leads setting the look as a one-off `at` (3 of 4), with no if command (4 of 4), and one under the wrong name. A one-off whose if command says no ends, so a skipped look was gone. lead.md now says the look repeats (every, or cron for a set hour), never at or in, gives the skip-while-writing check as a command, names it by example (web.lead.next for web.lead), and says the next setting uses replace. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013jkdGEbQZnXUy1kATtYcQ6 --- sonny/FEATURES.md | 4 ++-- sonny/agents/lead.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sonny/FEATURES.md b/sonny/FEATURES.md index 55fbb31f8..ccf4cdc46 100644 --- a/sonny/FEATURES.md +++ b/sonny/FEATURES.md @@ -49,7 +49,7 @@ per feature; group small things into the row they belong to. | Task states and resolving (`/status` groups, `resolve_task`) | shipped | added 10-28 #244; tasks resolve themselves 10-28 #284; a goal's passed check reads `checked` 10-30 #379; one mark per state everywhere, `?` needs you, footer counts say their words, Idle is Done 10-30 #427 | See at a glance what needs you, is working, or is done | ? | | Lead and task roles as editable files (`~/.agents/agents/lead.md`, `task.md`) | shipped | added 10-28 #240; leads and tasks run as any profile, profiles extend others 10-29 #296; an upgrade offers a merge with an edited role 10-29 #357; messages between agents carry what the reader needs to act 10-30 #443; shipped roles have byte budgets (a test), repeated rules merged 10-30 #440 | The person owns the roles; Sonny never overwrites their edits | ? | | How leads work: act across tasks, care about the person, carry what one task lacks to whoever has it, react to the person's words in a task, pick task models | shipped | added 10-27 #229; routing 10-29 #295; reaction 10-29 #330, #340; models from the list 10-30 #385; asked why it starts another task 10-30 #386; memory overrides the role files' defaults, pinned picks left to the model 10-30 #410; a turn waiting on an outside review stays the lead's, opt-in `superlead` checks its projects' work fits together 10-30 #430 | Leads that coordinate instead of only relaying | ? | -| Proactive leads: straight feedback, coaching repeated habits, weighing what sources report, starting what the person said they will do | experimental | added 10-30 #394; commitments 10-30 #399; nightly habit reflection 10-30 #403; a self-set next look (one repeating `LEAD.next` trigger at the earliest time something held goes stale, skipped while the person writes, later when looks find nothing, none when nothing is open; no fixed heartbeat) 10-30 #433 | Agents that flag what the person missed, say what they see unsugared, and follow through without being asked; a lead hears only when something happens, so what goes stale quietly needs a look it sets itself | ? | +| Proactive leads: straight feedback, coaching repeated habits, weighing what sources report, starting what the person said they will do | experimental | added 10-30 #394; commitments 10-30 #399; nightly habit reflection 10-30 #403; a self-set next look (one repeating `LEAD.next` trigger at the earliest time something held goes stale, skipped while the person writes, later when looks find nothing, none when nothing is open; no fixed heartbeat) 10-30 #433; the look repeats (`every` or `cron`), with a ready skip-while-writing check and a name example 10-30 | Agents that flag what the person missed, say what they see unsugared, and follow through without being asked; a lead hears only when something happens, so what goes stale quietly needs a look it sets itself | ? | | Sandbox for leads and tasks by default; trusted commands run outside it | shipped | added 10-28 #285; allow rules 10-29 #309; repositories inside an agent's folder keep their hooks and config 10-30 #414 | A secure default, as Codex does, with ways out when blocked | ? | | `sonny agent ARGS`, the one way agents reach the daemon | shipped | added 10-27 #199, #214 | One documented path instead of `$AGENT_BIN` or a bare `agent` | ? | | Agent actions as declared tools instead of `sonny X` commands | shipped | six commands 10-28 #268; memory and triggers 10-28 #278; current tools given to agents 10-29 #382 | First-class tools the daemon knows, not shell commands | ? | @@ -126,7 +126,7 @@ per feature; group small things into the row they belong to. | Lookout: tells a lead what its tasks did | shipped | added 10-26 #154, #161; one per daemon 10-27 #180; subscriptions 10-27 #195; subscribers only, leads subscribed 10-27 #212; news within a minute 10-27 #228, #232; a companion of the daemon 10-29 #370; a turn's end carries its reply 10-30 #378; stays light with thousands of bots 10-30 #398; a task's question is news 10-30 #395; follows only the event kinds it reads 10-30 #407 | A lead can rest while tasks work and hear the moment they need it | ? | | Lookout judge: a model decides what interrupts the lead (drift, interruptions, look-fors) | removed | added 10-26 #154; judge forks expire 10-27 #203; drift check 10-27 #204; interruptions only 10-27 #215; one judge with the approver 10-27 #217; picks 10-28 #239, #273; removed 10-29 #310 | Removed because the checks felt ad hoc and did not scale; the lookout now forwards everything and only picks the timing | ? | | Lookout sources: pollers the person sets up feed news to agents | experimental | added 10-29 #305 | Agents hear about the world, not only their tasks | ? | -| `/background`: triggers and lookout subscriptions, fire or remove | shipped | added 10-28 #267; in the pane 10-28 #283; what failed first, `✗ N failed` in the footer and the glance, turn failures in the triggers tool 10-30; a one-off whose turn fails after it sent still shows 10-30 | See and control what runs while nobody watches; a nightly run that failed while the laptop was offline or a login expired otherwise fails unseen | ? | +| `/background`: triggers and lookout subscriptions, fire or remove | shipped | added 10-28 #267; in the pane 10-28 #283; what failed first, `✗ N failed` in the footer and the glance, turn failures in the triggers tool 10-30; a one-off whose turn fails after it sent still shows 10-30 #442 | See and control what runs while nobody watches; a nightly run that failed while the laptop was offline or a login expired otherwise fails unseen | ? | ## Approvals diff --git a/sonny/agents/lead.md b/sonny/agents/lead.md index 14b24dbf6..405b043f6 100644 --- a/sonny/agents/lead.md +++ b/sonny/agents/lead.md @@ -30,7 +30,7 @@ With the person, quiet is the default: speak when they need to act or decide, wh To wake an agent later, again and again, or when something happens, add a trigger with the triggers tool, for yourself unless it names a bot, or a start to make one on the first fire in this folder, as a task is made. Write its message as what to check and when to stop, including removing its own trigger once the work is done; give it an if command when a check that finds nothing should cost no turn. To watch something for the person, such as a team channel, an inbox or a queue, do not check it in your own turns: read the automation skill listed under Skills and give it a monitor, its own agent, so only what needs you reaches you. -What you hold can go stale without anything happening, such as a review waiting, a deadline near or a question the person has not answered, and you hear only when something happens. So keep one repeating trigger for yourself, named after you with .next, set to when you would next look: for each thing you hold, judge from its stakes, how much is moving around it, how soon it is due and how urgent the person is, when it would go stale if nobody looked, and take the earliest. Everything you hear feeds that judgement, your tasks and whatever sources the person connected alike: the more that moves around a thing, the closer you stay, and what they tell you about what is coming or where the person is moves it. Give it an if command that skips it while the person is writing, since then they are the check-in, such as one that finds nothing from them in what `~/.agent/memory said` lists since twenty minutes ago. Look later than last time when a look found nothing new, and outside the hours their memory gives, leave for their morning what can wait; when memory says nothing about their hours, judge them from when they write and save what you see. Each look ends by setting it again or removing it once nothing is open, and tells the person only what needs them. +What you hold can go stale without anything happening, such as a review waiting, a deadline near or a question the person has not answered, and you hear only when something happens. So keep one trigger for yourself, named with your own name and .next, such as web.lead.next for web.lead, that repeats: every set to how long until you would next look, or cron for a set hour such as the person's morning, never at or in, since a one-off its if command skips is gone where one that repeats tries again. For each thing you hold, judge from its stakes, how much is moving around it, how soon it is due and how urgent the person is, when it would go stale if nobody looked, and take the earliest. Everything you hear feeds that judgement, your tasks and whatever sources the person connected alike: the more that moves around a thing, the closer you stay, and what they tell you about what is coming or where the person is moves it. Give it this if command, which skips the look while the person wrote in the last twenty minutes, since then they are the check-in: `~/.agent/memory said $(( $(date +%s) * 1000 - 1200000 )) | grep -qF '"said":[]'`. Look later than last time when a look found nothing new, and outside the hours their memory gives, leave for their morning what can wait; when memory says nothing about their hours, judge them from when they write and save what you see. Each look ends by setting it again with replace, or removing it once nothing is open, and tells the person only what needs them. The person you work for may be under pressure, short on time, or carrying more than they mention. Care about how they are doing and whether they succeed, not only about the ask in front of you. Take work off their plate rather than adding to it. Notice what they will need next: do it when it plainly follows from what they asked, otherwise offer it. Help them see what they would not see alone, and get better at what they do. When you see a better way to work, something that keeps costing them time, a risk, something they missed or said they would do and have not, or a mistake of theirs, tell them as soon as you see it, plainly and without softening it, with what to do instead: they would rather hear it straight than late. Say each thing once; when it comes back, the repeat is the news, so name the pattern and coach the habit, not the instance. Do not lecture, flatter or pad a reply to look caring: helping them well is the care.