@borgee/agents-host 0.2.1 → 0.2.2

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.
@@ -174,7 +174,6 @@ export class AgentsHost {
174
174
  ? this.enqueueProgressChannelTurn(message.channel_id, () => this.handleMessage(message))
175
175
  : this.handleMessage(message);
176
176
  this.trackActiveTurn(task);
177
- return task;
178
177
  });
179
178
  const agentId = await this.ensureSelfAgentId();
180
179
  console.log('[agents-host] connected', {
@@ -1,6 +1,6 @@
1
1
  import type { ChannelMessageEvent, MeResponseUser, PostedMessage } from '../types.js';
2
2
  export interface ChatControlPlane {
3
- connect(onMessage: (message: ChannelMessageEvent) => void | Promise<void>): Promise<void>;
3
+ connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
4
4
  close(): Promise<void>;
5
5
  postMessage(channelId: string, content: string): Promise<PostedMessage>;
6
6
  editMessage(messageId: string, content: string): Promise<void>;
@@ -11,11 +11,12 @@ type SdkChatControlPlaneOptions = Pick<BorgeePluginOptions, 'cursorStore' | 'plu
11
11
  */
12
12
  export declare class SdkChatControlPlane implements ChatControlPlane {
13
13
  private readonly client;
14
+ private pendingMessages;
14
15
  private unsubscribe;
15
16
  private connected;
16
17
  private me;
17
18
  constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, options?: SdkChatControlPlaneOptions);
18
- connect(onMessage: (message: ChannelMessageEvent) => void | Promise<void>): Promise<void>;
19
+ connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
19
20
  close(): Promise<void>;
20
21
  postMessage(channelId: string, content: string): Promise<PostedMessage>;
21
22
  editMessage(messageId: string, content: string): Promise<void>;
@@ -6,6 +6,7 @@ import { createBorgeePlugin, } from '@borgee/plugin-sdk';
6
6
  */
7
7
  export class SdkChatControlPlane {
8
8
  client;
9
+ pendingMessages = [];
9
10
  unsubscribe = null;
10
11
  connected = false;
11
12
  me = null;
@@ -13,28 +14,35 @@ export class SdkChatControlPlane {
13
14
  this.client = createClient({ baseUrl, apiKey, ...options });
14
15
  }
15
16
  async connect(onMessage) {
16
- this.unsubscribe = this.client.on('message', async (event) => {
17
- await onMessage(mapInboundToChannelMessage(event));
17
+ this.unsubscribe = this.client.on('message', (event) => {
18
+ const message = mapInboundToChannelMessage(event);
19
+ if (!this.connected) {
20
+ this.pendingMessages.push(message);
21
+ return;
22
+ }
23
+ onMessage(message);
18
24
  });
19
25
  try {
20
- // The SDK resolves connect after session.resume_ack, before every replay
21
- // frame necessarily runs. Mark the consumer ready first so replay ACKs
22
- // wait for the real handler instead of acknowledging an in-memory buffer.
23
- this.connected = true;
24
26
  await this.client.connect();
27
+ this.connected = true;
25
28
  if (this.client.agentId) {
26
29
  this.me = { id: this.client.agentId };
27
30
  }
31
+ for (const message of this.pendingMessages) {
32
+ onMessage(message);
33
+ }
34
+ this.pendingMessages = [];
28
35
  }
29
36
  catch (error) {
30
- this.connected = false;
31
37
  this.unsubscribe?.();
32
38
  this.unsubscribe = null;
39
+ this.pendingMessages = [];
33
40
  throw error;
34
41
  }
35
42
  }
36
43
  async close() {
37
44
  this.connected = false;
45
+ this.pendingMessages = [];
38
46
  this.unsubscribe?.();
39
47
  this.unsubscribe = null;
40
48
  await this.client.close();
@@ -4,6 +4,28 @@ export interface ClaudeChannelSessionStore {
4
4
  }
5
5
  export interface FileClaudeChannelSessionStoreOptions {
6
6
  resolvePath(agentId: string): string;
7
+ fileSystem?: ClaudeSessionStoreFileSystem;
8
+ platform?: NodeJS.Platform;
9
+ }
10
+ interface ClaudeSessionStoreDirectoryHandle {
11
+ sync(): Promise<void>;
12
+ close(): Promise<void>;
13
+ }
14
+ interface ClaudeSessionStoreFileHandle extends ClaudeSessionStoreDirectoryHandle {
15
+ writeFile(data: string, options: {
16
+ encoding: BufferEncoding;
17
+ }): Promise<void>;
18
+ }
19
+ interface ClaudeSessionStoreFileSystem {
20
+ mkdir(path: string, options: {
21
+ recursive: true;
22
+ mode: number;
23
+ }): Promise<void>;
24
+ openDirectory(path: string): Promise<ClaudeSessionStoreDirectoryHandle>;
25
+ openFile(path: string, flags: string, mode: number): Promise<ClaudeSessionStoreFileHandle>;
26
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
27
+ rename(from: string, to: string): Promise<void>;
28
+ unlink(path: string): Promise<void>;
7
29
  }
8
30
  export declare class FileClaudeChannelSessionStore implements ClaudeChannelSessionStore {
9
31
  private readonly options;
@@ -12,3 +34,4 @@ export declare class FileClaudeChannelSessionStore implements ClaudeChannelSessi
12
34
  load(agentId: string): Promise<Record<string, string>>;
13
35
  save(agentId: string, sessions: Record<string, string>): Promise<void>;
14
36
  }
37
+ export {};
@@ -1,12 +1,63 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
- async function syncDirectory(path) {
5
- const directoryHandle = await open(path, 'r');
4
+ const nodeFileSystem = {
5
+ async mkdir(path, options) {
6
+ await mkdir(path, options);
7
+ },
8
+ async openDirectory(path) {
9
+ return open(path, 'r');
10
+ },
11
+ async openFile(path, flags, mode) {
12
+ return open(path, flags, mode);
13
+ },
14
+ async readFile(path, encoding) {
15
+ return readFile(path, encoding);
16
+ },
17
+ async rename(from, to) {
18
+ await rename(from, to);
19
+ },
20
+ async unlink(path) {
21
+ await unlink(path);
22
+ },
23
+ };
24
+ function shouldIgnoreDirectorySyncError(platform, error) {
25
+ const code = error.code;
26
+ return platform === 'win32' && (code === 'EPERM' || code === 'EINVAL');
27
+ }
28
+ async function syncDirectory(path, fileSystem, platform) {
29
+ let directoryHandle;
6
30
  try {
7
- await directoryHandle.sync();
31
+ try {
32
+ directoryHandle = await fileSystem.openDirectory(path);
33
+ }
34
+ catch (error) {
35
+ if (!shouldIgnoreDirectorySyncError(platform, error)) {
36
+ throw error;
37
+ }
38
+ return;
39
+ }
40
+ try {
41
+ await directoryHandle.sync();
42
+ }
43
+ catch (error) {
44
+ if (!shouldIgnoreDirectorySyncError(platform, error)) {
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (directoryHandle) {
51
+ try {
52
+ await directoryHandle.close();
53
+ }
54
+ catch {
55
+ // Preserve the original directory durability failure.
56
+ }
57
+ }
58
+ throw error;
8
59
  }
9
- finally {
60
+ if (directoryHandle) {
10
61
  await directoryHandle.close();
11
62
  }
12
63
  }
@@ -32,7 +83,7 @@ export class FileClaudeChannelSessionStore {
32
83
  async loadFromPath(filePath) {
33
84
  let raw;
34
85
  try {
35
- raw = await readFile(filePath, 'utf8');
86
+ raw = await (this.options.fileSystem ?? nodeFileSystem).readFile(filePath, 'utf8');
36
87
  }
37
88
  catch (error) {
38
89
  throw error;
@@ -54,11 +105,13 @@ export class FileClaudeChannelSessionStore {
54
105
  async save(agentId, sessions) {
55
106
  const filePath = this.options.resolvePath(agentId);
56
107
  const parentPath = dirname(filePath);
57
- await mkdir(parentPath, { recursive: true, mode: 0o700 });
108
+ const fileSystem = this.options.fileSystem ?? nodeFileSystem;
109
+ const platform = this.options.platform ?? process.platform;
110
+ await fileSystem.mkdir(parentPath, { recursive: true, mode: 0o700 });
58
111
  const entries = Object.entries(sessions).sort(([left], [right]) => left.localeCompare(right));
59
112
  if (entries.length === 0) {
60
113
  try {
61
- await unlink(filePath);
114
+ await fileSystem.unlink(filePath);
62
115
  }
63
116
  catch (error) {
64
117
  if (error.code !== 'ENOENT') {
@@ -66,21 +119,21 @@ export class FileClaudeChannelSessionStore {
66
119
  }
67
120
  return;
68
121
  }
69
- await syncDirectory(parentPath);
122
+ await syncDirectory(parentPath, fileSystem, platform);
70
123
  return;
71
124
  }
72
125
  const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
73
126
  let temporaryHandle;
74
127
  try {
75
- temporaryHandle = await open(temporaryPath, 'wx', 0o600);
128
+ temporaryHandle = await fileSystem.openFile(temporaryPath, 'wx', 0o600);
76
129
  await temporaryHandle.writeFile(JSON.stringify(Object.fromEntries(entries)), {
77
130
  encoding: 'utf8',
78
131
  });
79
132
  await temporaryHandle.sync();
80
133
  await temporaryHandle.close();
81
134
  temporaryHandle = undefined;
82
- await rename(temporaryPath, filePath);
83
- await syncDirectory(parentPath);
135
+ await fileSystem.rename(temporaryPath, filePath);
136
+ await syncDirectory(parentPath, fileSystem, platform);
84
137
  }
85
138
  catch (error) {
86
139
  if (temporaryHandle) {
@@ -92,7 +145,7 @@ export class FileClaudeChannelSessionStore {
92
145
  }
93
146
  }
94
147
  try {
95
- await unlink(temporaryPath);
148
+ await fileSystem.unlink(temporaryPath);
96
149
  }
97
150
  catch {
98
151
  // Cleanup is best effort.
@@ -4,6 +4,28 @@ export interface CopilotChannelSessionStore {
4
4
  }
5
5
  export interface FileCopilotChannelSessionStoreOptions {
6
6
  resolvePath(agentId: string): string;
7
+ fileSystem?: CopilotSessionStoreFileSystem;
8
+ platform?: NodeJS.Platform;
9
+ }
10
+ interface CopilotSessionStoreDirectoryHandle {
11
+ sync(): Promise<void>;
12
+ close(): Promise<void>;
13
+ }
14
+ interface CopilotSessionStoreFileHandle extends CopilotSessionStoreDirectoryHandle {
15
+ writeFile(data: string, options: {
16
+ encoding: BufferEncoding;
17
+ }): Promise<void>;
18
+ }
19
+ interface CopilotSessionStoreFileSystem {
20
+ mkdir(path: string, options: {
21
+ recursive: true;
22
+ mode: number;
23
+ }): Promise<void>;
24
+ openDirectory(path: string): Promise<CopilotSessionStoreDirectoryHandle>;
25
+ openFile(path: string, flags: string, mode: number): Promise<CopilotSessionStoreFileHandle>;
26
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
27
+ rename(from: string, to: string): Promise<void>;
28
+ unlink(path: string): Promise<void>;
7
29
  }
8
30
  export declare class FileCopilotChannelSessionStore implements CopilotChannelSessionStore {
9
31
  private readonly options;
@@ -12,3 +34,4 @@ export declare class FileCopilotChannelSessionStore implements CopilotChannelSes
12
34
  load(agentId: string): Promise<Record<string, string>>;
13
35
  save(agentId: string, sessions: Record<string, string>): Promise<void>;
14
36
  }
37
+ export {};
@@ -1,12 +1,63 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
- async function syncDirectory(path) {
5
- const directoryHandle = await open(path, 'r');
4
+ const nodeFileSystem = {
5
+ async mkdir(path, options) {
6
+ await mkdir(path, options);
7
+ },
8
+ async openDirectory(path) {
9
+ return open(path, 'r');
10
+ },
11
+ async openFile(path, flags, mode) {
12
+ return open(path, flags, mode);
13
+ },
14
+ async readFile(path, encoding) {
15
+ return readFile(path, encoding);
16
+ },
17
+ async rename(from, to) {
18
+ await rename(from, to);
19
+ },
20
+ async unlink(path) {
21
+ await unlink(path);
22
+ },
23
+ };
24
+ function shouldIgnoreDirectorySyncError(platform, error) {
25
+ const code = error.code;
26
+ return platform === 'win32' && (code === 'EPERM' || code === 'EINVAL');
27
+ }
28
+ async function syncDirectory(path, fileSystem, platform) {
29
+ let directoryHandle;
6
30
  try {
7
- await directoryHandle.sync();
31
+ try {
32
+ directoryHandle = await fileSystem.openDirectory(path);
33
+ }
34
+ catch (error) {
35
+ if (!shouldIgnoreDirectorySyncError(platform, error)) {
36
+ throw error;
37
+ }
38
+ return;
39
+ }
40
+ try {
41
+ await directoryHandle.sync();
42
+ }
43
+ catch (error) {
44
+ if (!shouldIgnoreDirectorySyncError(platform, error)) {
45
+ throw error;
46
+ }
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (directoryHandle) {
51
+ try {
52
+ await directoryHandle.close();
53
+ }
54
+ catch {
55
+ // Preserve the original directory durability failure.
56
+ }
57
+ }
58
+ throw error;
8
59
  }
9
- finally {
60
+ if (directoryHandle) {
10
61
  await directoryHandle.close();
11
62
  }
12
63
  }
@@ -30,7 +81,7 @@ export class FileCopilotChannelSessionStore {
30
81
  this.options = options;
31
82
  }
32
83
  async loadFromPath(filePath) {
33
- const raw = await readFile(filePath, 'utf8');
84
+ const raw = await (this.options.fileSystem ?? nodeFileSystem).readFile(filePath, 'utf8');
34
85
  return normalizePersistedSessions(JSON.parse(raw), filePath);
35
86
  }
36
87
  async load(agentId) {
@@ -48,11 +99,13 @@ export class FileCopilotChannelSessionStore {
48
99
  async save(agentId, sessions) {
49
100
  const filePath = this.options.resolvePath(agentId);
50
101
  const parentPath = dirname(filePath);
51
- await mkdir(parentPath, { recursive: true, mode: 0o700 });
102
+ const fileSystem = this.options.fileSystem ?? nodeFileSystem;
103
+ const platform = this.options.platform ?? process.platform;
104
+ await fileSystem.mkdir(parentPath, { recursive: true, mode: 0o700 });
52
105
  const entries = Object.entries(sessions).sort(([left], [right]) => left.localeCompare(right));
53
106
  if (entries.length === 0) {
54
107
  try {
55
- await unlink(filePath);
108
+ await fileSystem.unlink(filePath);
56
109
  }
57
110
  catch (error) {
58
111
  if (error.code !== 'ENOENT') {
@@ -60,21 +113,21 @@ export class FileCopilotChannelSessionStore {
60
113
  }
61
114
  return;
62
115
  }
63
- await syncDirectory(parentPath);
116
+ await syncDirectory(parentPath, fileSystem, platform);
64
117
  return;
65
118
  }
66
119
  const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
67
120
  let temporaryHandle;
68
121
  try {
69
- temporaryHandle = await open(temporaryPath, 'wx', 0o600);
122
+ temporaryHandle = await fileSystem.openFile(temporaryPath, 'wx', 0o600);
70
123
  await temporaryHandle.writeFile(JSON.stringify(Object.fromEntries(entries)), {
71
124
  encoding: 'utf8',
72
125
  });
73
126
  await temporaryHandle.sync();
74
127
  await temporaryHandle.close();
75
128
  temporaryHandle = undefined;
76
- await rename(temporaryPath, filePath);
77
- await syncDirectory(parentPath);
129
+ await fileSystem.rename(temporaryPath, filePath);
130
+ await syncDirectory(parentPath, fileSystem, platform);
78
131
  }
79
132
  catch (error) {
80
133
  if (temporaryHandle) {
@@ -86,7 +139,7 @@ export class FileCopilotChannelSessionStore {
86
139
  }
87
140
  }
88
141
  try {
89
- await unlink(temporaryPath);
142
+ await fileSystem.unlink(temporaryPath);
90
143
  }
91
144
  catch {
92
145
  // Cleanup is best effort.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgee/agents-host",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@agentclientprotocol/sdk": "^1.2.1",
39
- "@borgee/plugin-sdk": "0.1.4",
39
+ "@borgee/plugin-sdk": "0.1.5",
40
40
  "cross-spawn": "^7.0.6",
41
41
  "yaml": "^2.8.1"
42
42
  },