@ours.network/fleet 0.13.2 → 0.14.0

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.
@@ -7,6 +7,7 @@ export class ScheduledLoopManager {
7
7
  definitions = new Map();
8
8
  store;
9
9
  timer;
10
+ runTimeouts = new Set();
10
11
  stopping = false;
11
12
  constructor(role, definitions, stateDir, arbiter, deps) {
12
13
  this.role = role;
@@ -27,6 +28,9 @@ export class ScheduledLoopManager {
27
28
  if (this.timer !== undefined)
28
29
  this.deps.clearTimer(this.timer);
29
30
  this.timer = undefined;
31
+ for (const timeout of this.runTimeouts)
32
+ this.deps.clearTimer(timeout);
33
+ this.runTimeouts.clear();
30
34
  this.store.state.clock.lastWallMs = this.deps.now();
31
35
  this.store.persist();
32
36
  }
@@ -140,7 +144,25 @@ export class ScheduledLoopManager {
140
144
  return { state: 'unavailable', runId };
141
145
  }
142
146
  this.deps.log(`[${this.role}] loop ${definition.name} started run=${runId.slice(0, 11)} scheduled=${new Date(scheduledAt).toISOString()}`);
143
- void result.queued.completion.then(turn => this.finish(definition, state, runId, turn));
147
+ // Maintenance is best-effort and must never monopolize a persistent role.
148
+ // A cancellation which the ACP adapter ignores is escalated by AcpSession
149
+ // into an adapter restart, preserving the session and deferred owner mail.
150
+ const timeoutMs = Math.max(60_000, Math.min(5 * 60_000, Math.floor(definition.intervalMs / 2)));
151
+ const timeout = this.deps.setTimer(() => {
152
+ this.runTimeouts.delete(timeout);
153
+ if (state.activeRunId !== runId || this.stopping)
154
+ return;
155
+ this.deps.log(`[${this.role}] loop ${definition.name} timed out run=${runId.slice(0, 11)} after ${timeoutMs}ms; cancelling`);
156
+ void this.arbiter.interrupt('scheduled-loop').catch(error => {
157
+ this.deps.log(`[${this.role}] loop ${definition.name} timeout cancellation failed: ${error?.name ?? 'Error'}`);
158
+ });
159
+ }, timeoutMs);
160
+ this.runTimeouts.add(timeout);
161
+ void result.queued.completion.then(turn => {
162
+ this.deps.clearTimer(timeout);
163
+ this.runTimeouts.delete(timeout);
164
+ this.finish(definition, state, runId, turn);
165
+ });
144
166
  return { state: 'started', runId };
145
167
  }
146
168
  finish(definition, state, runId, result) {
@@ -1,12 +1,15 @@
1
1
  import { type ChildProcessWithoutNullStreams } from 'node:child_process';
2
2
  import { type OwnerChannelConfig } from '../config.js';
3
3
  import type { SessionHandle } from '../session/types.js';
4
+ import { type OwnerFleetOps } from './commands.js';
4
5
  import { type OursToolClient } from './mcp.js';
5
6
  import { type OwnerUpdatePhase } from './notices.js';
6
7
  import { type OwnerEntry } from './state.js';
7
8
  import { type OwnerTaskPhase } from './tasks.js';
8
9
  export interface OwnerChannelOptions {
9
10
  role: string;
11
+ /** Harness id of the role (e.g. 'claude-code', 'codex'); gates which slash commands may be forwarded. */
12
+ harness: string;
10
13
  config: OwnerChannelConfig;
11
14
  session: SessionHandle;
12
15
  stateDir: string;
@@ -16,6 +19,10 @@ export interface OwnerChannelOptions {
16
19
  client?: OursToolClient;
17
20
  /** Test seam; production uses `ours-mcp watch <identity>`. */
18
21
  watch?: (identity: string) => ChildProcessWithoutNullStreams;
22
+ /** Test seam; production uses the detached ours-fleet CLI (`fleetCliOps`). */
23
+ fleet?: OwnerFleetOps;
24
+ /** Forwarded to fleet CLI invocations spawned for owner commands. */
25
+ configPath?: string;
19
26
  }
20
27
  export interface OwnerChannelHandle {
21
28
  start(): Promise<void>;
@@ -109,6 +116,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
109
116
  private readonly client;
110
117
  private readonly state;
111
118
  private readonly authorizations;
119
+ private readonly conversations;
112
120
  private readonly tasks;
113
121
  private readonly attachmentRecovery;
114
122
  private readonly attachmentConfig;
@@ -118,6 +126,8 @@ export declare class OwnerChannel implements OwnerChannelHandle {
118
126
  * (a crash must replay them) but must not be queued twice while live.
119
127
  */
120
128
  private readonly inFlight;
129
+ /** Wires already NACKed to the managed agent, so a deferred replay stays quiet. */
130
+ private readonly relayNacks;
121
131
  private stopping;
122
132
  private watchProcess?;
123
133
  private watchTask?;
@@ -127,6 +137,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
127
137
  private readonly activeRequests;
128
138
  private managementTail;
129
139
  private ready;
140
+ private readonly fleetOps;
130
141
  constructor(options: OwnerChannelOptions);
131
142
  start(): Promise<void>;
132
143
  drain(): Promise<void>;
@@ -139,14 +150,55 @@ export declare class OwnerChannel implements OwnerChannelHandle {
139
150
  private assertLabel;
140
151
  private safeMetadata;
141
152
  private sendOwnerUpdate;
153
+ private sendProactiveMessage;
142
154
  private openOwnerTask;
143
155
  private sendOwnerTaskReport;
144
156
  private safeOwnerUpdate;
145
157
  private safeTaskReport;
158
+ private safeProactiveMessage;
146
159
  private drainAll;
147
160
  private attachmentGroups;
148
161
  private handleAttachmentGroup;
149
162
  private handle;
163
+ /**
164
+ * Deterministic command path: the message never becomes an agent prompt.
165
+ * Authorization already happened — the managed-agent relay branch and the
166
+ * owner-CID check in handle() both run before dispatch, so only an
167
+ * authenticated owner reaches this: neither ordinary peers nor the managed
168
+ * agent itself can execute /force-restart, /model, or any other command.
169
+ */
170
+ private handleCommand;
171
+ /** Queue raw slash text to the harness and report the turn's outcome. */
172
+ private runHarnessCommand;
173
+ /**
174
+ * Confirmation and the durable wire record must both land BEFORE the fleet
175
+ * CLI is asked to bounce this very process; neither can happen afterwards.
176
+ */
177
+ private restartSelf;
178
+ /** Code-point-safe tail of the worklog, or undefined when there is none. */
179
+ private readWorklogTail;
180
+ /** Bound harness-command output to a single outbound message. */
181
+ private commandOutput;
182
+ private acceptedSender;
183
+ private isAgentSender;
184
+ private isEffectiveOwner;
185
+ private relayManagedAgentMessage;
186
+ /**
187
+ * One bounded NACK per wire: an unroutable or refused relay must be visible
188
+ * to the authenticated agent, while its deferred replays stay quiet. NACK
189
+ * delivery is best-effort — it must never make the failure worse.
190
+ */
191
+ private nackManagedAgent;
192
+ /**
193
+ * Daemon contact resolution is case-exact, so a canonical config CID picked
194
+ * by the sole-owner fallback is translated to the daemon-known contact form
195
+ * when one exists. Last-inbound routes already carry the daemon form.
196
+ */
197
+ private routableContact;
198
+ private safeRelayMessage;
199
+ private warnOwnerOfUnauthorizedSender;
200
+ private effectiveOwners;
201
+ private authorizationIntegrity;
150
202
  private complete;
151
203
  private ownerAttachmentPrompt;
152
204
  private ownerPrompt;