@ours.network/fleet 1.2.0-nightly.68 → 1.2.0-nightly.69

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.
@@ -1,4 +1,14 @@
1
1
  import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, interruptOutcome, } from './types.js';
2
+ /**
3
+ * Turn sources an urgent reminder may cancel. Owner-channel requests and the
4
+ * owner console are owner commands: cancelling one finalizes the request and
5
+ * its reply route (owner-channel/channel.ts), which is an owner-visible
6
+ * effect. Startup is the readiness gate, stall-watchdog is mid-recovery, and
7
+ * another self-reminder's turn must not be cancelled by a sibling.
8
+ */
9
+ const URGENT_INTERRUPTIBLE = new Set(['fleet-monitor', 'scheduled-loop', 'local-console']);
10
+ /** How long a settled cancellation may take to surface the target's own terminal result. */
11
+ const URGENT_CORRELATION_MS = 2_000;
2
12
  /**
3
13
  * Raised for admissions that were still waiting on a generation which had to be
4
14
  * retired. It carries `ACP_CANCEL_DEADLINE_EXCEEDED` because that is the only
@@ -28,6 +38,10 @@ export class RoleTurnArbiter {
28
38
  capabilities;
29
39
  tail = Promise.resolve();
30
40
  unsettled = 0;
41
+ /** Admissions waiting for the boundary; an urgent interruption yields to them. */
42
+ waiting = 0;
43
+ /** Terminal results of prompts this arbiter admitted and that have not settled yet. */
44
+ live = new Map();
31
45
  stopping = false;
32
46
  retiring = false;
33
47
  /** Bumped by `retireStalledAdmission`; every claim carries the one it was made under. */
@@ -49,11 +63,18 @@ export class RoleTurnArbiter {
49
63
  */
50
64
  exclusive(operation) {
51
65
  const generation = this.generation;
66
+ this.waiting++;
67
+ let counted = true;
68
+ const uncount = () => { if (counted) {
69
+ counted = false;
70
+ this.waiting = Math.max(0, this.waiting - 1);
71
+ } };
52
72
  const run = Promise.race([this.tail, this.retirement.promise]).then(() => {
73
+ uncount();
53
74
  if (generation !== this.generation)
54
75
  throw new AdmissionRetiredError();
55
76
  return operation();
56
- });
77
+ }, error => { uncount(); throw error; });
57
78
  this.tail = run.then(() => undefined, () => undefined);
58
79
  return run;
59
80
  }
@@ -61,10 +82,12 @@ export class RoleTurnArbiter {
61
82
  const generation = this.generation;
62
83
  this.unsettled++;
63
84
  const completion = queued.completion.finally(() => {
85
+ this.live.delete(queued.promptId);
64
86
  // A retired generation's claims were released in one step already.
65
87
  if (generation === this.generation)
66
88
  this.unsettled = Math.max(0, this.unsettled - 1);
67
89
  });
90
+ this.live.set(queued.promptId, completion);
68
91
  return { ...queued, completion };
69
92
  }
70
93
  /**
@@ -87,6 +110,7 @@ export class RoleTurnArbiter {
87
110
  this.retirement = retirementSignal();
88
111
  this.tail = Promise.resolve();
89
112
  this.unsettled = 0;
113
+ this.waiting = 0;
90
114
  retired.retire();
91
115
  }
92
116
  queuePrompt(text, options = {}) {
@@ -101,8 +125,13 @@ export class RoleTurnArbiter {
101
125
  * Other origins retain their configured monitor policy.
102
126
  */
103
127
  monitorAdmissionOptions(options) {
104
- return options.origin?.kind === 'fleet-monitor'
105
- && this.session.snapshot().activePromptSource === 'owner-admin-console'
128
+ if (options.origin?.kind !== 'fleet-monitor')
129
+ return options;
130
+ const active = this.session.snapshot().activePromptSource;
131
+ // A self-reminder turn is protected the same way: a retried wake (say the
132
+ // one an urgent reminder just interrupted) queues behind it instead of
133
+ // cancelling it before the agent could inspect and acknowledge.
134
+ return active === 'owner-admin-console' || active === 'self-reminder'
106
135
  ? { ...options, interrupt: false, steer: false } : options;
107
136
  }
108
137
  async submitPrompt(text, options = {}) {
@@ -187,6 +216,132 @@ export class RoleTurnArbiter {
187
216
  throw error;
188
217
  });
189
218
  }
219
+ /**
220
+ * Admit an URGENT self-reminder wake. Idle: exactly `tryReminder`. Busy: the
221
+ * reminder may ask the exact checked turn to stop — once per event — and is
222
+ * then admitted at the next boundary like any other reminder.
223
+ *
224
+ * The check and the cancel request happen in one synchronous step inside the
225
+ * admission boundary, so the turn asked to stop is the turn that was checked.
226
+ * Only a running, correlatable, interruptible turn with nothing queued behind
227
+ * it and no admission waiting is ever asked to stop; anything else is a
228
+ * reported deferral (`busy`). After the cancellation the boundary is given up
229
+ * before the wake is queued, so an owner prompt (or any producer) that
230
+ * arrived meanwhile is admitted first and the wake simply finds the role
231
+ * busy. A forced, failed or uncertain cancellation never queues the wake.
232
+ */
233
+ async tryUrgentReminder(origin, hooks) {
234
+ await new Promise(resolve => setImmediate(resolve));
235
+ const unavailable = (error) => ({ state: 'unavailable', error, admitted: false });
236
+ const first = await this.exclusive(async () => {
237
+ const snapshot = this.session.snapshot();
238
+ if (this.stopping || this.retiring || !snapshot.alive || snapshot.readiness === 'failed')
239
+ return unavailable(this.stopping ? 'role is stopping' : 'session is unavailable');
240
+ if (snapshot.readiness === 'idle' && this.unsettled === 0)
241
+ return this.queueReminder(hooks, origin);
242
+ const deferral = this.urgentTarget(snapshot);
243
+ if (typeof deferral === 'string' || !hooks.mayInterrupt()) {
244
+ hooks.deferred(typeof deferral === 'string' ? deferral : 'interrupt_already_used');
245
+ return { state: 'busy' };
246
+ }
247
+ if (!hooks.beforeQueue())
248
+ return unavailable('claim declined');
249
+ if (!hooks.markRequested(deferral))
250
+ return unavailable('interrupt request not recorded');
251
+ const terminal = this.live.get(deferral.promptId);
252
+ // No await between the check above and this call: the session cancels
253
+ // the turn that was checked, never a successor.
254
+ let report;
255
+ try {
256
+ const outcome = interruptOutcome(await this.session.interrupt('self-reminder'));
257
+ report = outcome.state === 'forced'
258
+ ? { target: deferral, outcome: 'forced', ...(outcome.reasonCode ? { reason: outcome.reasonCode } : {}) }
259
+ : { target: deferral, outcome: await correlate(terminal) };
260
+ }
261
+ catch (error) {
262
+ report = { target: deferral, outcome: 'failed', reason: controlReason(error) };
263
+ }
264
+ hooks.recordOutcome(report);
265
+ return report;
266
+ }).catch(error => {
267
+ if (error instanceof AdmissionRetiredError)
268
+ return unavailable(error.message);
269
+ throw error;
270
+ });
271
+ if ('state' in first)
272
+ return first;
273
+ if (first.outcome === 'forced' || first.outcome === 'failed')
274
+ return unavailable(`interruption ${first.outcome}; the wake was not queued`);
275
+ // Give up the boundary: anything that queued during the cancellation —
276
+ // owner input first among them — is admitted before the wake.
277
+ await new Promise(resolve => setImmediate(resolve));
278
+ return this.exclusive(async () => {
279
+ const snapshot = this.session.snapshot();
280
+ if (this.stopping || this.retiring || !snapshot.alive || snapshot.readiness === 'failed')
281
+ return unavailable(this.stopping ? 'role is stopping' : 'session is unavailable');
282
+ if (!hooks.stillAdmitting())
283
+ return unavailable('reminder changed during the interruption');
284
+ if (snapshot.readiness !== 'idle' || this.unsettled > 0 || this.waiting > 0)
285
+ return { state: 'busy' };
286
+ return this.queueReminder(hooks, origin, first, false);
287
+ }).catch(error => {
288
+ if (error instanceof AdmissionRetiredError)
289
+ return unavailable(error.message);
290
+ throw error;
291
+ });
292
+ }
293
+ /** The exact interruptible turn, or why there is none. Synchronous by design. */
294
+ urgentTarget(snapshot) {
295
+ if (snapshot.readiness === 'starting')
296
+ return 'session_starting';
297
+ if (snapshot.readiness === 'awaiting_permission' || snapshot.pendingPermissionId)
298
+ return 'permission_pending';
299
+ if (snapshot.readiness !== 'running')
300
+ return 'untracked_turn';
301
+ const source = snapshot.activePromptSource;
302
+ if (!source)
303
+ return 'untracked_turn';
304
+ if (!URGENT_INTERRUPTIBLE.has(source))
305
+ return 'protected_turn';
306
+ if (!this.session.capabilities?.interrupt)
307
+ return 'interrupt_unsupported';
308
+ if (this.waiting > 0 || this.unsettled > 1)
309
+ return 'queued_work';
310
+ if (!this.session.observeConversation)
311
+ return 'uncorrelatable';
312
+ let state;
313
+ try {
314
+ const observation = this.session.observeConversation(() => { });
315
+ state = observation.snapshot;
316
+ observation.unsubscribe();
317
+ }
318
+ catch {
319
+ return 'uncorrelatable';
320
+ }
321
+ if (state.pendingPermissions.length || state.pendingPermissionIds.length)
322
+ return 'permission_pending';
323
+ if (state.pendingPromptCount > 0 || state.queueDepth > 1)
324
+ return 'queued_work';
325
+ if (state.activePromptIds.length < 1)
326
+ return 'untracked_turn';
327
+ return {
328
+ source, generation: state.sessionGeneration, promptId: state.activePromptIds[0],
329
+ ...(snapshot.sessionId ? { sessionId: snapshot.sessionId } : {}),
330
+ };
331
+ }
332
+ async queueReminder(hooks, origin, report, claim = true) {
333
+ if (claim && !hooks.beforeQueue())
334
+ return { state: 'unavailable', error: 'claim declined', admitted: false };
335
+ try {
336
+ const queued = await this.session.queuePrompt(hooks.text(report), {
337
+ interrupt: false, steer: false, origin, releaseGuard: () => hooks.releaseGuard(),
338
+ });
339
+ return { state: 'admitted', queued: this.track(queued) };
340
+ }
341
+ catch (error) {
342
+ return { state: 'unavailable', error: error?.message ?? String(error), admitted: false };
343
+ }
344
+ }
190
345
  /** Retire only at the same admission boundary used by all prompt producers. */
191
346
  retireIfIdle(expired) {
192
347
  return this.exclusive(async () => {
@@ -267,3 +422,25 @@ export class RoleTurnArbiter {
267
422
  exitResult() { return this.session.exitResult(); }
268
423
  close() { return this.session.close(); }
269
424
  }
425
+ /**
426
+ * Whether the target's own terminal result proves this request cancelled it.
427
+ * A settled interrupt says only that the turn (or nothing) ended; without the
428
+ * target's correlated result the honest answer is `ended`.
429
+ */
430
+ async function correlate(terminal) {
431
+ if (!terminal)
432
+ return 'ended';
433
+ let timer;
434
+ const result = await Promise.race([
435
+ terminal,
436
+ new Promise(resolve => { timer = setTimeout(() => resolve(undefined), URGENT_CORRELATION_MS); timer.unref?.(); }),
437
+ ]);
438
+ if (timer)
439
+ clearTimeout(timer);
440
+ return result?.outcome === 'cancelled' && result.cancellationSource === 'self-reminder' ? 'cancelled' : 'ended';
441
+ }
442
+ function controlReason(error) {
443
+ if (error instanceof SessionControlError)
444
+ return error.reasonCode ?? error.kind;
445
+ return 'control_error';
446
+ }
@@ -14,7 +14,9 @@ import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPr
14
14
  */
15
15
  export type SessionReadiness = 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed';
16
16
  export type TurnOutcome = 'completed' | 'refused' | 'cancelled' | 'failed' | 'inconclusive';
17
- export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown' | 'stall-watchdog';
17
+ export type TurnCancellationSource = 'owner' | 'local-console' | 'fleet-monitor' | 'scheduled-loop' | 'shutdown' | 'stall-watchdog'
18
+ /** An urgent self-reminder of the same agent asked its current turn to stop. */
19
+ | 'self-reminder';
18
20
  export type PromptOrigin = {
19
21
  kind: 'startup';
20
22
  } | {
@@ -19,6 +19,7 @@ is nothing to install or enable.
19
19
  at 16:00", "follow up on the review on Monday morning".
20
20
  - Replace polling or sleeping inside a turn. A reminder costs nothing while it
21
21
  waits; a turn that sleeps or a loop that polls costs model time every tick.
22
+ - Wait for a peer without forgetting the task: see [Waiting on a peer](#waiting-on-a-peer).
22
23
  - Not for recurring maintenance: use configured `loops` for that.
23
24
 
24
25
  ## Tools
@@ -29,11 +30,11 @@ reminders, and an id that belongs to another agent is simply not found.
29
30
 
30
31
  | Tool | Arguments | Result |
31
32
  | --- | --- | --- |
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_create` | `request_key`, `title`, `note`?, `local_time`, `time_zone`, `fold`?, `late_window_seconds`?, `interrupt`? | the reminder (`id`, `revision`, `due_at`, `expires_at`, `interrupt`, …) and `created` |
33
34
  | `fleet_reminder_list` | `state`? (`open` default, `closed`, `all`), `limit`? (1–100), `after`? | a page of reminders, `more`, `after`, `uncertain_events` |
34
35
  | `fleet_reminder_update` | `id`, `expected_revision`, any of the schedule fields | the reminder at its new revision |
35
36
  | `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_inspect` | `event_id` | the event (with `interrupt` / `interrupt_deferred` for an urgent one), its reminder (with `note`), `current`, `eligible` |
37
38
  | `fleet_reminder_ack` | `event_id`, `expected_revision` | `acknowledged` (repeat calls return `duplicate: true`) |
38
39
 
39
40
  ### Time
@@ -58,6 +59,10 @@ Daylight-saving changes are explicit:
58
59
  arrive if the agent, Fleet or the host was down, paused or busy at the due time.
59
60
  After the window the reminder expires without a turn.
60
61
 
62
+ `interrupt` (default `false`) marks a reminder as urgent; see
63
+ [Urgent reminders](#urgent-reminders-interrupt-true). Leave it out for ordinary
64
+ reminders.
65
+
61
66
  ### Idempotency and revisions
62
67
 
63
68
  `request_key` identifies one scheduling decision. Repeating `create` with the same
@@ -66,6 +71,11 @@ timeout never creates a second one; the same key with different input is refused
66
71
  (`request_key_conflict`). Keys are remembered while the reminder is retained (the
67
72
  200 most recent closed reminders are kept; at most 100 can be open).
68
73
 
74
+ An urgent request differs from an ordinary one: repeating a call made without
75
+ `interrupt` (or with `interrupt: false`) matches the original, while the same key
76
+ with `interrupt: true` is a `request_key_conflict`. `update` can turn urgency on or
77
+ off; it keeps its current value when not given.
78
+
69
79
  Every `update` and `cancel` names the revision it expects (`stale_revision`
70
80
  otherwise) and bumps it. A wake issued for an older revision is void: if it is
71
81
  still waiting it is dropped before it reaches the model, and if it was already
@@ -109,11 +119,12 @@ current wake becomes void and a new one is issued at the new time.
109
119
  directory). With no open reminders it arms no timer; otherwise it checks at the
110
120
  next due time and at least once a minute. Only a due reminder creates a model
111
121
  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.
122
+ - **When it arrives.** By default, at the first idle turn boundary after the
123
+ due time, and only after the agent's startup turn has succeeded. An ordinary
124
+ wake never interrupts the agent and never waits in a queue behind other work;
125
+ a busy agent receives it when its current work finishes. An urgent reminder may
126
+ instead stop interruptible busy work once (next section). A reminder wake always
127
+ runs as its own turn and is never combined with owner or mail input.
117
128
  - **Offline.** Nothing runs while the host is off or asleep, or while Fleet or the
118
129
  agent is stopped. Reminders are kept, and those still inside their late window
119
130
  are delivered when the agent is back; the rest expire without a turn. Pause and
@@ -136,6 +147,93 @@ current wake becomes void and a new one is issued at the new time.
136
147
  The wake text contains the title, not the note, and the conversation ledger
137
148
  stores only a digest of the wake. Logs carry event ids, never reminder text.
138
149
 
150
+ ## Urgent reminders (`interrupt: true`)
151
+
152
+ An ordinary reminder waits for the agent to be idle, so a long busy turn delays
153
+ it. For a return that is genuinely bound to a time — a deadline, a meeting, a
154
+ window that closes — the agent can create the reminder with `interrupt: true`.
155
+ When it is due and the agent is busy, the supervisor may ask the current turn to
156
+ stop and then start the wake in the same session, instead of waiting.
157
+
158
+ What happens, as separate recorded facts:
159
+
160
+ 1. **Due.** The reminder reaches its time (nothing happens before it).
161
+ 2. **Interruption requested.** Only if the busy turn is interruptible (below).
162
+ The request and its exact target (the turn's source, prompt id and session
163
+ generation; `inspect` shows them as `interrupt.target`) are durably recorded
164
+ *before* the session is asked to stop. Each reminder revision may request
165
+ this **once**. Urgent reminders that may still interrupt are considered
166
+ before ordinary ones and before urgent ones whose interruption is spent.
167
+ 3. **Terminal.** How the stop ended, shown by `inspect` as `interrupt.state`:
168
+ `cancelled` (the target turn's own result confirms this request cancelled it),
169
+ `ended` (the session reported it settled, but cancellation is not confirmed),
170
+ `forced` (the adapter ignored the stop and was restarted), `failed` (the
171
+ control call failed) or `unknown` (Fleet restarted before the outcome).
172
+ 4. **Released.** The wake is handed to the model through the same release fence
173
+ as every reminder. After `forced` or `failed` it is not queued; it is
174
+ delivered later, idle-only, within its late window.
175
+ 5. **Acknowledged**, and any **external action** the agent then takes, are the
176
+ agent's own and remain separate facts.
177
+
178
+ The session and its context are kept: nothing is reset, no second agent and no
179
+ parallel model turn is started. The wake tells the agent what it interrupted so
180
+ it can re-check and resume that work. It sends nothing to anyone and grants no
181
+ authority.
182
+
183
+ An urgent reminder **never** stops, and instead waits like an ordinary one:
184
+
185
+ - an owner-channel request or an owner admin-console turn (owner commands keep
186
+ priority, and cancelling one would end the owner's request and its reply);
187
+ - the startup turn, a stall-recovery turn, or another reminder's wake;
188
+ - a turn with a pending permission decision;
189
+ - work queued behind the current turn, or another admission waiting for the role;
190
+ - a turn Fleet cannot identify exactly (for example one started by adapter-side
191
+ steering, which reports the role idle).
192
+
193
+ Interruptible turns are mail wakes, scheduled loops and local-console prompts.
194
+ When the reminder defers, `inspect` shows why in `event.interrupt_deferred`, and it
195
+ interrupts at a later boundary if one becomes interruptible. A mail wake that was
196
+ stopped is retried by the monitor and queued behind the reminder; it cannot stop
197
+ the reminder in turn.
198
+
199
+ Limits:
200
+
201
+ - Not a hard real-time alarm: the dispatcher checks at least once a minute, and
202
+ stopping a turn and starting a model turn both take time.
203
+ - Nothing happens while the host is off or asleep, or Fleet or the agent is down.
204
+ - One interruption per revision: after it (whatever its outcome) the wake is
205
+ idle-only. A failed or forced stop means the urgency was lost for that event,
206
+ and `inspect` says so; it never reports a successful interruption.
207
+ - Use it sparingly. Ordinary coordination waits stay `interrupt: false`.
208
+
209
+ ## Waiting on a peer
210
+
211
+ In paired work (Developer and Critic, Engineer and Critic) an agent that stops to
212
+ wait for a review, answer, artifact or check schedules **one** idle-only reminder
213
+ about 20 minutes out (or at the agreed time), naming the task or room and the
214
+ exact expectation:
215
+
216
+ ```json
217
+ fleet_reminder_create {
218
+ "request_key": "pr-412-wait-critic-final-1",
219
+ "title": "PR 412: Critic FINAL on head 1a2b3c",
220
+ "note": "Task room for PR 412: waiting for the Critic's FINAL on head 1a2b3c. If it arrived, just ack.",
221
+ "local_time": "2027-01-20T16:20",
222
+ "time_zone": "UTC"
223
+ }
224
+ ```
225
+
226
+ - The answer arrives, the state changes or the work is finished: `cancel` (or
227
+ `update`) the reminder with its current revision.
228
+ - On the wake: `inspect`, re-check the task. Resolved or closed: `ack`, nothing
229
+ else. Still waiting: one concise follow-up in the same room, `ack`, and at most
230
+ one more reminder; if that one finds no progress either, report the
231
+ nonresponse or blocker once through the configured routes and stop reminding.
232
+ - Never a short polling interval, a recurring chain of reminders, repeated
233
+ follow-ups or acknowledgements, or a "done" status because a reminder fired.
234
+ - A deleted or finished task removes the agent's reminders, so a closed task
235
+ produces no wake.
236
+
139
237
  Operators can see an agent's reminders through the existing supervisor tool
140
238
  route, which acts as that agent's supervisor:
141
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.68",
3
+ "version": "1.2.0-nightly.69",
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,4 +25,17 @@ persona: |
25
25
  after a direct room attempt unless the room contract defines another window; honor any later
26
26
  absolute ETA from the peer's timestamp or stated start time. Leave recover,
27
27
  block/unblock, review/finish, deletion, replacement, and respawn decisions to Fleet Coordinator.
28
+
29
+ Waiting on a peer: before you stop to wait for a specific review, answer, artifact or check in
30
+ this room, schedule ONE self-reminder with `fleet_reminder_create` about 20 minutes out (or at
31
+ the time you agreed). Make its `request_key` and `title` name the task or room and what you are
32
+ waiting for, put the exact expectation in its `note`, and leave `interrupt` false. When the
33
+ answer arrives, the state changes or you finish, cancel or reschedule that reminder with its
34
+ current revision. On its wake, inspect it and re-check the task: if the wait is resolved or the
35
+ task is closed, just acknowledge it; if you are still waiting, post one concise follow-up in the
36
+ same room, acknowledge, and set at most one more reminder. If that one also finds no progress,
37
+ report the peer nonresponse or real blocker once through the routes above and stop reminding.
38
+ Never poll on a short interval, chain recurring reminders, repeat follow-ups or acknowledgements,
39
+ or mark work done because a reminder fired. Reserve `interrupt: true` for a return that is
40
+ genuinely bound to a time, never for ordinary coordination waits.
28
41
  bio: Independent reviewer; engage to challenge assumptions and observable evidence and withhold sign-off while material failures remain.
@@ -25,4 +25,17 @@ persona: |
25
25
  after a direct room attempt unless the room contract defines another window; honor any later
26
26
  absolute ETA from the peer's timestamp or stated start time. Leave recover,
27
27
  block/unblock, review/finish, deletion, replacement, and respawn decisions to Fleet Coordinator.
28
+
29
+ Waiting on a peer: before you stop to wait for a specific review, answer, artifact or check in
30
+ this room, schedule ONE self-reminder with `fleet_reminder_create` about 20 minutes out (or at
31
+ the time you agreed). Make its `request_key` and `title` name the task or room and what you are
32
+ waiting for, put the exact expectation in its `note`, and leave `interrupt` false. When the
33
+ answer arrives, the state changes or you finish, cancel or reschedule that reminder with its
34
+ current revision. On its wake, inspect it and re-check the task: if the wait is resolved or the
35
+ task is closed, just acknowledge it; if you are still waiting, post one concise follow-up in the
36
+ same room, acknowledge, and set at most one more reminder. If that one also finds no progress,
37
+ report the peer nonresponse or real blocker once through the routes above and stop reminding.
38
+ Never poll on a short interval, chain recurring reminders, repeat follow-ups or acknowledgements,
39
+ or mark work done because a reminder fired. Reserve `interrupt: true` for a return that is
40
+ genuinely bound to a time, never for ordinary coordination waits.
28
41
  bio: Implementation and task-execution specialist; engage to deliver a bounded change with observable verification evidence.
@@ -37,4 +37,17 @@ persona: |
37
37
  after a direct room attempt unless the room contract defines another window; honor any later
38
38
  absolute ETA from the peer's timestamp or stated start time. Leave recover,
39
39
  block/unblock, review/finish, deletion, replacement, and respawn decisions to Fleet Coordinator.
40
+
41
+ Waiting on a peer: before you stop to wait for a specific review, answer, artifact or check in
42
+ this room, schedule ONE self-reminder with `fleet_reminder_create` about 20 minutes out (or at
43
+ the time you agreed). Make its `request_key` and `title` name the task or room and what you are
44
+ waiting for, put the exact expectation in its `note`, and leave `interrupt` false. When the
45
+ answer arrives, the state changes or you finish, cancel or reschedule that reminder with its
46
+ current revision. On its wake, inspect it and re-check the task: if the wait is resolved or the
47
+ task is closed, just acknowledge it; if you are still waiting, post one concise follow-up in the
48
+ same room, acknowledge, and set at most one more reminder. If that one also finds no progress,
49
+ report the peer nonresponse or real blocker once through the routes above and stop reminding.
50
+ Never poll on a short interval, chain recurring reminders, repeat follow-ups or acknowledgements,
51
+ or mark work done because a reminder fired. Reserve `interrupt: true` for a return that is
52
+ genuinely bound to a time, never for ordinary coordination waits.
40
53
  bio: Gate-pipeline implementation specialist; engage to deliver a bounded change with gate-by-gate verification evidence.