@borgee/agents-host 0.2.0 → 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.
package/README.md CHANGED
@@ -315,13 +315,19 @@ Each Borgee channel is mapped 1:1 to a provider-native session while it stays
315
315
  active:
316
316
 
317
317
  - **Claude**: first turn for a channel uses `--session-id <uuid>`; every turn
318
- after uses `-r/--resume <uuid>`.
318
+ after uses `-r/--resume <uuid>`. The host persists the channel→session map in
319
+ its state root, serializes cross-channel map rewrites, and retries once with a
320
+ fresh session immediately if Claude reports a stale `--resume` target.
319
321
  - **Copilot**: one persistent `copilot --acp` subprocess is shared by a single
320
322
  `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are
321
- serialized; different channels keep isolated ACP sessions.
322
-
323
- There is no separate history store on our side. If a runner restarts, that
324
- agent's provider-side channel sessions start fresh.
323
+ serialized; different channels keep isolated ACP sessions. The host persists
324
+ the channel→ACP-session map in its state root and restores same-host
325
+ continuity with ACP `session/resume` when available, otherwise `session/load`
326
+ plus local replay cleanup on runtimes that advertise only load support.
327
+
328
+ There is no separate transcript/history store on our side. Claude and Copilot
329
+ only persist the native session ids they should try to resume later; cross-host
330
+ continuity still does not exist in this runtime.
325
331
 
326
332
  ## Testing
327
333
 
@@ -27,6 +27,7 @@ export declare class AgentsHost {
27
27
  private readonly progressChannelQueues;
28
28
  private readonly progressDrafts;
29
29
  private selfAgentId;
30
+ private selfAgentIdPromise;
30
31
  private started;
31
32
  private controlPlaneClosed;
32
33
  constructor(config: AgentsHostConfig, deps?: {
@@ -39,4 +40,5 @@ export declare class AgentsHost {
39
40
  private trackActiveTurn;
40
41
  private enqueueProgressChannelTurn;
41
42
  private ensureProgressDraft;
43
+ private ensureSelfAgentId;
42
44
  }
@@ -1,6 +1,13 @@
1
+ import { chmod, mkdir } from 'node:fs/promises';
1
2
  import { SdkChatControlPlane } from './chat/sdk-chat-control-plane.js';
3
+ import { createDurableCursorStore } from './durable-cursor-store.js';
2
4
  import { createProvider } from './providers/create-provider.js';
5
+ const PRIVATE_STATE_ROOT_MODE = 0o700;
3
6
  const STREAM_PROGRESS_EDIT_THROTTLE_MS = 150;
7
+ async function ensurePrivateStateRoot(path) {
8
+ await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
9
+ await chmod(path, PRIVATE_STATE_ROOT_MODE);
10
+ }
4
11
  function hasVisibleText(value) {
5
12
  return value.trim().length > 0;
6
13
  }
@@ -128,29 +135,37 @@ export class AgentsHost {
128
135
  progressChannelQueues = new Map();
129
136
  progressDrafts = new Map();
130
137
  selfAgentId = null;
138
+ selfAgentIdPromise = null;
131
139
  started = false;
132
140
  controlPlaneClosed = false;
133
141
  constructor(config, deps) {
134
142
  this.config = config;
135
143
  this.provider = deps?.provider ?? createProvider({
136
144
  provider: config.agent.provider,
145
+ stateRootDir: config.stateRootDir,
146
+ resolveStableAgentId: () => this.selfAgentId ?? undefined,
137
147
  claudeCommand: config.claudeCommand,
138
148
  claudeArgs: config.claudeArgs,
139
149
  copilotCommand: config.copilotCommand,
140
150
  copilotArgs: config.copilotArgs,
141
151
  copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
142
152
  });
143
- this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined,
144
- // Lets the server know which runtime/provider actually connected
145
- // (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
146
- // the web UI can show real state instead of a client-side guess.
147
- `agents-host:${config.agent.provider}`);
153
+ this.borgee = deps?.borgee ?? new SdkChatControlPlane(config.borgeeBaseUrl, config.agent.agentApiKey, undefined, {
154
+ cursorStore: createDurableCursorStore({
155
+ stateRootDir: config.stateRootDir,
156
+ }),
157
+ // Lets the server know which runtime/provider actually connected
158
+ // (internal/bpp ConnectHandler → users.last_connected_plugin_id), so
159
+ // the web UI can show real state instead of a client-side guess.
160
+ pluginId: `agents-host:${config.agent.provider}`,
161
+ });
148
162
  }
149
163
  async start() {
150
164
  if (this.started)
151
165
  return;
152
166
  this.started = true;
153
167
  this.controlPlaneClosed = false;
168
+ await ensurePrivateStateRoot(this.config.stateRootDir);
154
169
  await this.borgee.connect((message) => {
155
170
  if (!this.started) {
156
171
  return;
@@ -160,10 +175,9 @@ export class AgentsHost {
160
175
  : this.handleMessage(message);
161
176
  this.trackActiveTurn(task);
162
177
  });
163
- const me = await this.borgee.getMe();
164
- this.selfAgentId = me.id;
178
+ const agentId = await this.ensureSelfAgentId();
165
179
  console.log('[agents-host] connected', {
166
- agentId: me.id,
180
+ agentId,
167
181
  agentName: this.config.agent.agentName,
168
182
  provider: this.config.agent.provider,
169
183
  });
@@ -187,8 +201,9 @@ export class AgentsHost {
187
201
  agentName: this.config.agent.agentName,
188
202
  channelId: msg.channel_id,
189
203
  });
204
+ const selfAgentId = await this.ensureSelfAgentId();
190
205
  const authorId = String(msg.user_id ?? msg.sender_id ?? 'unknown');
191
- if (this.selfAgentId && authorId === this.selfAgentId) {
206
+ if (authorId === selfAgentId) {
192
207
  return;
193
208
  }
194
209
  const content = String(msg.content ?? msg.body ?? '').trim();
@@ -276,4 +291,19 @@ export class AgentsHost {
276
291
  }
277
292
  return draft;
278
293
  }
294
+ async ensureSelfAgentId() {
295
+ if (this.selfAgentId) {
296
+ return this.selfAgentId;
297
+ }
298
+ if (!this.selfAgentIdPromise) {
299
+ this.selfAgentIdPromise = this.borgee.getMe().then((me) => {
300
+ this.selfAgentId = me.id;
301
+ return me.id;
302
+ }).catch((error) => {
303
+ this.selfAgentIdPromise = null;
304
+ throw error;
305
+ });
306
+ }
307
+ return this.selfAgentIdPromise;
308
+ }
279
309
  }
@@ -3,6 +3,7 @@ import type { ChannelMessageEvent, MeResponseUser, PostedMessage } from '../type
3
3
  import type { ChatControlPlane } from './chat-control-plane.js';
4
4
  type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'deleteMessage' | 'editMessage' | 'getMe' | 'on' | 'sendMessage' | 'startTyping'>;
5
5
  type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
6
+ type SdkChatControlPlaneOptions = Pick<BorgeePluginOptions, 'cursorStore' | 'pluginId'>;
6
7
  /**
7
8
  * Thin adapter over `@borgee/plugin-sdk` (the same BPP/`/ws/plugin` SDK used by
8
9
  * the OpenClaw plugin) that implements the minimal `ChatControlPlane` surface
@@ -14,7 +15,7 @@ export declare class SdkChatControlPlane implements ChatControlPlane {
14
15
  private unsubscribe;
15
16
  private connected;
16
17
  private me;
17
- constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, pluginId?: string);
18
+ constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, options?: SdkChatControlPlaneOptions);
18
19
  connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
19
20
  close(): Promise<void>;
20
21
  postMessage(channelId: string, content: string): Promise<PostedMessage>;
@@ -10,8 +10,8 @@ export class SdkChatControlPlane {
10
10
  unsubscribe = null;
11
11
  connected = false;
12
12
  me = null;
13
- constructor(baseUrl, apiKey, createClient = createBorgeePlugin, pluginId) {
14
- this.client = createClient({ baseUrl, apiKey, pluginId });
13
+ constructor(baseUrl, apiKey, createClient = createBorgeePlugin, options = {}) {
14
+ this.client = createClient({ baseUrl, apiKey, ...options });
15
15
  }
16
16
  async connect(onMessage) {
17
17
  this.unsubscribe = this.client.on('message', (event) => {
package/dist/cli.js CHANGED
File without changes
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolveSingleAgentStateRoot } from './state-paths.js';
1
2
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
2
3
  export const MAX_COPILOT_SESSION_TTL_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
3
4
  export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
@@ -74,6 +75,7 @@ export function loadConfigFromEnv(env = process.env) {
74
75
  const provider = resolveProvider(envOr('RUNTIME_PROVIDER', 'claude', env), 'Unsupported RUNTIME_PROVIDER');
75
76
  return {
76
77
  borgeeBaseUrl: requireEnv('BORGEE_BASE_URL', env),
78
+ stateRootDir: resolveSingleAgentStateRoot(env),
77
79
  ...resolveProviderCommandConfig({
78
80
  claudeCommand: envOr('CLAUDE_COMMAND', DEFAULT_PROVIDER_COMMAND_CONFIG.claudeCommand, env),
79
81
  claudeArgs: parseArgs(envOr('CLAUDE_ARGS', DEFAULT_PROVIDER_COMMAND_CONFIG.claudeArgs.join(' '), env)),
@@ -0,0 +1,5 @@
1
+ import { type CursorStore } from '@borgee/plugin-sdk';
2
+ export interface DurableCursorStoreOptions {
3
+ stateRootDir: string;
4
+ }
5
+ export declare function createDurableCursorStore(options: DurableCursorStoreOptions): CursorStore;
@@ -0,0 +1,7 @@
1
+ import { FileCursorStore } from '@borgee/plugin-sdk';
2
+ import { resolveAgentCursorPath } from './state-paths.js';
3
+ export function createDurableCursorStore(options) {
4
+ return new FileCursorStore({
5
+ resolvePath: (agentId) => resolveAgentCursorPath(options.stateRootDir, agentId),
6
+ });
7
+ }
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { parseDocument, stringify } from 'yaml';
5
5
  import { optionalNonEmptyString, optionalStringArray, parseCopilotSessionTtlMinutesValue, requireNonEmptyString, resolveProvider, resolveProviderCommandConfig, } from './config.js';
6
+ import { resolveLocalConfigAgentStateRoot } from './state-paths.js';
6
7
  const SUPPORTED_CONFIG_EXTENSIONS = new Set(['.json', '.yaml', '.yml']);
7
8
  export const DEFAULT_LOCAL_HOST_CONFIG_FILENAME = 'agents-host.yaml';
8
9
  export const DEFAULT_LOCAL_AGENTS_DIRNAME = 'agents';
@@ -131,6 +132,9 @@ function parseHostConfigFile(hostConfigPath, value) {
131
132
  function toGeneratedAgentConfigFilename(key) {
132
133
  return `${encodeURIComponent(key)}.yaml`;
133
134
  }
135
+ function toCaseInsensitiveFilesystemIdentity(path) {
136
+ return path.toLowerCase();
137
+ }
134
138
  function parseGenerateHostConfigFile(sourceLabel, value) {
135
139
  if (value.agentsDir !== undefined) {
136
140
  throw new Error(`Invalid generate-config host spec in ${sourceLabel}: agentsDir is not supported; generate-config always writes the canonical default layout`);
@@ -573,13 +577,14 @@ function renderAgentConfigYaml(agent) {
573
577
  }
574
578
  return stringify(config);
575
579
  }
576
- function buildManagedAgentSnapshot(host, sourcePath, agent) {
580
+ function buildManagedAgentSnapshot(host, stateRootBaseDir, sourcePath, agent) {
577
581
  const providerConfig = resolveProviderCommandConfig({
578
582
  ...host.defaults,
579
583
  ...parseProviderCommandOverrides(agent, `agent config ${sourcePath}`),
580
584
  });
581
585
  const config = {
582
586
  borgeeBaseUrl: host.borgeeBaseUrl,
587
+ stateRootDir: resolveLocalConfigAgentStateRoot(stateRootBaseDir, agent.key),
583
588
  ...providerConfig,
584
589
  agent: {
585
590
  agentApiKey: agent.apiKey,
@@ -610,6 +615,7 @@ export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
610
615
  const resolvedHostConfigDir = dirname(resolvedHostConfigPath);
611
616
  const hostConfigRecord = await loadParsedDocument(fileSystem, resolvedHostConfigPath, 'host config');
612
617
  const hostConfig = parseHostConfigFile(absoluteHostConfigPath, hostConfigRecord);
618
+ const stateRootBaseDir = managedGeneration.managedRootPath ?? hostConfigDir;
613
619
  const agentsDir = toAgentsDir(absoluteHostConfigPath, hostConfig.agentsDir);
614
620
  const resolvedAgentsDir = resolve(fileSystem.realPath
615
621
  ? await fileSystem.realPath(toAgentsDir(resolvedHostConfigPath, hostConfig.agentsDir))
@@ -643,7 +649,7 @@ export async function loadLocalConfigSnapshot(hostConfigPath, deps = {}) {
643
649
  resolvedAgentsDir,
644
650
  agents: parsedAgents
645
651
  .filter((entry) => entry.config.enabled !== false)
646
- .map((entry) => buildManagedAgentSnapshot(hostConfig, entry.sourcePath, entry.config)),
652
+ .map((entry) => buildManagedAgentSnapshot(hostConfig, stateRootBaseDir, entry.sourcePath, entry.config)),
647
653
  };
648
654
  }
649
655
  finally {
@@ -1,5 +1,6 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import type { ProviderGenerateOptions } from '../../types.js';
3
+ import type { ClaudeChannelSessionStore } from './session-store.js';
3
4
  interface ClaudeCliRuntime {
4
5
  spawn: typeof spawn;
5
6
  }
@@ -14,15 +15,30 @@ interface ClaudeCliRuntime {
14
15
  export declare class ClaudeCliClient {
15
16
  private readonly command;
16
17
  private readonly args;
18
+ private readonly sessionStore?;
19
+ private readonly resolveSessionStoreAgentId;
17
20
  private readonly runtime;
18
21
  private readonly channels;
22
+ private readonly persistedSessions;
23
+ private loadedSessionStoreAgentId;
19
24
  private stopped;
20
- constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>);
25
+ private sessionStoreLoadPromise;
26
+ private sessionStoreWriteQueue;
27
+ constructor(command: string, args: string[], runtimeOverrides?: Partial<ClaudeCliRuntime>, sessionStore?: ClaudeChannelSessionStore | undefined, resolveSessionStoreAgentId?: () => string | undefined);
21
28
  generateReply(channelId: string, prompt: string, options?: ProviderGenerateOptions): Promise<string>;
22
29
  dispose(): Promise<void>;
23
30
  private getOrCreateChannelState;
24
31
  private processChannelQueue;
25
32
  private runTurn;
33
+ private runTurnAttempt;
34
+ private currentSessionStoreAgentId;
35
+ private ensureSessionStoreLoaded;
36
+ private hydrateChannelState;
37
+ private persistSession;
38
+ private persistSessionBestEffort;
39
+ private resetPersistedSession;
40
+ private resetPersistedSessionBestEffort;
41
+ private enqueueSessionStoreWrite;
26
42
  private run;
27
43
  }
28
44
  export {};
@@ -4,6 +4,10 @@ const DEFAULT_RUNTIME = {
4
4
  spawn,
5
5
  };
6
6
  const STREAM_JSON_ARGS = ['--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
7
+ function isStaleResumeFailure(error) {
8
+ const message = normalizeError(error).message.toLowerCase();
9
+ return message.includes('session not found') || message.includes('cannot resume');
10
+ }
7
11
  function normalizeError(error) {
8
12
  return error instanceof Error ? error : new Error(String(error));
9
13
  }
@@ -157,12 +161,20 @@ class ClaudeStreamCollector {
157
161
  export class ClaudeCliClient {
158
162
  command;
159
163
  args;
164
+ sessionStore;
165
+ resolveSessionStoreAgentId;
160
166
  runtime;
161
167
  channels = new Map();
168
+ persistedSessions = new Map();
169
+ loadedSessionStoreAgentId = null;
162
170
  stopped = false;
163
- constructor(command, args, runtimeOverrides = {}) {
171
+ sessionStoreLoadPromise = null;
172
+ sessionStoreWriteQueue = Promise.resolve();
173
+ constructor(command, args, runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
164
174
  this.command = command;
165
175
  this.args = args;
176
+ this.sessionStore = sessionStore;
177
+ this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
166
178
  this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
167
179
  }
168
180
  async generateReply(channelId, prompt, options) {
@@ -195,6 +207,7 @@ export class ClaudeCliClient {
195
207
  state = {
196
208
  processing: false,
197
209
  queue: [],
210
+ sessionHydrated: false,
198
211
  sessionEstablished: false,
199
212
  };
200
213
  this.channels.set(channelId, state);
@@ -209,13 +222,26 @@ export class ClaudeCliClient {
209
222
  void (async () => {
210
223
  try {
211
224
  while (!this.stopped) {
225
+ const queuedTurn = state.queue[0];
226
+ if (!queuedTurn) {
227
+ return;
228
+ }
229
+ try {
230
+ if (await this.ensureSessionStoreLoaded()) {
231
+ this.hydrateChannelState(channelId, state);
232
+ }
233
+ }
234
+ catch (error) {
235
+ state.queue.shift()?.reject(error);
236
+ continue;
237
+ }
212
238
  const turn = state.queue.shift();
213
239
  if (!turn) {
214
240
  return;
215
241
  }
216
242
  state.activeTurn = turn;
217
243
  try {
218
- const text = await this.runTurn(state, turn);
244
+ const text = await this.runTurn(channelId, state, turn);
219
245
  turn.resolve(text);
220
246
  }
221
247
  catch (error) {
@@ -234,16 +260,138 @@ export class ClaudeCliClient {
234
260
  }
235
261
  })();
236
262
  }
237
- async runTurn(state, turn) {
263
+ async runTurn(channelId, state, turn) {
264
+ return this.runTurnAttempt(channelId, state, turn, true);
265
+ }
266
+ async runTurnAttempt(channelId, state, turn, allowFreshRetryAfterStaleResume) {
238
267
  if (this.stopped) {
239
268
  throw new Error('Claude CLI backend stopped');
240
269
  }
241
270
  const sessionId = state.sessionEstablished ? state.sessionId : randomUUID();
242
271
  const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
243
- const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.options);
244
- state.sessionId = sessionId;
272
+ try {
273
+ const text = await this.run(state, [...this.args, ...sessionArgs, ...STREAM_JSON_ARGS], turn.prompt, turn.options);
274
+ state.sessionId = sessionId;
275
+ state.sessionEstablished = true;
276
+ await this.persistSessionBestEffort(channelId, sessionId);
277
+ return text;
278
+ }
279
+ catch (error) {
280
+ if (state.sessionEstablished && isStaleResumeFailure(error)) {
281
+ await this.resetPersistedSessionBestEffort(channelId, state);
282
+ if (allowFreshRetryAfterStaleResume) {
283
+ return this.runTurnAttempt(channelId, state, turn, false);
284
+ }
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+ currentSessionStoreAgentId() {
290
+ const agentId = this.resolveSessionStoreAgentId()?.trim();
291
+ return agentId && agentId.length > 0 ? agentId : null;
292
+ }
293
+ async ensureSessionStoreLoaded() {
294
+ if (!this.sessionStore) {
295
+ return true;
296
+ }
297
+ const agentId = this.currentSessionStoreAgentId();
298
+ if (!agentId) {
299
+ return false;
300
+ }
301
+ if (this.loadedSessionStoreAgentId && this.loadedSessionStoreAgentId !== agentId) {
302
+ this.loadedSessionStoreAgentId = null;
303
+ this.sessionStoreLoadPromise = null;
304
+ this.persistedSessions.clear();
305
+ for (const state of this.channels.values()) {
306
+ state.sessionHydrated = false;
307
+ }
308
+ }
309
+ if (!this.sessionStoreLoadPromise) {
310
+ this.sessionStoreLoadPromise = (async () => {
311
+ try {
312
+ const stored = await this.sessionStore.load(agentId);
313
+ this.persistedSessions.clear();
314
+ for (const [channelId, sessionId] of Object.entries(stored)) {
315
+ this.persistedSessions.set(channelId, sessionId);
316
+ }
317
+ this.loadedSessionStoreAgentId = agentId;
318
+ }
319
+ catch (error) {
320
+ this.sessionStoreLoadPromise = null;
321
+ throw error;
322
+ }
323
+ })();
324
+ }
325
+ await this.sessionStoreLoadPromise;
326
+ return true;
327
+ }
328
+ hydrateChannelState(channelId, state) {
329
+ if (state.sessionHydrated) {
330
+ return;
331
+ }
332
+ state.sessionHydrated = true;
333
+ const persistedSessionId = this.persistedSessions.get(channelId);
334
+ if (!persistedSessionId) {
335
+ return;
336
+ }
337
+ state.sessionId = persistedSessionId;
245
338
  state.sessionEstablished = true;
246
- return text;
339
+ }
340
+ async persistSession(channelId, sessionId) {
341
+ if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
342
+ return;
343
+ }
344
+ await this.enqueueSessionStoreWrite(async () => {
345
+ if (this.persistedSessions.get(channelId) === sessionId) {
346
+ return;
347
+ }
348
+ this.persistedSessions.set(channelId, sessionId);
349
+ await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
350
+ });
351
+ }
352
+ async persistSessionBestEffort(channelId, sessionId) {
353
+ try {
354
+ await this.persistSession(channelId, sessionId);
355
+ }
356
+ catch (error) {
357
+ console.error('[agents-host] failed to persist Claude session map; keeping reply delivery', {
358
+ agentId: this.loadedSessionStoreAgentId,
359
+ channelId,
360
+ sessionId,
361
+ error,
362
+ });
363
+ }
364
+ }
365
+ async resetPersistedSession(channelId, state) {
366
+ state.sessionEstablished = false;
367
+ state.sessionId = undefined;
368
+ if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
369
+ return;
370
+ }
371
+ await this.enqueueSessionStoreWrite(async () => {
372
+ if (!this.persistedSessions.has(channelId)) {
373
+ return;
374
+ }
375
+ this.persistedSessions.delete(channelId);
376
+ await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries(this.persistedSessions));
377
+ });
378
+ }
379
+ async resetPersistedSessionBestEffort(channelId, state) {
380
+ try {
381
+ await this.resetPersistedSession(channelId, state);
382
+ }
383
+ catch (error) {
384
+ console.error('[agents-host] failed to clear stale Claude session map; retrying in-memory only', {
385
+ agentId: this.loadedSessionStoreAgentId,
386
+ channelId,
387
+ error,
388
+ });
389
+ }
390
+ }
391
+ async enqueueSessionStoreWrite(writeOperation) {
392
+ const queuedWrite = this.sessionStoreWriteQueue.then(writeOperation);
393
+ this.sessionStoreWriteQueue = queuedWrite.catch(() => undefined);
394
+ await queuedWrite;
247
395
  }
248
396
  async run(state, args, prompt, options) {
249
397
  return new Promise((resolve, reject) => {
@@ -0,0 +1,37 @@
1
+ export interface ClaudeChannelSessionStore {
2
+ load(agentId: string): Promise<Record<string, string>>;
3
+ save(agentId: string, sessions: Record<string, string>): Promise<void>;
4
+ }
5
+ export interface FileClaudeChannelSessionStoreOptions {
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>;
29
+ }
30
+ export declare class FileClaudeChannelSessionStore implements ClaudeChannelSessionStore {
31
+ private readonly options;
32
+ constructor(options: FileClaudeChannelSessionStoreOptions);
33
+ private loadFromPath;
34
+ load(agentId: string): Promise<Record<string, string>>;
35
+ save(agentId: string, sessions: Record<string, string>): Promise<void>;
36
+ }
37
+ export {};