@ours.network/fleet 0.17.6 → 0.17.7

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": "0.17.6",
3
- "buildId": "a2f7650e3a39",
4
- "commit": "96df9112202bb3957d18d95bc4210c1cf49d867f",
2
+ "version": "0.17.7",
3
+ "buildId": "8afdf08c9c69",
4
+ "commit": "1a6243fe213281c0b82f5bfeaf97dfc4c697fcf7",
5
5
  "dirty": false,
6
- "builtAt": "2026-08-17T11:51:49.146Z",
6
+ "builtAt": "2026-08-17T12:01:37.858Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -648,7 +648,10 @@ function renderLoopRows(role, state, loop) {
648
648
  return Object.entries(state.loops).filter(([name]) => !loop || name === loop).map(([name, item]) => `${role}/${name} ${item.enabled && !item.operatorDisabled ? 'enabled' : 'disabled'} `
649
649
  + `${item.activeRunId ? 'running' : 'idle'} next=${item.nextDueAt} last=${item.lastOutcome ?? 'never'} `
650
650
  + `counts=${item.counts.started}/${item.counts.completed}/${item.counts.failed} `
651
- + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`);
651
+ + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`
652
+ + (item.missedGap
653
+ ? ` gap=${item.missedGap.count}@${item.missedGap.fromAt}..${item.missedGap.throughAt}`
654
+ : ''));
652
655
  }
653
656
  cOpt(loopsCommand.command('status [role] [loop]').description('show live or stored loop state'))
654
657
  .option('--json', 'emit stable JSON')
@@ -68,8 +68,32 @@ export declare class ScheduledLoopManager implements ScheduledLoopManagerHandle
68
68
  private armAbandon;
69
69
  private finish;
70
70
  private advance;
71
+ /**
72
+ * Coalesce a backlog into one skip. The counters alone say how many
73
+ * occurrences were lost but never when or for how long, so the window is
74
+ * recorded too and carried on the state until a run is actually told about it
75
+ * — a dropped pass has to stay visible to the next one, not just to whoever
76
+ * was reading the log at the time.
77
+ */
71
78
  private skipMissed;
72
- private skipRestartMisses;
79
+ /**
80
+ * Restart is not, by itself, a reason to lose an occurrence a running manager
81
+ * would still have run. `poll` tolerates lateness up to one full interval and
82
+ * runs the tick late; this path used to drop anything already due however
83
+ * recently, so a role restarted seconds after its own tick came due lost it
84
+ * outright. For an oversight role that is precisely the pass which would have
85
+ * recorded why it restarted, so the failure erased its own witness.
86
+ *
87
+ * The tolerance is the only thing shared with `poll`. A backlog at least one
88
+ * interval deep is still coalesced into a single skip and never replayed —
89
+ * after a long outage exactly one occurrence survives, and `schedule` then
90
+ * arms it through the ordinary path rather than firing a burst here.
91
+ *
92
+ * Running the survivor late cannot outpace the configured cadence: `advance`
93
+ * moves the cursor by exactly one `intervalMs` per occurrence from the nominal
94
+ * time, so a loop that keeps restarting still runs at most once per interval.
95
+ */
96
+ private skipRestartBacklog;
73
97
  /**
74
98
  * A run the store could not record is dropped, not retried: the cursor has
75
99
  * already moved, so this can never become a busy loop, and the outage is
@@ -86,5 +110,10 @@ export declare class ScheduledLoopManager implements ScheduledLoopManagerHandle
86
110
  * until the process was restarted.
87
111
  */
88
112
  private recover;
113
+ /**
114
+ * The envelope is the only channel a scheduled pass has for learning about
115
+ * the passes that did not happen. A gap stated here is what lets an oversight
116
+ * role report its own outage instead of resuming as if nothing was missed.
117
+ */
89
118
  private envelope;
90
119
  }
@@ -39,7 +39,7 @@ export class ScheduledLoopManager {
39
39
  }
40
40
  start() {
41
41
  if (!this.store.fresh)
42
- this.skipRestartMisses();
42
+ this.skipRestartBacklog();
43
43
  this.schedule();
44
44
  }
45
45
  async stop() {
@@ -143,10 +143,15 @@ export class ScheduledLoopManager {
143
143
  async attempt(definition, state, scheduledAt) {
144
144
  const runId = `sl_${randomUUID()}`;
145
145
  const origin = { kind: 'scheduled-loop', loop: definition.name, runId };
146
- const prompt = this.envelope(definition, runId, scheduledAt);
146
+ // The gap is read here and cleared only if the turn is actually admitted:
147
+ // an attempt that ends `skipped_busy` or `unavailable` reported it to
148
+ // nobody, so it has to still be there for the attempt that succeeds.
149
+ const gap = state.missedGap;
150
+ const prompt = this.envelope(definition, runId, scheduledAt, gap);
147
151
  let claimed = false;
148
152
  const result = await this.arbiter.tryScheduled(prompt, origin, () => {
149
153
  claimed = true;
154
+ state.missedGap = null;
150
155
  state.activeRunId = runId;
151
156
  state.lastRunId = runId;
152
157
  state.lastStartedAt = new Date(this.deps.now()).toISOString();
@@ -265,7 +270,15 @@ export class ScheduledLoopManager {
265
270
  state.nextScheduledAt = new Date(next).toISOString();
266
271
  state.nextDueAt = new Date(next + deterministicJitter(this.role, definition.name, next, definition.jitterMs)).toISOString();
267
272
  }
273
+ /**
274
+ * Coalesce a backlog into one skip. The counters alone say how many
275
+ * occurrences were lost but never when or for how long, so the window is
276
+ * recorded too and carried on the state until a run is actually told about it
277
+ * — a dropped pass has to stay visible to the next one, not just to whoever
278
+ * was reading the log at the time.
279
+ */
268
280
  skipMissed(definition, state, now) {
281
+ const from = state.nextScheduledAt;
269
282
  let missed = 0;
270
283
  while (Date.parse(state.nextDueAt) <= now) {
271
284
  this.advance(definition, state);
@@ -275,14 +288,43 @@ export class ScheduledLoopManager {
275
288
  state.counts.skippedMissed = increment(state.counts.skippedMissed, missed);
276
289
  state.lastOutcome = 'skipped_missed';
277
290
  state.lastFinishedAt = new Date(now).toISOString();
291
+ // Successive outages before any run lands merge into one gap: the earliest
292
+ // start wins, so the window always spans the whole silence.
293
+ const previous = state.missedGap;
294
+ state.missedGap = {
295
+ count: increment(previous?.count ?? 0, missed),
296
+ fromAt: previous?.fromAt ?? from,
297
+ throughAt: state.lastScheduledAt ?? from,
298
+ detectedAt: new Date(now).toISOString(),
299
+ };
278
300
  this.store.persist();
279
- this.deps.log(`[${this.role}] loop ${definition.name} skipped_missed count=${missed}`);
301
+ this.deps.log(`[${this.role}] loop ${definition.name} skipped_missed count=${missed} `
302
+ + `gap=${from}..${state.missedGap.throughAt} `
303
+ + `unreported=${state.missedGap.count}`);
280
304
  }
281
- skipRestartMisses() {
305
+ /**
306
+ * Restart is not, by itself, a reason to lose an occurrence a running manager
307
+ * would still have run. `poll` tolerates lateness up to one full interval and
308
+ * runs the tick late; this path used to drop anything already due however
309
+ * recently, so a role restarted seconds after its own tick came due lost it
310
+ * outright. For an oversight role that is precisely the pass which would have
311
+ * recorded why it restarted, so the failure erased its own witness.
312
+ *
313
+ * The tolerance is the only thing shared with `poll`. A backlog at least one
314
+ * interval deep is still coalesced into a single skip and never replayed —
315
+ * after a long outage exactly one occurrence survives, and `schedule` then
316
+ * arms it through the ordinary path rather than firing a burst here.
317
+ *
318
+ * Running the survivor late cannot outpace the configured cadence: `advance`
319
+ * moves the cursor by exactly one `intervalMs` per occurrence from the nominal
320
+ * time, so a loop that keeps restarting still runs at most once per interval.
321
+ */
322
+ skipRestartBacklog() {
282
323
  const now = this.deps.now();
283
324
  for (const definition of this.definitions.values()) {
284
325
  const state = this.store.state.loops[definition.name];
285
- if (definition.enabled && !state.operatorDisabled && Date.parse(state.nextDueAt) <= now)
326
+ if (definition.enabled && !state.operatorDisabled
327
+ && now >= Date.parse(state.nextDueAt) + definition.intervalMs)
286
328
  this.skipMissed(definition, state, now);
287
329
  }
288
330
  }
@@ -340,17 +382,34 @@ export class ScheduledLoopManager {
340
382
  this.deps.clearTimer(this.timer);
341
383
  this.arm(backoffMs(this.pollFailures));
342
384
  }
343
- envelope(definition, runId, scheduledAt) {
385
+ /**
386
+ * The envelope is the only channel a scheduled pass has for learning about
387
+ * the passes that did not happen. A gap stated here is what lets an oversight
388
+ * role report its own outage instead of resuming as if nothing was missed.
389
+ */
390
+ envelope(definition, runId, scheduledAt, gap) {
391
+ const lateBy = Math.max(0, this.deps.now() - scheduledAt);
344
392
  return [
345
393
  '[fleet-loop]',
346
394
  `loop: ${definition.name}`,
347
395
  `run: ${runId}`,
348
396
  `scheduled_at: ${new Date(scheduledAt).toISOString()}`,
397
+ ...(lateBy > 0 ? [`started_late_by_ms: ${lateBy}`] : []),
398
+ ...(gap ? [
399
+ `missed_occurrences: ${gap.count}`,
400
+ `missed_window: ${gap.fromAt}..${gap.throughAt}`,
401
+ `missed_gap_ms: ${Math.max(0, Date.parse(gap.detectedAt) - Date.parse(gap.fromAt))}`,
402
+ ] : []),
349
403
  'origin: local-trusted-config',
350
404
  '',
351
405
  'This is a scheduled internal maintenance turn, not an owner message and not ordinary ours mail.',
352
406
  'Perform one bounded pass. Do not wait for the next tick. Do not report to an owner unless your',
353
407
  'configured policy and an existing authenticated proactive-report route authorize a material report.',
408
+ ...(gap ? ['',
409
+ 'This loop did not run for the window above: those occurrences were coalesced away while the role',
410
+ 'was unavailable, and this pass is the first since. Treat the gap as part of what you are reporting',
411
+ 'on — it is the record of your own outage, and no later pass will be told about it.',
412
+ ] : []),
354
413
  '',
355
414
  definition.prompt,
356
415
  ].join('\n');
@@ -8,11 +8,29 @@ export interface LoopCounts {
8
8
  skippedBusy: number;
9
9
  skippedMissed: number;
10
10
  }
11
+ /**
12
+ * A coalesced run of occurrences that were never submitted, held until a run
13
+ * actually starts and can be told about it. Without it a dropped occurrence
14
+ * survives only as a counter, which says how many were lost but never when or
15
+ * for how long — and an oversight role cannot report an outage it cannot date.
16
+ */
17
+ export interface LoopMissedGap {
18
+ /** Occurrences coalesced away, summed across every skip since the last run. */
19
+ count: number;
20
+ /** Nominal time of the earliest occurrence in the gap. */
21
+ fromAt: string;
22
+ /** Nominal time of the latest occurrence in the gap. */
23
+ throughAt: string;
24
+ /** When the manager noticed — the end of the outage, not of the last skip. */
25
+ detectedAt: string;
26
+ }
11
27
  export interface LoopRuntimeState {
12
28
  definitionHash: string;
13
29
  promptHash: string;
14
30
  enabled: boolean;
15
31
  operatorDisabled: boolean;
32
+ /** Unreported gap, cleared by the first run that carries it. */
33
+ missedGap: LoopMissedGap | null;
16
34
  nextScheduledAt: string;
17
35
  nextDueAt: string;
18
36
  lastScheduledAt: string | null;
@@ -100,6 +100,9 @@ export class ScheduledLoopStateStore {
100
100
  if (old?.definitionHash === definition.definitionHash) {
101
101
  next[definition.name] = {
102
102
  ...old, promptHash: definition.promptHash, enabled: definition.enabled,
103
+ // A file written before this field existed restores as undefined; an
104
+ // unreported gap is absent, not lost, so normalize rather than trust.
105
+ missedGap: old.missedGap ?? null,
103
106
  };
104
107
  }
105
108
  else {
@@ -119,6 +122,7 @@ export class ScheduledLoopStateStore {
119
122
  activeRunId: old?.activeRunId ?? null,
120
123
  counts: old?.counts ?? zeroCounts(), lastError: old?.lastError ?? null,
121
124
  operatorDisabled: old?.operatorDisabled ?? false,
125
+ missedGap: old?.missedGap ?? null,
122
126
  };
123
127
  }
124
128
  if (recoverActive && next[definition.name].activeRunId) {
@@ -5,6 +5,21 @@ import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCom
5
5
  import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, RuntimeSelectorMetadata, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
6
6
  /** Bound safe-boundary waiting without turning a hung tool into cancellation. */
7
7
  export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000;
8
+ /**
9
+ * How long a steering-started turn is presumed to still own the adapter after
10
+ * its last update. Such a turn has no prompt id, so it never reports a
11
+ * stopReason and there is no exact end to observe — silence is the only signal
12
+ * available, and this is the bound that turns it into a decision.
13
+ *
14
+ * Sized from the fleet's own scheduled-run history: across 1513 completed
15
+ * scheduled runs the longest silence WITHIN a working turn was 120.2 s (p99
16
+ * 41.0 s; 5 runs above 60 s). A shorter grace would release the lease while the
17
+ * adapter is still working and re-admit a prompt into a busy turn, which is the
18
+ * FLEET-003 failure itself. The costs are deliberately asymmetric: holding too
19
+ * long skips one best-effort maintenance tick, releasing too early SIGTERMs a
20
+ * live role.
21
+ */
22
+ export declare const STEERING_OCCUPANCY_IDLE_MS = 150000;
8
23
  /** Server-generated typed provenance followed by the exact human-authored body. */
9
24
  export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
10
25
  export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
@@ -33,6 +48,8 @@ export interface AcpSessionOptions {
33
48
  controllerGraceMs?: number;
34
49
  /** Test seam; production uses AFTER_TOOL_BOUNDARY_TIMEOUT_MS. */
35
50
  afterToolBoundaryTimeoutMs?: number;
51
+ /** Test seam; production uses STEERING_OCCUPANCY_IDLE_MS. */
52
+ steeringOccupancyIdleMs?: number;
36
53
  }
37
54
  /**
38
55
  * Classify an ACP `stopReason` into a terminal outcome. A refusal and a
@@ -81,6 +98,13 @@ export declare class AcpSession implements SessionHandle {
81
98
  private cancelEscalation?;
82
99
  private cancelForceKill?;
83
100
  private cancelRecoveryReason?;
101
+ /**
102
+ * Held while a steering-started turn is believed to own the adapter. It is a
103
+ * lease, not a latch: `steeringRelease` always fires, so the role can never be
104
+ * stranded busy by a wake whose turn ended without telling anyone.
105
+ */
106
+ private steeringOccupied;
107
+ private steeringRelease?;
84
108
  /**
85
109
  * Rejects the moment the adapter process is gone. Every in-flight ACP request
86
110
  * races it, so a dead adapter can never leave a turn — and therefore a
@@ -102,6 +126,20 @@ export declare class AcpSession implements SessionHandle {
102
126
  */
103
127
  private recoverOpenPrompts;
104
128
  isAlive(): boolean;
129
+ /**
130
+ * Take the occupancy lease for a turn the adapter started on its own behalf.
131
+ * Refreshed by every adapter update, so it tracks work actually happening
132
+ * rather than a fixed guess at how long a wake takes.
133
+ */
134
+ private holdSteeringOccupancy;
135
+ private refreshSteeringOccupancy;
136
+ /**
137
+ * Every exit from occupancy comes through here, including the ones that are
138
+ * not the timer: a real turn boundary, close, and adapter exit. A lease that
139
+ * can leak is worse than the bug it fixes — it would leave the role reporting
140
+ * `running` forever and starve scheduled admission permanently.
141
+ */
142
+ private releaseSteeringOccupancy;
105
143
  snapshot(): SessionSnapshot;
106
144
  private toolCall;
107
145
  private reserveTool;
@@ -16,6 +16,21 @@ const PERMISSION_TIMEOUT_MS = 10 * 60_000;
16
16
  const CONTROLLER_GRACE_MS = 12_000;
17
17
  /** Bound safe-boundary waiting without turning a hung tool into cancellation. */
18
18
  export const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120_000;
19
+ /**
20
+ * How long a steering-started turn is presumed to still own the adapter after
21
+ * its last update. Such a turn has no prompt id, so it never reports a
22
+ * stopReason and there is no exact end to observe — silence is the only signal
23
+ * available, and this is the bound that turns it into a decision.
24
+ *
25
+ * Sized from the fleet's own scheduled-run history: across 1513 completed
26
+ * scheduled runs the longest silence WITHIN a working turn was 120.2 s (p99
27
+ * 41.0 s; 5 runs above 60 s). A shorter grace would release the lease while the
28
+ * adapter is still working and re-admit a prompt into a busy turn, which is the
29
+ * FLEET-003 failure itself. The costs are deliberately asymmetric: holding too
30
+ * long skips one best-effort maintenance tick, releasing too early SIGTERMs a
31
+ * live role.
32
+ */
33
+ export const STEERING_OCCUPANCY_IDLE_MS = 150_000;
19
34
  const TERMINAL_TOOL_STATUSES = new Set(['completed', 'failed']);
20
35
  const SCHEDULED_LOOP_REDACTION = '[scheduled-loop content redacted]';
21
36
  const OWNER_COMMENTARY_REDACTION = '[assistant commentary redacted]';
@@ -223,6 +238,13 @@ export class AcpSession {
223
238
  cancelEscalation;
224
239
  cancelForceKill;
225
240
  cancelRecoveryReason;
241
+ /**
242
+ * Held while a steering-started turn is believed to own the adapter. It is a
243
+ * lease, not a latch: `steeringRelease` always fires, so the role can never be
244
+ * stranded busy by a wake whose turn ended without telling anyone.
245
+ */
246
+ steeringOccupied = false;
247
+ steeringRelease;
226
248
  /**
227
249
  * Rejects the moment the adapter process is gone. Every in-flight ACP request
228
250
  * races it, so a dead adapter can never leave a turn — and therefore a
@@ -253,6 +275,7 @@ export class AcpSession {
253
275
  if (this.cancelForceKill)
254
276
  clearTimeout(this.cancelForceKill);
255
277
  this.cancelForceKill = undefined;
278
+ this.releaseSteeringOccupancy('adapter exited');
256
279
  // Record the child's real exit code/signal. The tmux path can only see a
257
280
  // shell's `$?`; here the truth is available, so keep it.
258
281
  const classified = classifyChildExit(code, signal);
@@ -353,11 +376,50 @@ export class AcpSession {
353
376
  // terminal fact (signal exits deliberately leave exitCode null).
354
377
  return this.child.exitCode === null && (this.child.signalCode ?? null) === null;
355
378
  }
379
+ /**
380
+ * Take the occupancy lease for a turn the adapter started on its own behalf.
381
+ * Refreshed by every adapter update, so it tracks work actually happening
382
+ * rather than a fixed guess at how long a wake takes.
383
+ */
384
+ holdSteeringOccupancy() {
385
+ if (this.closing || !this.isAlive())
386
+ return;
387
+ this.steeringOccupied = true;
388
+ this.refreshSteeringOccupancy();
389
+ }
390
+ refreshSteeringOccupancy() {
391
+ if (!this.steeringOccupied)
392
+ return;
393
+ if (this.steeringRelease)
394
+ clearTimeout(this.steeringRelease);
395
+ this.steeringRelease = setTimeout(() => this.releaseSteeringOccupancy('adapter silent'), this.options.steeringOccupancyIdleMs ?? STEERING_OCCUPANCY_IDLE_MS);
396
+ this.steeringRelease.unref?.();
397
+ }
398
+ /**
399
+ * Every exit from occupancy comes through here, including the ones that are
400
+ * not the timer: a real turn boundary, close, and adapter exit. A lease that
401
+ * can leak is worse than the bug it fixes — it would leave the role reporting
402
+ * `running` forever and starve scheduled admission permanently.
403
+ */
404
+ releaseSteeringOccupancy(reason) {
405
+ if (this.steeringRelease)
406
+ clearTimeout(this.steeringRelease);
407
+ this.steeringRelease = undefined;
408
+ if (!this.steeringOccupied)
409
+ return;
410
+ this.steeringOccupied = false;
411
+ this.options.log(`[${this.options.name}] steering-started turn no longer holds the adapter (${reason})`);
412
+ }
356
413
  snapshot() {
357
414
  return {
358
415
  backend: 'acp',
359
416
  alive: this.isAlive(),
360
- readiness: this.readiness,
417
+ // A steering-started turn is real work with no prompt id. Reporting the
418
+ // session idle while it runs is what let the arbiter admit a scheduled
419
+ // prompt into a busy adapter, whose `session/prompt` then never returned
420
+ // a stopReason and ended in a cancellation deadline and a SIGTERM.
421
+ readiness: this.readiness === 'idle' && this.steeringOccupied
422
+ ? 'running' : this.readiness,
361
423
  sessionId: this.sessionId,
362
424
  lastError: this.lastError,
363
425
  pendingPermissionId: this.pendingPermissions.keys().next().value,
@@ -865,6 +927,7 @@ export class AcpSession {
865
927
  if (this.controllerGrace)
866
928
  clearTimeout(this.controllerGrace);
867
929
  this.controllerGrace = undefined;
930
+ this.releaseSteeringOccupancy('session closed');
868
931
  for (const [permissionId, pending] of [...this.pendingPermissions])
869
932
  this.settlePendingAutomatically(permissionId, pending, 'cancelled', undefined, 'the session closed while this request was pending');
870
933
  this.releaseAllTools();
@@ -1023,6 +1086,10 @@ export class AcpSession {
1023
1086
  }
1024
1087
  finally {
1025
1088
  this.releaseAllTools();
1089
+ // A turn this client owned has ended, so the adapter has reported a
1090
+ // boundary: whatever a steering call started before it is over too. This
1091
+ // is the release path that does not depend on the silence timer.
1092
+ this.releaseSteeringOccupancy('turn boundary');
1026
1093
  if (this.activeTurn?.id === turnId) {
1027
1094
  this.activeTurn.settle();
1028
1095
  if (this.cancelEscalation)
@@ -1045,6 +1112,12 @@ export class AcpSession {
1045
1112
  ]);
1046
1113
  if (response.outcome === 'failed')
1047
1114
  return turnResult(false, 'failed', 'ACP steering failed');
1115
+ // `injected` joined a turn this client already owns and will settle.
1116
+ // `startedNewTurn` created one nobody owns: the adapter is working and
1117
+ // will never answer for it, so admission has to learn about it here or
1118
+ // not at all.
1119
+ if (response.outcome === 'startedNewTurn')
1120
+ this.holdSteeringOccupancy();
1048
1121
  return turnResult(true, 'inconclusive', response.outcome);
1049
1122
  }
1050
1123
  catch (error) {
@@ -1230,9 +1303,13 @@ export class AcpSession {
1230
1303
  }
1231
1304
  recordUpdate(update) {
1232
1305
  // Replayed history is not current activity: `session/load` would otherwise
1233
- // make a cold session look like it had just been working.
1234
- if (!this.replaying)
1306
+ // make a cold session look like it had just been working. The same reason
1307
+ // keeps it from extending the steering lease, which is evidence the adapter
1308
+ // is working right now — for a steering-started turn, the only evidence.
1309
+ if (!this.replaying) {
1235
1310
  this.lastUpdateAt = new Date().toISOString();
1311
+ this.refreshSteeringOccupancy();
1312
+ }
1236
1313
  const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
1237
1314
  const messagePhase = update.sessionUpdate === 'agent_message_chunk'
1238
1315
  ? this.codexMessagePhase(update) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
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",