@ours.network/fleet 1.2.0-nightly.41 → 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,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.41",
3
- "buildId": "aa7ed657a14a",
4
- "commit": "bbdc34de495026f1119ce9df3442a787d8d02dc0",
2
+ "version": "1.2.0-nightly.42",
3
+ "buildId": "536c9cc8b8d7",
4
+ "commit": "c51b38eaa547d240e97f776e09286590222e727e",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-03T12:50:11.407Z",
6
+ "builtAt": "2026-10-03T20:14:35.466Z",
7
7
  "capabilities": [
8
8
  "workspace.enroll.preserve-profile-v1",
9
9
  "cowork.http-management-v1",
@@ -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
@@ -626,6 +626,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
626
626
  let unsubscribeRecovery;
627
627
  let monitorLoop;
628
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
+ };
629
639
  let sessionClosed = false;
630
640
  let ownerChannel;
631
641
  let ownerBinder;
@@ -654,13 +664,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
654
664
  pid = agentSession.pid;
655
665
  arbiter = new RoleTurnArbiter(agentSession);
656
666
  sessionHandle = arbiter;
657
- unsubscribeRecovery = agentSession.subscribe(event => {
667
+ const unsubscribeFailure = agentSession.subscribe(event => {
658
668
  if (event.kind !== 'error' || !event.text || event.origin?.kind === 'stall-watchdog')
659
669
  return;
660
670
  const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
661
671
  if (evidence)
662
672
  resolvedMonitorDeps.onFailureEvidence?.(evidence);
663
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?.(); };
664
684
  if (role.owner_channel) {
665
685
  try {
666
686
  ownerBinder = await deps.acquireOwnerBinder(dir, name, role.owner_channel.identity);
@@ -739,10 +759,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
739
759
  // refusal or a cancellation reached the agent and was not acted on, so
740
760
  // the monitor must keep its cursor and try again.
741
761
  submit: async (text, options) => {
742
- // Cancelling the runner-owned startup prompt makes startup look failed
743
- // and closes the session before the wake turn can run. During startup,
744
- // steer into the live turn instead; after it completes, honor the
745
- // configured interrupt policy normally.
762
+ // Delivery starts after the protected startup turn succeeds. Honor the
763
+ // configured interrupt policy for subsequent turns.
746
764
  const policy = options?.interrupt;
747
765
  const interrupt = policy === true && sessionStartupComplete;
748
766
  const promptOptions = {
@@ -750,8 +768,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
750
768
  ...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
751
769
  origin: { kind: 'fleet-monitor' },
752
770
  };
753
- // Startup is already a protected boundary: as with immediate mode,
754
- // steer rather than waiting on/cancelling the runner-owned first turn.
771
+ // after_tool waits for a safe boundary in subsequent active turns.
755
772
  const result = policy === 'after_tool' && sessionStartupComplete
756
773
  ? await arbiter.submitPromptAfterTool(text, promptOptions)
757
774
  : await arbiter.submitPrompt(text, promptOptions);
@@ -787,10 +804,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
787
804
  // startup prompt and then refuses it has not started; logging the role as
788
805
  // up would hide a role that never read its briefing.
789
806
  const starting = arbiter.submitPrompt(firstPrompt, { origin: { kind: 'startup' } });
790
- // Monitoring starts immediately. The delivery adapter above downgrades
791
- // interruption to steering until this startup turn reaches a terminal
792
- // success, so there is neither a deaf gap nor a boot-cancellation loop.
793
- 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.
794
809
  // Owner traffic and agent replies must be observed during a long first
795
810
  // turn. Ordinary owner input steers until startup has proved successful.
796
811
  if (ownerChannel) {
@@ -850,7 +865,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
850
865
  else if (interruptedForWake)
851
866
  deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
852
867
  + 'keeping temporary supervisor alive');
853
- sessionStartupComplete = true;
868
+ startupRecoveryAllowed = interruptedForWake;
869
+ sessionStartupComplete = started.succeeded || (interruptedForWake && successfulTurnObserved);
854
870
  reloadLoopConfig = async () => {
855
871
  const nextRole = findRole(loadConfig(configPath), name);
856
872
  const definitions = nextRole.loops ?? [];
@@ -907,9 +923,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
907
923
  }
908
924
  }
909
925
  deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
910
- // The monitor loop lives exactly as long as the session: it starts once the
911
- // pane pid is known and is stopped when that pid dies (task dies with runner).
912
- 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();
913
930
  recoveryController = new RoleRecoveryController({
914
931
  role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
915
932
  log: deps.log,
@@ -1028,6 +1045,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
1028
1045
  }
1029
1046
  }
1030
1047
  }
1048
+ monitorMayStart = false;
1031
1049
  if (loopManager) {
1032
1050
  control?.setLoopManager(undefined);
1033
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.41",
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",