@ours.network/fleet 1.2.0-nightly.69 → 1.2.0-nightly.70

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,5 +1,6 @@
1
1
  import type { FileDeliveryInput, DeliveredFile } from '../file-delivery/types.js';
2
2
  import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
3
+ import { type Server } from 'node:net';
3
4
  import type { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
4
5
  import type { OursClient } from '@ours.network/sdk/client';
5
6
  import { FileCallbacks } from './file-rpc.js';
@@ -17,6 +18,17 @@ export interface EndpointOptions {
17
18
  /** Fleet self-reminders, bound later by the runner; tools refuse until then. */
18
19
  reminders?: () => ReminderToolBackend | undefined;
19
20
  }
21
+ /**
22
+ * Listen on the bridge socket. A close unlinks it, but a predecessor that ended
23
+ * abruptly leaves the file behind, and every later start of the same generation
24
+ * then fails with EADDRINUSE until the role is held down. Only a provably dead
25
+ * socket is removed, once, and only if the path still names that same file;
26
+ * a live listener, a symlink, any other file, an inconclusive probe or a path
27
+ * replaced since the probe still fails as before. `afterProbe` is a test seam.
28
+ */
29
+ export declare function listenOnBridgeSocket(server: Server, path: string, hooks?: {
30
+ afterProbe?: () => Promise<void>;
31
+ }): Promise<void>;
20
32
  export declare function startMcpEndpoint(options: EndpointOptions): Promise<{
21
33
  close(): Promise<void>;
22
34
  }>;
@@ -1,12 +1,80 @@
1
1
  import * as managedMcp from '@ours.network/mcp/server';
2
- import { createServer } from 'node:net';
3
- import { chmod, unlink } from 'node:fs/promises';
2
+ import { connect, createServer } from 'node:net';
3
+ import { chmod, lstat, unlink } from 'node:fs/promises';
4
4
  import { timingSafeEqual } from 'node:crypto';
5
5
  import { createManagedOursMcpServer } from '@ours.network/mcp/server';
6
6
  import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { FileCallbacks } from './file-rpc.js';
8
8
  import { Wire } from './wire.js';
9
9
  import { registerReminderTools } from '../reminders/tools.js';
10
+ const ORPHAN_PROBE_TIMEOUT_MS = 1_000;
11
+ function listenOnce(server, path) {
12
+ return new Promise((resolve, reject) => {
13
+ server.once('error', reject);
14
+ server.listen(path, () => {
15
+ server.removeListener('error', reject);
16
+ resolve();
17
+ });
18
+ });
19
+ }
20
+ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.ctimeNs === b.ctimeNs && a.birthtimeNs === b.birthtimeNs;
21
+ /**
22
+ * The identity of a socket file, never a symlink or other file, that refuses
23
+ * connections; undefined when it is not provably dead.
24
+ */
25
+ async function orphanSocket(path) {
26
+ let found;
27
+ try {
28
+ found = await lstat(path, { bigint: true });
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ if (!found.isSocket())
34
+ return undefined;
35
+ const dead = await new Promise(resolve => {
36
+ const probe = connect(path);
37
+ const done = (orphan) => { clearTimeout(timer); probe.destroy(); resolve(orphan); };
38
+ const timer = setTimeout(() => done(false), ORPHAN_PROBE_TIMEOUT_MS);
39
+ probe.once('connect', () => done(false));
40
+ probe.once('error', error => done(error.code === 'ECONNREFUSED'));
41
+ });
42
+ return dead ? { dev: found.dev, ino: found.ino, ctimeNs: found.ctimeNs, birthtimeNs: found.birthtimeNs } : undefined;
43
+ }
44
+ /**
45
+ * Listen on the bridge socket. A close unlinks it, but a predecessor that ended
46
+ * abruptly leaves the file behind, and every later start of the same generation
47
+ * then fails with EADDRINUSE until the role is held down. Only a provably dead
48
+ * socket is removed, once, and only if the path still names that same file;
49
+ * a live listener, a symlink, any other file, an inconclusive probe or a path
50
+ * replaced since the probe still fails as before. `afterProbe` is a test seam.
51
+ */
52
+ export async function listenOnBridgeSocket(server, path, hooks = {}) {
53
+ let original;
54
+ try {
55
+ return await listenOnce(server, path);
56
+ }
57
+ catch (error) {
58
+ original = error;
59
+ if (error.code !== 'EADDRINUSE')
60
+ throw error;
61
+ }
62
+ const orphan = await orphanSocket(path);
63
+ if (!orphan)
64
+ throw original;
65
+ await hooks.afterProbe?.();
66
+ let now;
67
+ try {
68
+ now = await lstat(path, { bigint: true });
69
+ }
70
+ catch {
71
+ throw original;
72
+ }
73
+ if (!now.isSocket() || !sameFile(now, orphan))
74
+ throw original;
75
+ await unlink(path);
76
+ await listenOnce(server, path);
77
+ }
10
78
  export async function startMcpEndpoint(options) {
11
79
  // Every managed Agent offers current-chat delivery, so a package without the contract is a broken install, not a quiet downgrade.
12
80
  if (options.currentChatFile && managedMcp.MANAGED_FILE_DELIVERY_VERSION !== 1)
@@ -100,13 +168,7 @@ export async function startMcpEndpoint(options) {
100
168
  transport.onmessage?.(message);
101
169
  };
102
170
  });
103
- await new Promise((resolve, reject) => {
104
- server.once('error', reject);
105
- server.listen(options.socket, () => {
106
- server.removeListener('error', reject);
107
- resolve();
108
- });
109
- });
171
+ await listenOnBridgeSocket(server, options.socket);
110
172
  await chmod(options.socket, 0o600);
111
173
  return {
112
174
  close: async () => {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.69",
3
- "buildId": "fd8e44d5cbd1",
4
- "commit": "9f7830e8ce959b0ff6f88a95c71262c6ee0b0931",
2
+ "version": "1.2.0-nightly.70",
3
+ "buildId": "ca16d5313844",
4
+ "commit": "16bdad930ad484741ec362a5b1f608d9c5e8706a",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-16T18:59:33.079Z",
6
+ "builtAt": "2026-10-16T20:49:37.491Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.69",
3
+ "version": "1.2.0-nightly.70",
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",