@ours.network/fleet 1.2.0-nightly.67 → 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.
- package/dist/briefing.js +3 -1
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +28 -6
- package/dist/preset-migration.js +9 -0
- package/dist/reminders/manager.d.ts +11 -4
- package/dist/reminders/manager.js +71 -5
- package/dist/reminders/store.d.ts +100 -0
- package/dist/reminders/store.js +132 -5
- package/dist/reminders/tools.js +7 -1
- package/dist/session/arbiter.d.ts +60 -0
- package/dist/session/arbiter.js +180 -3
- package/dist/session/types.d.ts +3 -1
- package/dist/workspace-enrollment.d.ts +3 -2
- package/dist/workspace-enrollment.js +19 -5
- package/docs/self-reminders.md +105 -7
- package/package.json +1 -1
- package/presets/fleet/roles/Critic.yaml +13 -0
- package/presets/fleet/roles/Developer.yaml +13 -0
- package/presets/fleet/roles/Engineer.yaml +13 -0
package/dist/session/arbiter.js
CHANGED
|
@@ -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
|
-
|
|
105
|
-
|
|
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
|
+
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -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
|
} | {
|
|
@@ -4,8 +4,9 @@ export interface WorkspacePayload {
|
|
|
4
4
|
appOrigin: string;
|
|
5
5
|
hostname: string;
|
|
6
6
|
rootName: string;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
fullName?: string | null;
|
|
8
|
+
name?: string | null;
|
|
9
|
+
surname?: string | null;
|
|
9
10
|
connectorToken?: string;
|
|
10
11
|
invitation: string;
|
|
11
12
|
serverCid: string;
|
|
@@ -25,14 +25,24 @@ export function decodeWorkspacePayload(input, options = {}) {
|
|
|
25
25
|
catch {
|
|
26
26
|
throw Error('Invalid workspace payload');
|
|
27
27
|
}
|
|
28
|
-
if (p.version !== 1 || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(p.appOrigin) || !/^[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(p.hostname) || !/^[a-z0-9-]{2,30}@[a-z0-9-]{2,30}$/.test(p.rootName) || !/^[a-f0-9]{64}$/i.test(p.serverCid) || !p.challenge || !['nonce', 'accountId', 'workspaceId'].every(k => /^[\w-]{43}$/.test(String(p.challenge[k]))) || (!options.allowExpired && p.challenge.expiresAt <= Date.now()) || !Number.isSafeInteger(p.challenge.expiresAt) || p.challenge.expiresAt > Date.now() + 16 * 60000)
|
|
28
|
+
if (!p || typeof p !== 'object' || Array.isArray(p) || p.version !== 1 || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(p.appOrigin) || !/^[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(p.hostname) || !/^[a-z0-9-]{2,30}@[a-z0-9-]{2,30}$/.test(p.rootName) || !/^[a-f0-9]{64}$/i.test(p.serverCid) || !p.challenge || !['nonce', 'accountId', 'workspaceId'].every(k => /^[\w-]{43}$/.test(String(p.challenge[k]))) || (!options.allowExpired && p.challenge.expiresAt <= Date.now()) || !Number.isSafeInteger(p.challenge.expiresAt) || p.challenge.expiresAt > Date.now() + 16 * 60000)
|
|
29
29
|
throw Error('Workspace payload is invalid or expired');
|
|
30
|
-
for (const k of ['connectorToken', 'invitation'
|
|
30
|
+
for (const k of ['connectorToken', 'invitation']) {
|
|
31
31
|
if (k === 'connectorToken' && p[k] === undefined)
|
|
32
32
|
continue;
|
|
33
33
|
if (typeof p[k] !== 'string' || !p[k] || p[k].length > 8192 || /[\x00-\x1f\x7f]/.test(p[k]))
|
|
34
34
|
throw Error('Invalid workspace payload');
|
|
35
35
|
}
|
|
36
|
+
for (const k of ['fullName', 'name', 'surname']) {
|
|
37
|
+
const value = p[k];
|
|
38
|
+
if (value == null || value === '') {
|
|
39
|
+
p[k] = null;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== 'string' || value.length > 8192 || /[\x00-\x1f\x7f]/.test(value))
|
|
43
|
+
throw Error('Invalid workspace profile');
|
|
44
|
+
p[k] = value.trim() || null;
|
|
45
|
+
}
|
|
36
46
|
return p;
|
|
37
47
|
}
|
|
38
48
|
/** Credential-free renewal is allowed only on the host already holding this private connector. */
|
|
@@ -120,11 +130,11 @@ const CID = /^[a-f0-9]{64}$/i, same = (a, b) => a.toLowerCase() === b.toLowerCas
|
|
|
120
130
|
async function messengerIdentity(request, attach) {
|
|
121
131
|
const bound = await request('workspace/enrollment-identity');
|
|
122
132
|
if (bound.rootCid === undefined)
|
|
123
|
-
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true };
|
|
133
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, optionalWorkspaceProfileV1: bound.optionalWorkspaceProfileV1 === true };
|
|
124
134
|
if (typeof bound.cid !== 'string' || !CID.test(bound.cid) || typeof bound.rootCid !== 'string' || !CID.test(bound.rootCid) || same(bound.cid, bound.rootCid))
|
|
125
135
|
throw Error('Bound Messenger identity unavailable');
|
|
126
136
|
const root = await humanRoot(attach, bound.rootCid, bound.cid);
|
|
127
|
-
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, root };
|
|
137
|
+
return { cid: bound.cid, preserveProfile: bound.preserveProfile === true, optionalWorkspaceProfileV1: bound.optionalWorkspaceProfileV1 === true, root };
|
|
128
138
|
}
|
|
129
139
|
/** This host's only Human root, as its daemon lists it; optionally with a permanent identity that must be listed under it. */
|
|
130
140
|
async function humanRoot(attach, rootCid, under) {
|
|
@@ -280,6 +290,8 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
280
290
|
&& (options.replacingWorkspace !== undefined || JSON.parse(readFileSync(previousFile, 'utf8')).serverCid?.toUpperCase() === payload.serverCid.toUpperCase());
|
|
281
291
|
if (options.preserveProfile && identity.preserveProfile !== true)
|
|
282
292
|
throw Error('Existing-host setup requires Messenger profile-preservation support; update the host Messenger before retrying');
|
|
293
|
+
if (!options.preserveProfile && (payload.fullName?.trim() || !payload.name?.trim() || !payload.surname?.trim()) && !identity.optionalWorkspaceProfileV1)
|
|
294
|
+
throw Error('Optional workspace profile requires updated Messenger; update the host Messenger before retrying (the host is not enrolled)');
|
|
283
295
|
if (loaded.rooms && !lost && !moved && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
284
296
|
throw Error('Retained room owner CID conflicts with the Human root');
|
|
285
297
|
const inviteFile = loaded.rooms?.owner.public_invite_file || join(dir, 'owner.invite');
|
|
@@ -291,7 +303,7 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
291
303
|
const devices = new WorkspaceDeviceStore();
|
|
292
304
|
const hostWorkspaceId = devices.workspaceId;
|
|
293
305
|
devices.close();
|
|
294
|
-
const { name, surname, ...proofPayload } = payload;
|
|
306
|
+
const { fullName, name, surname, ...proofPayload } = payload;
|
|
295
307
|
let result, ownerCid;
|
|
296
308
|
// A replacement can stop after its one-time proof but before local files commit. Read the
|
|
297
309
|
// account's existing signed receipt first; a verified proof must not be sent a second time.
|
|
@@ -316,6 +328,8 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
316
328
|
}
|
|
317
329
|
}
|
|
318
330
|
if (identity.root) {
|
|
331
|
+
if (!options.preserveProfile)
|
|
332
|
+
await request('identity/profile', { fullName: fullName ?? null, name: name ?? null, surname: surname ?? null });
|
|
319
333
|
// The Human root signs the binding; the person's own Messenger identity is the room Owner and issues the Owner invitation.
|
|
320
334
|
if (!proofAlreadyConfirmed)
|
|
321
335
|
await proveWithHumanRoot(attach, identity.root, payload, hostWorkspaceId, options.contactWaitMs ?? 10000, options.replacingWorkspace !== undefined);
|
package/docs/self-reminders.md
CHANGED
|
@@ -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.**
|
|
113
|
-
only after the agent's startup turn has succeeded.
|
|
114
|
-
agent and never waits in a queue behind other work;
|
|
115
|
-
when its current work finishes.
|
|
116
|
-
|
|
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.
|
|
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.
|