@love-moon/conductor-sdk 0.2.13 → 0.2.15
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/dist/backend/client.d.ts +17 -1
- package/dist/backend/client.js +15 -2
- package/dist/client.d.ts +3 -1
- package/dist/client.js +112 -10
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +10 -0
- package/dist/session/store.d.ts +12 -0
- package/dist/session/store.js +129 -21
- package/package.json +3 -2
package/dist/backend/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/backend/client.js
CHANGED
|
@@ -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
|
|
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
|
|
79
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
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:
|
|
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
|
package/dist/config/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/config/index.js
CHANGED
|
@@ -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';
|
package/dist/session/store.d.ts
CHANGED
|
@@ -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;
|
package/dist/session/store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
record
|
|
139
|
-
if (!record
|
|
140
|
-
record
|
|
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.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"@types/ws": "^8.5.12",
|
|
27
27
|
"typescript": "^5.6.3",
|
|
28
28
|
"vitest": "^2.1.4"
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"gitCommitId": "e500981"
|
|
30
31
|
}
|