@ours.network/fleet 1.2.0-nightly.40 → 1.2.0-nightly.42

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,7 +1,7 @@
1
1
  import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
2
2
  import { join, relative } from 'node:path';
3
3
  import { parse } from 'yaml';
4
- import { loadConfig, resolveMonitorConfig, resolvePermissions, ROLE_NAME_RE, } from '../config.js';
4
+ import { loadConfig, resolveSnapshotMonitor, resolvePermissions, ROLE_NAME_RE, } from '../config.js';
5
5
  import { agentsRoot, tmpRoot } from '../paths.js';
6
6
  import { readSpawnLineage } from '../web/topology.js';
7
7
  const MAX_SNAPSHOT_BYTES = 256 * 1024;
@@ -54,7 +54,7 @@ function snapshot(path, name) {
54
54
  session, identity: typeof raw.identity === 'string' ? raw.identity : name,
55
55
  permissions: resolvePermissions(undefined, raw.permissions),
56
56
  permissionsDeclared: raw.permissions !== undefined,
57
- monitor: resolveMonitorConfig(undefined, raw.monitor),
57
+ monitor: resolveSnapshotMonitor(raw.monitor),
58
58
  sourceFile: '(temp)',
59
59
  },
60
60
  };
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.40",
3
- "buildId": "de6076ea1748",
4
- "commit": "147b0e1859d9dc59f46140b0171329e9a479d3cc",
2
+ "version": "1.2.0-nightly.42",
3
+ "buildId": "536c9cc8b8d7",
4
+ "commit": "c51b38eaa547d240e97f776e09286590222e727e",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-03T11:45:41.330Z",
6
+ "builtAt": "2026-10-03T20:14:35.466Z",
7
7
  "capabilities": [
8
8
  "workspace.enroll.preserve-profile-v1",
9
9
  "cowork.http-management-v1",
package/dist/config.d.ts CHANGED
@@ -343,6 +343,11 @@ export declare function resolvePermissions(defaults: unknown, role: Partial<Comm
343
343
  * without tool-boundary steering fall back to non-cancelling delivery.
344
344
  */
345
345
  export declare function defaultMonitorInterrupt(harness: string | undefined): MonitorInterrupt;
346
+ /**
347
+ * Resolve a persisted temporary-role monitor. A snapshot written before
348
+ * monitor.interrupt existed keeps the non-interrupting delivery it ran with.
349
+ */
350
+ export declare function resolveSnapshotMonitor(monitor: unknown): MonitorConfig;
346
351
  export declare function resolveMonitorConfig(defMonitor: unknown, roleMonitor?: Partial<MonitorConfig>, labels?: {
347
352
  base?: string;
348
353
  file?: string;
package/dist/config.js CHANGED
@@ -1150,6 +1150,15 @@ function resolveStartStaggerMs(raw, base) {
1150
1150
  export function defaultMonitorInterrupt(harness) {
1151
1151
  return harness === 'hermes' ? false : 'after_tool';
1152
1152
  }
1153
+ /**
1154
+ * Resolve a persisted temporary-role monitor. A snapshot written before
1155
+ * monitor.interrupt existed keeps the non-interrupting delivery it ran with.
1156
+ */
1157
+ export function resolveSnapshotMonitor(monitor) {
1158
+ if (!isPlainObject(monitor))
1159
+ return resolveMonitorConfig(undefined, monitor);
1160
+ return resolveMonitorConfig(undefined, { interrupt: false, ...monitor });
1161
+ }
1153
1162
  export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
1154
1163
  const where = labels.file && labels.name ? `${labels.file}: role '${labels.name}' ` : '';
1155
1164
  if (defMonitor !== undefined && !isPlainObject(defMonitor))
@@ -45,9 +45,9 @@ export async function requestMemberAddition(input) {
45
45
  }
46
46
  const task = getTask(taskId);
47
47
  const room = task.room_id && getRoomRecord(task.room_id);
48
- if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || task.blocked || room.state !== 'active'
48
+ if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || room.state !== 'active'
49
49
  || room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
50
- throw new MemberAdditionRejected('Adding an agent requires an active, unblocked task room');
50
+ throw new MemberAdditionRejected('Adding an agent requires an active task room');
51
51
  const roleId = `${taskId.slice(0, 8)}-${r.slot}-1`;
52
52
  if (room.member_seats.some(s => s.role_name === roleId))
53
53
  throw new MemberAdditionRejected('This member slot already exists');
@@ -379,9 +379,9 @@ export async function provisionMembers(input) {
379
379
  if (input.append) {
380
380
  const task = input.taskId ? getTask(input.taskId) : undefined;
381
381
  const room = getRoomRecord(input.roomId);
382
- if (!task || !['active', 'review'].includes(task.state) || task.blocked || room?.state !== 'active'
382
+ if (!task || !['active', 'review'].includes(task.state) || room?.state !== 'active'
383
383
  || room.task_id !== task.task_id || room.room_identity_cid !== task.room_identity_cid)
384
- throw new Error('Adding an agent requires the exact active, unblocked task room');
384
+ throw new Error('Adding an agent requires the exact active task room');
385
385
  const names = expandMembers(input.template, shortId(task.task_id)).map(m => m.name);
386
386
  if (room.member_seats.some(seat => names.includes(seat.role_name)))
387
387
  throw new Error('Member slot is already recorded; inspect its existing launch');
package/dist/runner.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { parse } from 'yaml';
10
10
  import { agentDir, stateRoot } from './paths.js';
11
- import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
11
+ import { loadConfig, findRole, isolationContextFor, resolveSnapshotMonitor, resolvePermissions, } from './config.js';
12
12
  import { getAdapter } from './harness/registry.js';
13
13
  import { createMonitor, probeIdentityPresence, } from './monitor.js';
14
14
  import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
@@ -432,12 +432,9 @@ export function loadTempRole(name) {
432
432
  throw new Error(`temp role '${name}' has no snapshot at ${p}`);
433
433
  const role = parse(readFileSync(p, 'utf8'));
434
434
  // Upgrade snapshots written before monitor.mode/interrupt existed. A snapshot
435
- // with no monitor block keeps the historical native/no-supervisor behavior,
436
- // and one without interrupt keeps the non-interrupting delivery it ran with.
435
+ // with no monitor block keeps the historical native/no-supervisor behavior.
437
436
  if (role.monitor)
438
- role.monitor = resolveMonitorConfig(undefined, {
439
- interrupt: false, ...role.monitor,
440
- });
437
+ role.monitor = resolveSnapshotMonitor(role.monitor);
441
438
  role.__temp = true;
442
439
  return role;
443
440
  }
@@ -629,6 +626,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
629
626
  let unsubscribeRecovery;
630
627
  let monitorLoop;
631
628
  let sessionStartupComplete = false;
629
+ let startupRecoveryAllowed = false;
630
+ let successfulTurnObserved = false;
631
+ let monitorMayStart = false;
632
+ let monitorStarted = false;
633
+ const startMonitorAfterStartup = () => {
634
+ if (!monitorStarted && monitorMayStart && sessionStartupComplete && !deps.shouldStop?.()) {
635
+ monitorStarted = true;
636
+ monitorLoop = monitor?.run(pid);
637
+ }
638
+ };
632
639
  let sessionClosed = false;
633
640
  let ownerChannel;
634
641
  let ownerBinder;
@@ -657,13 +664,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
657
664
  pid = agentSession.pid;
658
665
  arbiter = new RoleTurnArbiter(agentSession);
659
666
  sessionHandle = arbiter;
660
- unsubscribeRecovery = agentSession.subscribe(event => {
667
+ const unsubscribeFailure = agentSession.subscribe(event => {
661
668
  if (event.kind !== 'error' || !event.text || event.origin?.kind === 'stall-watchdog')
662
669
  return;
663
670
  const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
664
671
  if (evidence)
665
672
  resolvedMonitorDeps.onFailureEvidence?.(evidence);
666
673
  });
674
+ const unsubscribeStartup = agentSession.subscribeConversation?.(event => {
675
+ if (event.kind !== 'turn.completed' || !('outcome' in event.payload) || event.payload.outcome !== 'completed')
676
+ return;
677
+ successfulTurnObserved = true;
678
+ if (startupRecoveryAllowed) {
679
+ sessionStartupComplete = true;
680
+ startMonitorAfterStartup();
681
+ }
682
+ });
683
+ unsubscribeRecovery = () => { unsubscribeFailure(); unsubscribeStartup?.(); };
667
684
  if (role.owner_channel) {
668
685
  try {
669
686
  ownerBinder = await deps.acquireOwnerBinder(dir, name, role.owner_channel.identity);
@@ -742,10 +759,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
742
759
  // refusal or a cancellation reached the agent and was not acted on, so
743
760
  // the monitor must keep its cursor and try again.
744
761
  submit: async (text, options) => {
745
- // Cancelling the runner-owned startup prompt makes startup look failed
746
- // and closes the session before the wake turn can run. During startup,
747
- // steer into the live turn instead; after it completes, honor the
748
- // configured interrupt policy normally.
762
+ // Delivery starts after the protected startup turn succeeds. Honor the
763
+ // configured interrupt policy for subsequent turns.
749
764
  const policy = options?.interrupt;
750
765
  const interrupt = policy === true && sessionStartupComplete;
751
766
  const promptOptions = {
@@ -753,8 +768,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
753
768
  ...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
754
769
  origin: { kind: 'fleet-monitor' },
755
770
  };
756
- // Startup is already a protected boundary: as with immediate mode,
757
- // steer rather than waiting on/cancelling the runner-owned first turn.
771
+ // after_tool waits for a safe boundary in subsequent active turns.
758
772
  const result = policy === 'after_tool' && sessionStartupComplete
759
773
  ? await arbiter.submitPromptAfterTool(text, promptOptions)
760
774
  : await arbiter.submitPrompt(text, promptOptions);
@@ -790,10 +804,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
790
804
  // startup prompt and then refuses it has not started; logging the role as
791
805
  // up would hide a role that never read its briefing.
792
806
  const starting = arbiter.submitPrompt(firstPrompt, { origin: { kind: 'startup' } });
793
- // Monitoring starts immediately. The delivery adapter above downgrades
794
- // interruption to steering until this startup turn reaches a terminal
795
- // success, so there is neither a deaf gap nor a boot-cancellation loop.
796
- monitorLoop = monitor?.run(pid);
807
+ // Keep the early cursor baseline; do not deliver wakes into the protected
808
+ // first turn. Steering can pre-empt startup just like cancellation.
797
809
  // Owner traffic and agent replies must be observed during a long first
798
810
  // turn. Ordinary owner input steers until startup has proved successful.
799
811
  if (ownerChannel) {
@@ -853,7 +865,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
853
865
  else if (interruptedForWake)
854
866
  deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
855
867
  + 'keeping temporary supervisor alive');
856
- sessionStartupComplete = true;
868
+ startupRecoveryAllowed = interruptedForWake;
869
+ sessionStartupComplete = started.succeeded || (interruptedForWake && successfulTurnObserved);
857
870
  reloadLoopConfig = async () => {
858
871
  const nextRole = findRole(loadConfig(configPath), name);
859
872
  const definitions = nextRole.loops ?? [];
@@ -910,9 +923,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
910
923
  }
911
924
  }
912
925
  deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
913
- // The monitor loop lives exactly as long as the session: it starts once the
914
- // pane pid is known and is stopped when that pid dies (task dies with runner).
915
- monitorLoop ??= monitor?.run(pid);
926
+ // Keep arrivals behind the early prime() cursor; never re-prime at startup.
927
+ // Recoverable cancellation preserves the supervisor, not monitor readiness.
928
+ monitorMayStart = true;
929
+ startMonitorAfterStartup();
916
930
  recoveryController = new RoleRecoveryController({
917
931
  role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
918
932
  log: deps.log,
@@ -1031,6 +1045,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
1031
1045
  }
1032
1046
  }
1033
1047
  }
1048
+ monitorMayStart = false;
1034
1049
  if (loopManager) {
1035
1050
  control?.setLoopManager(undefined);
1036
1051
  await loopManager.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.40",
3
+ "version": "1.2.0-nightly.42",
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",