@love-moon/conductor-sdk 0.2.13 → 0.2.14

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.
@@ -22,9 +22,12 @@ export declare class TaskSummary {
22
22
  readonly projectId: string | null;
23
23
  readonly title: string;
24
24
  readonly status: string;
25
+ readonly backendType?: string | null | undefined;
26
+ readonly sessionId?: string | null | undefined;
27
+ readonly sessionFilePath?: string | null | undefined;
25
28
  readonly createdAt?: string | null | undefined;
26
29
  readonly updatedAt?: string | null | undefined;
27
- constructor(id: string, projectId: string | null, title: string, status: string, createdAt?: string | null | undefined, updatedAt?: string | null | undefined);
30
+ constructor(id: string, projectId: string | null, title: string, status: string, backendType?: string | null | undefined, sessionId?: string | null | undefined, sessionFilePath?: string | null | undefined, createdAt?: string | null | undefined, updatedAt?: string | null | undefined);
28
31
  static fromJSON(payload: Record<string, any>): TaskSummary;
29
32
  }
30
33
  export declare class BackendApiClient {
@@ -48,8 +51,21 @@ export declare class BackendApiClient {
48
51
  projectId: string;
49
52
  title: string;
50
53
  backendType?: string;
54
+ sessionId?: string | null;
55
+ sessionFilePath?: string | null;
51
56
  initialContent?: string;
52
57
  agentHost?: string;
58
+ metadata?: Record<string, unknown>;
59
+ }): Promise<TaskSummary>;
60
+ updateTask(taskId: string, params: {
61
+ projectId?: string;
62
+ title?: string;
63
+ status?: string;
64
+ agentHost?: string | null;
65
+ metadata?: Record<string, unknown> | null;
66
+ backendType?: string | null;
67
+ sessionId?: string | null;
68
+ sessionFilePath?: string | null;
53
69
  }): Promise<TaskSummary>;
54
70
  createMessage(params: {
55
71
  taskId: string;
@@ -36,13 +36,19 @@ export class TaskSummary {
36
36
  projectId;
37
37
  title;
38
38
  status;
39
+ backendType;
40
+ sessionId;
41
+ sessionFilePath;
39
42
  createdAt;
40
43
  updatedAt;
41
- constructor(id, projectId, title, status, createdAt, updatedAt) {
44
+ constructor(id, projectId, title, status, backendType, sessionId, sessionFilePath, createdAt, updatedAt) {
42
45
  this.id = id;
43
46
  this.projectId = projectId;
44
47
  this.title = title;
45
48
  this.status = status;
49
+ this.backendType = backendType;
50
+ this.sessionId = sessionId;
51
+ this.sessionFilePath = sessionFilePath;
46
52
  this.createdAt = createdAt;
47
53
  this.updatedAt = updatedAt;
48
54
  }
@@ -56,7 +62,7 @@ export class TaskSummary {
56
62
  if (!title || !status) {
57
63
  throw new Error('Task payload missing required fields');
58
64
  }
59
- return new TaskSummary(id, payload.project_id ? String(payload.project_id) : null, title, status, payload.created_at ?? null, payload.updated_at ?? null);
65
+ return new TaskSummary(id, payload.project_id ? String(payload.project_id) : null, title, status, payload.backend_type ?? payload.backendType ?? null, payload.session_id ?? payload.sessionId ?? null, payload.session_file_path ?? payload.sessionFilePath ?? null, payload.created_at ?? null, payload.updated_at ?? null);
60
66
  }
61
67
  }
62
68
  export class BackendApiClient {
@@ -133,6 +139,13 @@ export class BackendApiClient {
133
139
  const payload = await this.parseJson(response);
134
140
  return TaskSummary.fromJSON(payload);
135
141
  }
142
+ async updateTask(taskId, params) {
143
+ const response = await this.request('PATCH', `/tasks/${taskId}`, {
144
+ body: JSON.stringify(params),
145
+ });
146
+ const payload = await this.parseJson(response);
147
+ return TaskSummary.fromJSON(payload);
148
+ }
136
149
  async createMessage(params) {
137
150
  const response = await this.request('POST', `/tasks/${params.taskId}/messages`, {
138
151
  body: JSON.stringify(params),
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import { ConductorConfig } from './config/index.js';
3
3
  import { MessageRouter } from './message/index.js';
4
4
  import { SessionDiskStore, SessionManager } from './session/index.js';
5
5
  import { ConductorWebSocketClient } from './ws/index.js';
6
- type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
6
+ type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'updateTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
7
7
  type RealtimeClientLike = Pick<ConductorWebSocketClient, 'registerHandler' | 'connect' | 'disconnect' | 'sendJson'>;
8
8
  export interface ConductorClientConnectOptions {
9
9
  config?: ConductorConfig;
@@ -81,6 +81,8 @@ export declare class ConductorClient {
81
81
  createProject(name: string, description?: string, metadata?: Record<string, unknown>): Promise<Record<string, any>>;
82
82
  listTasks(payload?: Record<string, any>): Promise<Record<string, any>>;
83
83
  getLocalProjectRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
84
+ getLocalTaskRecord(payload?: Record<string, any>): Promise<Record<string, any>>;
85
+ bindTaskSession(taskId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
84
86
  matchProjectByPath(payload?: Record<string, any>): Promise<Record<string, any>>;
85
87
  bindProjectPath(projectId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
86
88
  private readonly handleBackendEvent;
package/dist/client.js CHANGED
@@ -3,7 +3,7 @@ import { BackendApiClient } from './backend/index.js';
3
3
  import { loadConfig } from './config/index.js';
4
4
  import { getPlanLimitMessageFromError } from './limits/index.js';
5
5
  import { MessageRouter } from './message/index.js';
6
- import { SessionDiskStore, SessionManager, currentHostname, currentSessionId } from './session/index.js';
6
+ import { SessionDiskStore, SessionManager, currentHostname } from './session/index.js';
7
7
  import { ConductorWebSocketClient } from './ws/index.js';
8
8
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
9
  export class ConductorClient {
@@ -75,19 +75,42 @@ export class ConductorClient {
75
75
  }
76
76
  const title = String(payload.task_title || 'Untitled');
77
77
  const taskId = String(payload.task_id || safeRandomUuid());
78
- const sessionId = String(payload.session_id || taskId);
79
- await this.sessions.addSession(taskId, sessionId, projectId);
78
+ const explicitSessionId = typeof payload.session_id === 'string' && payload.session_id.trim()
79
+ ? payload.session_id.trim()
80
+ : null;
81
+ const explicitSessionFilePath = typeof payload.session_file_path === 'string' && payload.session_file_path.trim()
82
+ ? payload.session_file_path.trim()
83
+ : typeof payload.sessionFilePath === 'string' && payload.sessionFilePath.trim()
84
+ ? payload.sessionFilePath.trim()
85
+ : null;
86
+ const explicitDaemonName = typeof payload.daemon_name === 'string' && payload.daemon_name.trim()
87
+ ? payload.daemon_name.trim()
88
+ : typeof payload.daemonName === 'string' && payload.daemonName.trim()
89
+ ? payload.daemonName.trim()
90
+ : null;
91
+ const backendType = typeof payload.backend_type === 'string'
92
+ ? payload.backend_type
93
+ : typeof payload.backendType === 'string'
94
+ ? payload.backendType
95
+ : undefined;
96
+ const metadata = payload.metadata && typeof payload.metadata === 'object' && !Array.isArray(payload.metadata)
97
+ ? { ...payload.metadata }
98
+ : {};
99
+ if (explicitDaemonName && metadata.daemonName === undefined) {
100
+ metadata.daemonName = explicitDaemonName;
101
+ }
102
+ const logicalSessionId = explicitSessionId || taskId;
103
+ await this.sessions.addSession(taskId, logicalSessionId, projectId);
80
104
  try {
81
105
  await this.backendApi.createTask({
82
106
  id: taskId,
83
107
  projectId,
84
108
  title,
85
- backendType: typeof payload.backend_type === 'string'
86
- ? payload.backend_type
87
- : typeof payload.backendType === 'string'
88
- ? payload.backendType
89
- : undefined,
109
+ backendType,
110
+ sessionId: logicalSessionId,
111
+ sessionFilePath: explicitSessionFilePath,
90
112
  initialContent: typeof payload.prefill === 'string' ? payload.prefill : undefined,
113
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
91
114
  agentHost: typeof payload.agent_host === 'string'
92
115
  ? payload.agent_host
93
116
  : typeof payload.agentHost === 'string'
@@ -110,12 +133,14 @@ export class ConductorClient {
110
133
  projectId,
111
134
  taskId,
112
135
  projectPath,
113
- sessionId: currentSessionId(this.env),
136
+ sessionId: logicalSessionId,
137
+ sessionFilePath: explicitSessionFilePath,
138
+ backendType,
114
139
  hostname: this.resolveHostname(),
115
140
  });
116
141
  return {
117
142
  task_id: taskId,
118
- session_id: sessionId,
143
+ session_id: logicalSessionId,
119
144
  app_url: payload.app_url,
120
145
  };
121
146
  }
@@ -253,6 +278,9 @@ export class ConductorClient {
253
278
  project_id: task.project_id ?? task.projectId ?? null,
254
279
  title: task.title,
255
280
  status: task.status,
281
+ backend_type: task.backend_type ?? task.backendType ?? null,
282
+ session_id: task.session_id ?? task.sessionId ?? null,
283
+ session_file_path: task.session_file_path ?? task.sessionFilePath ?? null,
256
284
  created_at: task.created_at ?? task.createdAt ?? null,
257
285
  updated_at: task.updated_at ?? task.updatedAt ?? null,
258
286
  })),
@@ -273,6 +301,80 @@ export class ConductorClient {
273
301
  hostname: record.hostname,
274
302
  };
275
303
  }
304
+ async getLocalTaskRecord(payload = {}) {
305
+ const taskId = typeof payload.task_id === 'string' ? payload.task_id.trim() : '';
306
+ if (!taskId) {
307
+ throw new Error('task_id is required');
308
+ }
309
+ const record = this.sessionStore.findByTaskId(taskId);
310
+ if (!record) {
311
+ throw new Error(`No session record found for task ${taskId}`);
312
+ }
313
+ return {
314
+ project_id: record.projectId,
315
+ task_id: taskId,
316
+ task_ids: Array.from(record.taskIds),
317
+ project_path: record.projectPath,
318
+ session_id: record.sessionId ?? null,
319
+ session_file_path: record.sessionFilePath ?? null,
320
+ backend_type: record.backendType ?? null,
321
+ hostname: record.hostname,
322
+ };
323
+ }
324
+ async bindTaskSession(taskId, payload = {}) {
325
+ const normalizedTaskId = String(taskId || '').trim();
326
+ if (!normalizedTaskId) {
327
+ throw new Error('task_id is required');
328
+ }
329
+ const existing = this.sessionStore.findByTaskId(normalizedTaskId);
330
+ const inMemorySession = await this.sessions.getSession(normalizedTaskId);
331
+ const projectId = existing?.projectId ||
332
+ inMemorySession?.projectId ||
333
+ (typeof payload.project_id === 'string' && payload.project_id.trim() ? payload.project_id.trim() : 'unknown-project');
334
+ const projectPath = existing?.projectPath ||
335
+ (typeof payload.project_path === 'string' && payload.project_path
336
+ ? payload.project_path
337
+ : this.projectPath);
338
+ const record = this.sessionStore.upsert({
339
+ projectId,
340
+ taskId: normalizedTaskId,
341
+ projectPath,
342
+ sessionId: typeof payload.session_id === 'string'
343
+ ? payload.session_id
344
+ : payload.session_id === null
345
+ ? null
346
+ : existing?.sessionId ?? null,
347
+ sessionFilePath: typeof payload.session_file_path === 'string'
348
+ ? payload.session_file_path
349
+ : payload.session_file_path === null
350
+ ? null
351
+ : existing?.sessionFilePath ?? null,
352
+ backendType: typeof payload.backend_type === 'string'
353
+ ? payload.backend_type
354
+ : payload.backend_type === null
355
+ ? null
356
+ : existing?.backendType ?? null,
357
+ hostname: typeof payload.hostname === 'string'
358
+ ? payload.hostname
359
+ : existing?.hostname ?? this.resolveHostname(),
360
+ });
361
+ if (typeof this.backendApi.updateTask === 'function') {
362
+ await this.backendApi.updateTask(normalizedTaskId, {
363
+ backendType: record.backendType ?? null,
364
+ sessionId: record.sessionId ?? null,
365
+ sessionFilePath: record.sessionFilePath ?? null,
366
+ });
367
+ }
368
+ return {
369
+ project_id: record.projectId,
370
+ task_id: normalizedTaskId,
371
+ session_id: record.sessionId ?? null,
372
+ session_file_path: record.sessionFilePath ?? null,
373
+ backend_type: record.backendType ?? null,
374
+ project_path: record.projectPath,
375
+ hostname: record.hostname,
376
+ };
377
+ }
276
378
  async matchProjectByPath(payload = {}) {
277
379
  const hostname = typeof payload.hostname === 'string' ? payload.hostname : currentHostname();
278
380
  const projectPath = typeof payload.project_path === 'string' && payload.project_path
@@ -18,12 +18,14 @@ export interface ConductorConfigInit {
18
18
  backendUrl: string;
19
19
  websocketUrl?: string;
20
20
  logLevel?: string;
21
+ daemonName?: string;
21
22
  }
22
23
  export declare class ConductorConfig {
23
24
  readonly agentToken: string;
24
25
  readonly backendUrl: string;
25
26
  readonly websocketUrl?: string;
26
27
  readonly logLevel: string;
28
+ readonly daemonName?: string;
27
29
  constructor(init: ConductorConfigInit);
28
30
  get resolvedWebsocketUrl(): string;
29
31
  }
@@ -30,11 +30,13 @@ export class ConductorConfig {
30
30
  backendUrl;
31
31
  websocketUrl;
32
32
  logLevel;
33
+ daemonName;
33
34
  constructor(init) {
34
35
  this.agentToken = init.agentToken;
35
36
  this.backendUrl = init.backendUrl;
36
37
  this.websocketUrl = init.websocketUrl;
37
38
  this.logLevel = (init.logLevel || 'info').toLowerCase();
39
+ this.daemonName = normalizeOptionalString(init.daemonName);
38
40
  }
39
41
  get resolvedWebsocketUrl() {
40
42
  if (this.websocketUrl) {
@@ -88,6 +90,7 @@ export function loadConfig(targetPath, options = {}) {
88
90
  backendUrl: backendUrl,
89
91
  websocketUrl,
90
92
  logLevel: logLevel,
93
+ daemonName: normalizeOptionalString(merged.daemon_name),
91
94
  });
92
95
  }
93
96
  function resolveConfigPath(explicitPath, env) {
@@ -143,6 +146,13 @@ function normalizeToken(value) {
143
146
  const trimmed = value.trim();
144
147
  return trimmed || undefined;
145
148
  }
149
+ function normalizeOptionalString(value) {
150
+ if (typeof value !== 'string') {
151
+ return undefined;
152
+ }
153
+ const trimmed = value.trim();
154
+ return trimmed || undefined;
155
+ }
146
156
  function normalizeLogLevel(value) {
147
157
  if (typeof value !== 'string') {
148
158
  return 'info';
@@ -7,6 +7,8 @@ export interface SessionRecordInit {
7
7
  taskIds: string[];
8
8
  projectPath: string;
9
9
  sessionId?: string | null;
10
+ sessionFilePath?: string | null;
11
+ backendType?: string | null;
10
12
  hostname?: string | null;
11
13
  }
12
14
  export declare class SessionRecord {
@@ -14,6 +16,8 @@ export declare class SessionRecord {
14
16
  taskIds: string[];
15
17
  projectPath: string;
16
18
  sessionId?: string | null;
19
+ sessionFilePath?: string | null;
20
+ backendType?: string | null;
17
21
  hostname?: string | null;
18
22
  constructor(init: SessionRecordInit);
19
23
  static fromJSON(payload: Record<string, any>): SessionRecord;
@@ -21,6 +25,7 @@ export declare class SessionRecord {
21
25
  }
22
26
  export declare class SessionDiskStore {
23
27
  private readonly filePath;
28
+ private readonly lockPath;
24
29
  constructor(filePath?: string);
25
30
  /**
26
31
  * Create a SessionDiskStore for a specific backend URL.
@@ -28,15 +33,22 @@ export declare class SessionDiskStore {
28
33
  */
29
34
  static forBackendUrl(backendUrl: string): SessionDiskStore;
30
35
  load(): SessionRecord[];
36
+ private loadUnlocked;
31
37
  save(records: SessionRecord[]): void;
38
+ private saveUnlocked;
32
39
  findByPath(projectPath: string): SessionRecord | undefined;
40
+ findByTaskId(taskId: string): SessionRecord | undefined;
33
41
  upsert(params: {
34
42
  projectId: string;
35
43
  taskId: string;
36
44
  projectPath: string;
37
45
  sessionId?: string | null;
46
+ sessionFilePath?: string | null;
47
+ backendType?: string | null;
38
48
  hostname?: string | null;
39
49
  }): SessionRecord;
50
+ private withLock;
51
+ private acquireLock;
40
52
  }
41
53
  export declare function currentSessionId(env?: Record<string, string | undefined>): string | undefined;
42
54
  export declare function currentHostname(): string;
@@ -6,17 +6,38 @@ export const DEFAULT_SESSION_DIR = path.join(os.homedir(), '.conductor', 'sessio
6
6
  export const DEFAULT_SESSION_PATH = path.join(os.homedir(), '.conductor', 'session.yaml');
7
7
  export const DEFAULT_SESSION_ENV = 'CODEX_SESSION_ID';
8
8
  export const DEFAULT_SESSION_FALLBACK_ENV = 'SESSION_ID';
9
+ const SESSION_LOCK_TIMEOUT_MS = 10_000;
10
+ const SESSION_LOCK_RETRY_MS = 50;
11
+ const sleepSync = (ms) => {
12
+ if (ms <= 0)
13
+ return;
14
+ try {
15
+ const buffer = new SharedArrayBuffer(4);
16
+ const arr = new Int32Array(buffer);
17
+ Atomics.wait(arr, 0, 0, ms);
18
+ }
19
+ catch {
20
+ const startedAt = Date.now();
21
+ while (Date.now() - startedAt < ms) {
22
+ // busy wait fallback
23
+ }
24
+ }
25
+ };
9
26
  export class SessionRecord {
10
27
  projectId;
11
28
  taskIds;
12
29
  projectPath;
13
30
  sessionId;
31
+ sessionFilePath;
32
+ backendType;
14
33
  hostname;
15
34
  constructor(init) {
16
35
  this.projectId = init.projectId;
17
36
  this.taskIds = init.taskIds;
18
37
  this.projectPath = init.projectPath;
19
38
  this.sessionId = init.sessionId ?? null;
39
+ this.sessionFilePath = init.sessionFilePath ?? null;
40
+ this.backendType = init.backendType ?? null;
20
41
  this.hostname = init.hostname ?? null;
21
42
  }
22
43
  static fromJSON(payload) {
@@ -32,12 +53,16 @@ export class SessionRecord {
32
53
  ? [rawTasks]
33
54
  : [];
34
55
  const sessionId = payload.session_id ? String(payload.session_id) : null;
56
+ const sessionFilePath = payload.session_file_path ? String(payload.session_file_path) : null;
57
+ const backendType = payload.backend_type ? String(payload.backend_type) : null;
35
58
  const hostname = payload.hostname ? String(payload.hostname) : null;
36
59
  return new SessionRecord({
37
60
  projectId,
38
61
  projectPath,
39
62
  taskIds,
40
63
  sessionId,
64
+ sessionFilePath,
65
+ backendType,
41
66
  hostname,
42
67
  });
43
68
  }
@@ -47,14 +72,18 @@ export class SessionRecord {
47
72
  task_id: Array.from(this.taskIds),
48
73
  project_path: this.projectPath,
49
74
  session_id: this.sessionId,
75
+ session_file_path: this.sessionFilePath,
76
+ backend_type: this.backendType,
50
77
  hostname: this.hostname,
51
78
  };
52
79
  }
53
80
  }
54
81
  export class SessionDiskStore {
55
82
  filePath;
83
+ lockPath;
56
84
  constructor(filePath = DEFAULT_SESSION_PATH) {
57
85
  this.filePath = path.resolve(filePath);
86
+ this.lockPath = `${this.filePath}.lock`;
58
87
  }
59
88
  /**
60
89
  * Create a SessionDiskStore for a specific backend URL.
@@ -66,6 +95,9 @@ export class SessionDiskStore {
66
95
  return new SessionDiskStore(filePath);
67
96
  }
68
97
  load() {
98
+ return this.withLock(() => this.loadUnlocked());
99
+ }
100
+ loadUnlocked() {
69
101
  if (!fs.existsSync(this.filePath)) {
70
102
  return [];
71
103
  }
@@ -108,40 +140,116 @@ export class SessionDiskStore {
108
140
  }
109
141
  }
110
142
  save(records) {
143
+ this.withLock(() => this.saveUnlocked(records));
144
+ }
145
+ saveUnlocked(records) {
111
146
  const payload = {
112
147
  sessions: records.map((record) => record.toJSON()),
113
148
  };
114
149
  fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
115
- fs.writeFileSync(this.filePath, yaml.stringify(payload), 'utf-8');
150
+ const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
151
+ fs.writeFileSync(tempPath, yaml.stringify(payload), 'utf-8');
152
+ fs.renameSync(tempPath, this.filePath);
116
153
  }
117
154
  findByPath(projectPath) {
118
155
  const normalized = path.resolve(projectPath);
119
156
  return this.load().find((record) => path.resolve(record.projectPath) === normalized);
120
157
  }
158
+ findByTaskId(taskId) {
159
+ const normalizedTaskId = String(taskId || '').trim();
160
+ if (!normalizedTaskId) {
161
+ return undefined;
162
+ }
163
+ return this.load().find((record) => record.taskIds.includes(normalizedTaskId));
164
+ }
121
165
  upsert(params) {
122
- const normalized = path.resolve(params.projectPath);
123
- const records = this.load();
124
- let record = records.find((entry) => path.resolve(entry.projectPath) === normalized);
125
- if (!record) {
126
- record = new SessionRecord({
127
- projectId: params.projectId,
128
- taskIds: [params.taskId],
129
- projectPath: normalized,
130
- sessionId: params.sessionId,
131
- hostname: params.hostname,
132
- });
133
- records.push(record);
166
+ const normalizedTaskId = String(params.taskId || '').trim();
167
+ if (!normalizedTaskId) {
168
+ throw new Error('taskId is required');
134
169
  }
135
- else {
136
- record.projectId = params.projectId;
137
- record.sessionId = params.sessionId ?? record.sessionId;
138
- record.hostname = params.hostname ?? record.hostname;
139
- if (!record.taskIds.includes(params.taskId)) {
140
- record.taskIds.push(params.taskId);
170
+ const normalizedPath = path.resolve(params.projectPath);
171
+ return this.withLock(() => {
172
+ const records = this.loadUnlocked();
173
+ let record = records.find((entry) => entry.taskIds.includes(normalizedTaskId));
174
+ if (!record) {
175
+ record = new SessionRecord({
176
+ projectId: params.projectId,
177
+ taskIds: [normalizedTaskId],
178
+ projectPath: normalizedPath,
179
+ sessionId: params.sessionId ?? null,
180
+ sessionFilePath: params.sessionFilePath ?? null,
181
+ backendType: params.backendType ?? null,
182
+ hostname: params.hostname,
183
+ });
184
+ records.push(record);
185
+ }
186
+ else {
187
+ record.projectId = params.projectId;
188
+ record.projectPath = normalizedPath;
189
+ if (!record.taskIds.includes(normalizedTaskId)) {
190
+ record.taskIds.push(normalizedTaskId);
191
+ }
192
+ if (params.sessionId !== undefined) {
193
+ const normalizedSessionId = String(params.sessionId || '').trim();
194
+ record.sessionId = normalizedSessionId || null;
195
+ }
196
+ if (params.sessionFilePath !== undefined) {
197
+ const normalizedSessionFilePath = String(params.sessionFilePath || '').trim();
198
+ record.sessionFilePath = normalizedSessionFilePath || null;
199
+ }
200
+ if (params.backendType !== undefined) {
201
+ const normalizedBackendType = String(params.backendType || '').trim();
202
+ record.backendType = normalizedBackendType || null;
203
+ }
204
+ if (params.hostname !== undefined) {
205
+ const normalizedHostname = String(params.hostname || '').trim();
206
+ record.hostname = normalizedHostname || null;
207
+ }
208
+ }
209
+ this.saveUnlocked(records);
210
+ return record;
211
+ });
212
+ }
213
+ withLock(fn) {
214
+ const release = this.acquireLock();
215
+ try {
216
+ return fn();
217
+ }
218
+ finally {
219
+ release();
220
+ }
221
+ }
222
+ acquireLock() {
223
+ const startedAt = Date.now();
224
+ while (true) {
225
+ try {
226
+ const fd = fs.openSync(this.lockPath, 'wx');
227
+ return () => {
228
+ try {
229
+ fs.closeSync(fd);
230
+ }
231
+ catch {
232
+ // ignore close failure
233
+ }
234
+ try {
235
+ fs.unlinkSync(this.lockPath);
236
+ }
237
+ catch {
238
+ // ignore unlink failure
239
+ }
240
+ };
241
+ }
242
+ catch (error) {
243
+ const code = error.code;
244
+ if (code !== 'EEXIST') {
245
+ throw error;
246
+ }
247
+ if (Date.now() - startedAt > SESSION_LOCK_TIMEOUT_MS) {
248
+ throw new Error(`Timed out waiting for session store lock: ${this.lockPath}`);
249
+ }
250
+ sleepSync(SESSION_LOCK_RETRY_MS);
141
251
  }
142
252
  }
143
- this.save(records);
144
- return record;
145
253
  }
146
254
  }
147
255
  export function currentSessionId(env = process.env) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",