@ours.network/fleet 1.2.0-nightly.64 → 1.2.0-nightly.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ # Self-reminders
2
+
3
+ A Fleet-managed agent can schedule a one-time reminder that wakes **itself** at a
4
+ later date and time. When the reminder is due, the agent's own supervisor starts
5
+ a turn for it; the agent reads the reminder and decides what to do.
6
+
7
+ A reminder is not a message. It has no recipient, chat, route, API key or
8
+ Telegram setting, and Fleet never sends anything on the agent's behalf. If the
9
+ agent decides someone should hear about it, it uses its ordinary messaging tools
10
+ under its existing instructions and authorization.
11
+
12
+ It works for every managed session (`session: acp` or `session: codex-app-server`,
13
+ Claude or Codex) and every lifetime: persistent, temporary and task-owned. There
14
+ is nothing to install or enable.
15
+
16
+ ## When to use it
17
+
18
+ - Come back to something at a known time: "check whether the release CI finished
19
+ at 16:00", "follow up on the review on Monday morning".
20
+ - Replace polling or sleeping inside a turn. A reminder costs nothing while it
21
+ waits; a turn that sleeps or a loop that polls costs model time every tick.
22
+ - Not for recurring maintenance: use configured `loops` for that.
23
+
24
+ ## Tools
25
+
26
+ The tools are served by the agent's own fixed-identity ours MCP server. No
27
+ argument names an agent or identity: every call acts on the caller's own
28
+ reminders, and an id that belongs to another agent is simply not found.
29
+
30
+ | Tool | Arguments | Result |
31
+ | --- | --- | --- |
32
+ | `fleet_reminder_create` | `request_key`, `title`, `note`?, `local_time`, `time_zone`, `fold`?, `late_window_seconds`? | the reminder (`id`, `revision`, `due_at`, `expires_at`, …) and `created` |
33
+ | `fleet_reminder_list` | `state`? (`open` default, `closed`, `all`), `limit`? (1–100), `after`? | a page of reminders, `more`, `after`, `uncertain_events` |
34
+ | `fleet_reminder_update` | `id`, `expected_revision`, any of the schedule fields | the reminder at its new revision |
35
+ | `fleet_reminder_cancel` | `id`, `expected_revision` | the cancelled reminder |
36
+ | `fleet_reminder_inspect` | `event_id` | the event, its reminder (with `note`), `current`, `eligible` |
37
+ | `fleet_reminder_ack` | `event_id`, `expected_revision` | `acknowledged` (repeat calls return `duplicate: true`) |
38
+
39
+ ### Time
40
+
41
+ `local_time` is a calendar date and wall-clock time **without** an offset
42
+ (`2027-01-20T16:00` or `2027-01-20T16:00:00`); `time_zone` is an IANA zone such
43
+ as `Europe/London`, `America/New_York` or `UTC`. Use the zone of the person or
44
+ task the reminder is about; Fleet never assumes the host's zone. "Tomorrow at
45
+ nine" must be resolved by the agent to a real date first.
46
+
47
+ Daylight-saving changes are explicit:
48
+
49
+ - A time skipped when clocks go forward (`2027-03-28T01:30` in `Europe/London`)
50
+ does not exist and is refused (`nonexistent_local_time`).
51
+ - A time that occurs twice when clocks go back (`2027-10-31T01:30` in
52
+ `Europe/London`) needs `fold`: `0` for the first occurrence, `1` for the second
53
+ (`ambiguous_local_time` otherwise).
54
+ - Invalid dates (`2027-02-30`) are refused. The time must be in the future and
55
+ within five years.
56
+
57
+ `late_window_seconds` (60–604800, default 3600) is how late the wake may still
58
+ arrive if the agent, Fleet or the host was down, paused or busy at the due time.
59
+ After the window the reminder expires without a turn.
60
+
61
+ ### Idempotency and revisions
62
+
63
+ `request_key` identifies one scheduling decision. Repeating `create` with the same
64
+ key and the same input returns the existing reminder, so a retried call after a
65
+ timeout never creates a second one; the same key with different input is refused
66
+ (`request_key_conflict`). Keys are remembered while the reminder is retained (the
67
+ 200 most recent closed reminders are kept; at most 100 can be open).
68
+
69
+ Every `update` and `cancel` names the revision it expects (`stale_revision`
70
+ otherwise) and bumps it. A wake issued for an older revision is void: if it is
71
+ still waiting it is dropped before it reaches the model, and if it was already
72
+ delivered it can no longer be acknowledged.
73
+
74
+ ## Example
75
+
76
+ ```json
77
+ fleet_reminder_create {
78
+ "request_key": "release-ci-check-2027-01-20",
79
+ "title": "Check release CI",
80
+ "note": "PR #412 on prerelease. If CI is green, post the result in the task room; do not merge.",
81
+ "local_time": "2027-01-20T16:00",
82
+ "time_zone": "Europe/London"
83
+ }
84
+ ```
85
+
86
+ At 16:00 London time the agent receives a turn like:
87
+
88
+ ```text
89
+ [fleet-reminder]
90
+ event_id: rev_7c0d…
91
+ reminder_id: rem_1b9e…
92
+ revision: 1
93
+ title: "Check release CI"
94
+ due: 2027-01-20T16:00:00 Europe/London (2027-01-20T16:00:00.000Z)
95
+
96
+ This is a reminder you scheduled for yourself through Fleet. …
97
+ ```
98
+
99
+ It then calls `fleet_reminder_inspect {"event_id": "rev_7c0d…"}`, does the work
100
+ (or decides nothing is needed), and calls
101
+ `fleet_reminder_ack {"event_id": "rev_7c0d…", "expected_revision": 1}`. To
102
+ postpone instead, it calls `fleet_reminder_update` with a new `local_time`; the
103
+ current wake becomes void and a new one is issued at the new time.
104
+
105
+ ## Delivery guarantees and limits
106
+
107
+ - **Who keeps time.** The supervisor process that runs the agent owns the timer
108
+ and the private state (`.self-reminders.json`, mode 0600, in the agent's state
109
+ directory). With no open reminders it arms no timer; otherwise it checks at the
110
+ next due time and at least once a minute. Only a due reminder creates a model
111
+ turn.
112
+ - **When it arrives.** At the first idle turn boundary after the due time, and
113
+ only after the agent's startup turn has succeeded. A wake never interrupts the
114
+ agent and never waits in a queue behind other work; a busy agent receives it
115
+ when its current work finishes. A reminder wake always runs as its own turn and
116
+ is never combined with owner or mail input.
117
+ - **Offline.** Nothing runs while the host is off or asleep, or while Fleet or the
118
+ agent is stopped. Reminders are kept, and those still inside their late window
119
+ are delivered when the agent is back; the rest expire without a turn. Pause and
120
+ restart keep reminders; they never start a stopped agent.
121
+ - **Deletion.** Removing or retiring an agent, or deleting or finishing its task,
122
+ cancels its reminders and deletes their state, including from retained
123
+ evidence. A replaced identity or a new agent with the same name never inherits
124
+ them: a store is bound to the agent's role and identity (and, for a temporary
125
+ or task agent, its runtime instance), and a store that does not match is set
126
+ aside and nothing in it is delivered.
127
+ - **At most once.** Four facts are separate: the supervisor queued the wake, the
128
+ wake was released to the model, the agent acknowledged it, and any external
129
+ action the agent took. A wake is recorded as dispatched, durably, at the moment
130
+ it is handed to the model; if that record cannot be written, it is not handed
131
+ over. After a crash, an event that was never released is retried as the same
132
+ event; an event that may have reached the model is held as `uncertain` and is
133
+ never sent again. `list` reports `uncertain_events`; the agent checks its own
134
+ records, then acknowledges it or schedules something new.
135
+ - **Privacy.** Titles and notes are visible only through the agent's own tools.
136
+ The wake text contains the title, not the note, and the conversation ledger
137
+ stores only a digest of the wake. Logs carry event ids, never reminder text.
138
+
139
+ Operators can see an agent's reminders through the existing supervisor tool
140
+ route, which acts as that agent's supervisor:
141
+
142
+ ```sh
143
+ ours-fleet ours call DeveloperA fleet_reminder_list
144
+ ```
145
+
146
+ ## Migrating from the standalone `self-reminders` worker
147
+
148
+ The standalone skill ran one Python worker, its own SQLite ledger and a user
149
+ service per agent. Built-in reminders replace it; the worker is deprecated.
150
+ Personal-assistant reminder, source and outbound messaging stores are separate
151
+ systems and are not touched by this procedure. Migration is a deliberate,
152
+ separately authorized operation per agent; nothing migrates automatically.
153
+
154
+ Reminder data is handled **by the agent itself** with both tool sets, so its text
155
+ never passes through an operator. The operator only fences and later removes the
156
+ old service.
157
+
158
+ ### 1. Preconditions
159
+
160
+ - The agent runs a Fleet release with built-in reminders:
161
+ `ours-fleet ours tools Name` lists `fleet_reminder_create`.
162
+ - `fleet_reminder_list {"state": "all"}` (every page) shows each reminder's
163
+ `request_key`. Any key starting with `legacy:` belongs to an earlier attempt:
164
+ reconcile it with the rollback table below before starting again. Keys map
165
+ back to old ids (`legacy:<old id>:r<old revision>`), so an interrupted run
166
+ is always resumable from the two lists.
167
+
168
+ ### 2. Fence the old producer persistently
169
+
170
+ A `stop` alone is not a fence: an enabled unit starts again at the next login or
171
+ boot. The operator records the unit's prior state and its unit file, then
172
+ disables it. The old setup enabled a generated unit by absolute path outside the
173
+ unit search path, so disabling removes its only link; keep the recorded path and
174
+ do not delete that file until step 6:
175
+
176
+ ```sh
177
+ systemctl --user is-enabled self-reminders-Name.service # record: enabled/linked/...
178
+ systemctl --user is-active self-reminders-Name.service # record: active/inactive
179
+ systemctl --user show -p FragmentPath self-reminders-Name.service # record the absolute unit file
180
+ systemctl --user disable --now self-reminders-Name.service
181
+ systemctl --user is-enabled self-reminders-Name.service # must not be enabled
182
+ systemctl --user is-active self-reminders-Name.service # must be inactive
183
+ ```
184
+
185
+ From here on no old wake can be produced, across worker, Fleet or host
186
+ restarts, until someone deliberately re-enables the unit. Do not run the old
187
+ `tick` or `serve` commands during the cutover.
188
+
189
+ ### 3. Classify every old reminder
190
+
191
+ The old CLI lists reminders but not events, and its `cancel` leaves earlier
192
+ events in whatever state they had. Classification therefore uses the old
193
+ system's own eligibility rule, not raw event states: an event is **live** only
194
+ if its reminder is still `active`, the event's revision equals the reminder's
195
+ current revision, and its state is `submitting`, `queued` or `uncertain`.
196
+ Anything else is historical: superseded by an update or cancellation, or
197
+ belonging to a reminder that is already `handled`, `cancelled` or `expired`.
198
+
199
+ The operator supplies the metadata with a read-only query that selects no
200
+ title or context:
201
+
202
+ ```sh
203
+ sqlite3 -readonly /path/to/old/ledger.sqlite "
204
+ SELECT r.id, r.revision, r.state,
205
+ datetime(r.due, 'unixepoch'), datetime(r.expires, 'unixepoch'),
206
+ e.id, e.revision, e.state
207
+ FROM self_reminders r LEFT JOIN self_events e
208
+ ON e.reminder_id = r.id AND e.revision = r.revision
209
+ ORDER BY r.due, r.id"
210
+ ```
211
+
212
+ Each old reminder falls into exactly one row:
213
+
214
+ | Old reminder (current revision) | Meaning | Action |
215
+ | --- | --- | --- |
216
+ | `handled`, `cancelled` or `expired` | finished; its events are historical | nothing |
217
+ | `active`, `expires` already passed | lapsed while fenced; the old worker would expire it | old `cancel`, nothing to migrate |
218
+ | `active`, due in the future, no event | not yet fired | **migrate** (step 4), then old `cancel` (step 5) |
219
+ | `active`, already due, no event or event `pending` | due, never submitted | **decide**: migrate to a new future time, or old `cancel`; never both |
220
+ | `active`, event `submitting`, `queued` or `uncertain` | live: may already have reached the agent | **hold**: check the conversation and your own records. If handled and still inside its window, old `complete`. Otherwise old `cancel`, which makes the event historical, and create a new reminder only if one is still useful. Never re-create the old event as-is |
221
+ | `active`, event `handled`, `failed` or `invalidated` | finished for this revision | old `cancel` |
222
+
223
+ **Stop condition:** do not continue to step 6 while any old reminder is still
224
+ `active`. When every row above has been applied, nothing is active and no
225
+ event is live.
226
+
227
+ ### 4. Re-create the reminders that migrate
228
+
229
+ For each reminder marked *migrate*, the agent calls `fleet_reminder_create` with:
230
+
231
+ - `request_key` `legacy:<old id>:r<old revision>`;
232
+ - the same title, with the old `context` as `note`;
233
+ - the same `local_time`, `timezone` and `fold`;
234
+ - the old window (`expires − due`) as `late_window_seconds`.
235
+
236
+ The key makes this step safe to repeat. A reminder whose time has meanwhile
237
+ passed is refused as not in the future. It is then a *decide* row: pick a new
238
+ time, using `request_key` `legacy:<old id>:r<old revision>:retimed`, or let it
239
+ lapse.
240
+
241
+ ### 5. Verify, then retire the old entries
242
+
243
+ `fleet_reminder_list` shows exactly one `legacy:` reminder per migrated old id.
244
+ The agent then cancels each migrated old reminder with the old `cancel` (`id`,
245
+ `expected_revision`). Afterwards the old `list` shows no `active` reminders and
246
+ no unsettled events.
247
+
248
+ ### 6. Remove the old service
249
+
250
+ Only after step 5 is verified, the operator removes the user unit and the
251
+ worker's config (it is already disabled and stopped). Keep or delete the old
252
+ ledger according to the owner's retention policy; never point Fleet at it.
253
+
254
+ ### Rollback
255
+
256
+ Rollback is a reconciliation, not a restart. While the old unit stays disabled,
257
+ the agent goes through each `legacy:` Fleet reminder:
258
+
259
+ | Fleet reminder state | Action before the old producer may run again |
260
+ | --- | --- |
261
+ | open, no event or event `pending` | First check the old reminder named by the key (`legacy:<old id>:…`). Only if it is still `active` in the old system, `fleet_reminder_cancel` the Fleet copy. If the old one is no longer active, the Fleet copy is the only schedule: keep it |
262
+ | event `dispatched` or `uncertain`, or `acknowledged` | the wake already happened in Fleet: **cancel the old reminder** so it cannot fire again, and keep the Fleet record |
263
+ | `cancelled` or `expired` | nothing; make sure the old reminder is not active either unless that is intended |
264
+
265
+ Only when every `legacy:` reminder has been reconciled may the operator restore
266
+ the unit to its recorded prior state from the recorded unit file, for example
267
+ `systemctl --user enable --now /recorded/FragmentPath/self-reminders-Name.service`
268
+ if it was enabled and active. Never have both producers active for the same
269
+ reminder.
270
+
271
+ ### Instructions to update
272
+
273
+ Remove instructions that tell the agent to run the worker script, and replace
274
+ them with: "Use the `fleet_reminder_*` tools to schedule wakes for yourself. A
275
+ due wake is an internal event: inspect it, decide, then acknowledge it. Message
276
+ anyone only through your normal configured channels."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.64",
3
+ "version": "1.2.0-nightly.66",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -25,7 +25,8 @@
25
25
  "docs/validation/managed-cli-permissions.md",
26
26
  "docs/fleet-migration.md",
27
27
  "LICENSE",
28
- "docs/acp-files.md"
28
+ "docs/acp-files.md",
29
+ "docs/self-reminders.md"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=22.13.0"
@@ -55,7 +56,8 @@
55
56
  "qrcode": "1.5.4",
56
57
  "secure-json-parse": "^4.1.0",
57
58
  "ws": "^8.18.3",
58
- "yaml": "^2.5.0"
59
+ "yaml": "^2.5.0",
60
+ "zod": "^4.4.3"
59
61
  },
60
62
  "optionalDependencies": {
61
63
  "@agentclientprotocol/claude-agent-acp": "^0.63.0",