@ours.network/fleet 0.10.0 → 0.10.1

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/monitor.d.ts CHANGED
@@ -142,7 +142,9 @@ export interface MonitorOpts {
142
142
  }
143
143
  /** The lifecycle surface the runner drives: prime pre-launch, run, stop on pid death. */
144
144
  export interface MonitorHandle {
145
- prime(): Promise<void>;
145
+ prime(options?: {
146
+ resetCursor?: boolean;
147
+ }): Promise<void>;
146
148
  run(pid: number): Promise<void>;
147
149
  stop(): void;
148
150
  }
@@ -167,8 +169,14 @@ export declare class Monitor {
167
169
  /** Active degradations, keyed by cause. Empty means armed. */
168
170
  private readonly causes;
169
171
  constructor(o: MonitorOpts);
170
- /** Resume the last delivered cursor; only a brand-new monitor primes at stream tip. */
171
- prime(): Promise<void>;
172
+ /**
173
+ * Resume the last delivered cursor during ordinary fleet-owned restarts.
174
+ * A native→fleet ownership transition resets at stream tip because the native
175
+ * owner was responsible for arrivals while the supervisor was inactive.
176
+ */
177
+ prime(options?: {
178
+ resetCursor?: boolean;
179
+ }): Promise<void>;
172
180
  /** Long-poll → filter → coalesce → inject, until the pane pid dies or stop(). */
173
181
  run(pid: number): Promise<void>;
174
182
  stop(): void;
package/dist/monitor.js CHANGED
@@ -336,9 +336,13 @@ export class Monitor {
336
336
  const n = o.cfg.turn_fail_threshold;
337
337
  this.turnFailThreshold = typeof n === 'number' && n >= 1 ? n : DEFAULT_TURN_FAIL_THRESHOLD;
338
338
  }
339
- /** Resume the last delivered cursor; only a brand-new monitor primes at stream tip. */
340
- async prime() {
341
- const persisted = this.readPersistedCursor();
339
+ /**
340
+ * Resume the last delivered cursor during ordinary fleet-owned restarts.
341
+ * A native→fleet ownership transition resets at stream tip because the native
342
+ * owner was responsible for arrivals while the supervisor was inactive.
343
+ */
344
+ async prime(options = {}) {
345
+ const persisted = options.resetCursor ? null : this.readPersistedCursor();
342
346
  if (persisted !== null) {
343
347
  this.cursor = persisted;
344
348
  this.deliveredCursor = persisted;
package/dist/runner.d.ts CHANGED
@@ -19,6 +19,13 @@ export interface RunnerDeps {
19
19
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
20
20
  shouldStop?(): boolean;
21
21
  }
22
+ /**
23
+ * Record who owns wake delivery for this run. Returning true means a fleet
24
+ * monitor is taking ownership back from a native harness and must start at the
25
+ * current stream tip rather than replay notifications the native owner was
26
+ * responsible for.
27
+ */
28
+ export declare function recordMonitorOwner(dir: string, owner: 'fleet' | 'native'): boolean;
22
29
  /**
23
30
  * Compose the tmux pane shell command: env prefix + argv + exit-status capture.
24
31
  * `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
package/dist/runner.js CHANGED
@@ -32,6 +32,24 @@ const defaultDeps = () => ({
32
32
  fetch: (url, init) => globalThis.fetch(url, init),
33
33
  createMonitor: opts => createMonitor(opts),
34
34
  });
35
+ const MONITOR_OWNER_FILE = '.monitor-owner';
36
+ /**
37
+ * Record who owns wake delivery for this run. Returning true means a fleet
38
+ * monitor is taking ownership back from a native harness and must start at the
39
+ * current stream tip rather than replay notifications the native owner was
40
+ * responsible for.
41
+ */
42
+ export function recordMonitorOwner(dir, owner) {
43
+ let previous = null;
44
+ try {
45
+ const path = join(dir, MONITOR_OWNER_FILE);
46
+ if (existsSync(path))
47
+ previous = readFileSync(path, 'utf8').trim();
48
+ writeFileSync(path, `${owner}\n`);
49
+ }
50
+ catch { /* ownership diagnostics must never take the role down */ }
51
+ return owner === 'fleet' && previous === 'native';
52
+ }
35
53
  /**
36
54
  * Compose the tmux pane shell command: env prefix + argv + exit-status capture.
37
55
  * `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
@@ -334,18 +352,21 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
334
352
  // leave wake ownership to the harness. Temp snapshots predating `monitor:` are
335
353
  // treated as native (monitor may be undefined on an old role.yaml).
336
354
  const resolvedMonitorDeps = monitorDeps(deps, role.env);
337
- const monitor = role.monitor?.mode === 'fleet' ? deps.createMonitor({
355
+ const monitorOwner = role.monitor?.mode === 'fleet' ? 'fleet' : 'native';
356
+ const resetMonitorCursor = recordMonitorOwner(dir, monitorOwner);
357
+ const monitor = monitorOwner === 'fleet' ? deps.createMonitor({
338
358
  name, identity: role.identity, agentDir: dir, cfg: role.monitor,
339
359
  deps: resolvedMonitorDeps,
340
360
  }) : null;
341
361
  if (monitor)
342
- await monitor.prime();
362
+ await monitor.prime({ resetCursor: resetMonitorCursor });
343
363
  rmSync(exitFile, { force: true });
344
364
  let pid;
345
365
  let sessionHandle;
346
366
  let acpSession;
347
367
  let control;
348
368
  let monitorLoop;
369
+ let acpStartupComplete = false;
349
370
  if (sessionBackend === 'acp') {
350
371
  const perms = role.permissions ?? resolvePermissions(undefined, undefined);
351
372
  // Say once, at startup, that this role will decide permission requests by
@@ -373,7 +394,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
373
394
  // refusal or a cancellation reached the agent and was not acted on, so
374
395
  // the monitor must keep its cursor and try again.
375
396
  submit: async (text, options) => {
376
- const result = await acpSession.submitPrompt(text, { ...options, steer: true });
397
+ // Cancelling the runner-owned startup prompt makes startup look failed
398
+ // and closes the session before the wake turn can run. During startup,
399
+ // steer into the live turn instead; after it completes, honor the
400
+ // configured interrupt policy normally.
401
+ const interrupt = options?.interrupt === true && acpStartupComplete;
402
+ const result = await acpSession.submitPrompt(text, { ...options, interrupt, steer: true });
377
403
  const steered = result.accepted
378
404
  && (result.detail === 'injected' || result.detail === 'startedNewTurn');
379
405
  return {
@@ -390,8 +416,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
390
416
  // startup prompt and then refuses it has not started; logging the role as
391
417
  // up would hide a role that never read its briefing.
392
418
  const starting = acpSession.submitPrompt(firstPrompt);
393
- // Start monitoring as soon as the initial prompt has been submitted. ACP
394
- // steering can deliver a wake into that turn without a boot-time deaf gap.
419
+ // Monitoring starts immediately. The delivery adapter above downgrades
420
+ // interruption to steering until this startup turn reaches a terminal
421
+ // success, so there is neither a deaf gap nor a boot-cancellation loop.
395
422
  monitorLoop = monitor?.run(pid);
396
423
  const started = await starting;
397
424
  if (!started.succeeded) {
@@ -401,6 +428,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
401
428
  throw new Error(`[${name}] ACP startup prompt ${started.outcome}` +
402
429
  `${started.detail ? `: ${started.detail}` : ''}`);
403
430
  }
431
+ acpStartupComplete = true;
404
432
  }
405
433
  else {
406
434
  await deps.tmux.kill(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",