@love-moon/conductor-sdk 0.2.16 → 0.2.17
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 +57 -0
- package/dist/backend/client.js +102 -0
- package/dist/client.d.ts +32 -23
- package/dist/client.js +340 -173
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/outbox/downstream-cursor-store.d.ts +16 -0
- package/dist/outbox/downstream-cursor-store.js +96 -0
- package/dist/outbox/index.d.ts +2 -0
- package/dist/outbox/index.js +2 -0
- package/dist/outbox/store.d.ts +31 -0
- package/dist/outbox/store.js +174 -0
- package/package.json +2 -2
package/dist/backend/client.d.ts
CHANGED
|
@@ -4,6 +4,30 @@ export interface BackendClientOptions {
|
|
|
4
4
|
fetchImpl?: FetchFn;
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
}
|
|
7
|
+
export type AgentCommitEvent = {
|
|
8
|
+
eventType: 'sdk_message';
|
|
9
|
+
taskId: string;
|
|
10
|
+
content: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
messageId?: string | null;
|
|
13
|
+
} | {
|
|
14
|
+
eventType: 'task_status_update';
|
|
15
|
+
taskId: string;
|
|
16
|
+
status: string;
|
|
17
|
+
summary?: string | null;
|
|
18
|
+
statusEventId?: string | null;
|
|
19
|
+
} | {
|
|
20
|
+
eventType: 'agent_command_ack';
|
|
21
|
+
requestId: string;
|
|
22
|
+
taskId?: string | null;
|
|
23
|
+
accepted?: boolean;
|
|
24
|
+
commandEventType?: string | null;
|
|
25
|
+
} | {
|
|
26
|
+
eventType: 'task_stop_ack';
|
|
27
|
+
taskId: string;
|
|
28
|
+
requestId: string;
|
|
29
|
+
accepted?: boolean;
|
|
30
|
+
};
|
|
7
31
|
export declare class BackendApiError extends Error {
|
|
8
32
|
readonly statusCode?: number | undefined;
|
|
9
33
|
readonly details?: unknown | undefined;
|
|
@@ -73,6 +97,39 @@ export declare class BackendApiClient {
|
|
|
73
97
|
content: string;
|
|
74
98
|
metadata?: Record<string, unknown>;
|
|
75
99
|
}): Promise<any>;
|
|
100
|
+
commitAgentEvents(params: {
|
|
101
|
+
agentHost: string;
|
|
102
|
+
events: AgentCommitEvent[];
|
|
103
|
+
}): Promise<{
|
|
104
|
+
results: Array<Record<string, unknown>>;
|
|
105
|
+
}>;
|
|
106
|
+
commitSdkMessage(params: {
|
|
107
|
+
agentHost: string;
|
|
108
|
+
taskId: string;
|
|
109
|
+
content: string;
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
messageId?: string | null;
|
|
112
|
+
}): Promise<Record<string, unknown>>;
|
|
113
|
+
commitTaskStatusUpdate(params: {
|
|
114
|
+
agentHost: string;
|
|
115
|
+
taskId: string;
|
|
116
|
+
status: string;
|
|
117
|
+
summary?: string | null;
|
|
118
|
+
statusEventId?: string | null;
|
|
119
|
+
}): Promise<Record<string, unknown>>;
|
|
120
|
+
commitAgentCommandAck(params: {
|
|
121
|
+
agentHost: string;
|
|
122
|
+
requestId: string;
|
|
123
|
+
taskId?: string | null;
|
|
124
|
+
accepted?: boolean;
|
|
125
|
+
commandEventType?: string | null;
|
|
126
|
+
}): Promise<Record<string, unknown>>;
|
|
127
|
+
commitTaskStopAck(params: {
|
|
128
|
+
agentHost: string;
|
|
129
|
+
taskId: string;
|
|
130
|
+
requestId: string;
|
|
131
|
+
accepted?: boolean;
|
|
132
|
+
}): Promise<Record<string, unknown>>;
|
|
76
133
|
matchProjectByPath(params: {
|
|
77
134
|
hostname: string;
|
|
78
135
|
path: string;
|
package/dist/backend/client.js
CHANGED
|
@@ -152,6 +152,108 @@ export class BackendApiClient {
|
|
|
152
152
|
});
|
|
153
153
|
return this.parseJson(response);
|
|
154
154
|
}
|
|
155
|
+
async commitAgentEvents(params) {
|
|
156
|
+
const response = await this.request('POST', '/agent/events', {
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
agent_host: params.agentHost,
|
|
159
|
+
events: params.events.map((event) => {
|
|
160
|
+
if (event.eventType === 'sdk_message') {
|
|
161
|
+
return {
|
|
162
|
+
event_type: event.eventType,
|
|
163
|
+
task_id: event.taskId,
|
|
164
|
+
content: event.content,
|
|
165
|
+
metadata: event.metadata,
|
|
166
|
+
message_id: event.messageId ?? undefined,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (event.eventType === 'task_status_update') {
|
|
170
|
+
return {
|
|
171
|
+
event_type: event.eventType,
|
|
172
|
+
task_id: event.taskId,
|
|
173
|
+
status: event.status,
|
|
174
|
+
summary: event.summary ?? undefined,
|
|
175
|
+
status_event_id: event.statusEventId ?? undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (event.eventType === 'task_stop_ack') {
|
|
179
|
+
return {
|
|
180
|
+
event_type: event.eventType,
|
|
181
|
+
task_id: event.taskId,
|
|
182
|
+
request_id: event.requestId,
|
|
183
|
+
accepted: event.accepted !== false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
event_type: event.eventType,
|
|
188
|
+
request_id: event.requestId,
|
|
189
|
+
task_id: event.taskId ?? undefined,
|
|
190
|
+
accepted: event.accepted !== false,
|
|
191
|
+
command_event_type: event.commandEventType ?? undefined,
|
|
192
|
+
};
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
return this.parseJson(response);
|
|
197
|
+
}
|
|
198
|
+
async commitSdkMessage(params) {
|
|
199
|
+
const payload = await this.commitAgentEvents({
|
|
200
|
+
agentHost: params.agentHost,
|
|
201
|
+
events: [
|
|
202
|
+
{
|
|
203
|
+
eventType: 'sdk_message',
|
|
204
|
+
taskId: params.taskId,
|
|
205
|
+
content: params.content,
|
|
206
|
+
metadata: params.metadata,
|
|
207
|
+
messageId: params.messageId,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
return (Array.isArray(payload.results) ? payload.results[0] : null) ?? {};
|
|
212
|
+
}
|
|
213
|
+
async commitTaskStatusUpdate(params) {
|
|
214
|
+
const payload = await this.commitAgentEvents({
|
|
215
|
+
agentHost: params.agentHost,
|
|
216
|
+
events: [
|
|
217
|
+
{
|
|
218
|
+
eventType: 'task_status_update',
|
|
219
|
+
taskId: params.taskId,
|
|
220
|
+
status: params.status,
|
|
221
|
+
summary: params.summary,
|
|
222
|
+
statusEventId: params.statusEventId,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
return (Array.isArray(payload.results) ? payload.results[0] : null) ?? {};
|
|
227
|
+
}
|
|
228
|
+
async commitAgentCommandAck(params) {
|
|
229
|
+
const payload = await this.commitAgentEvents({
|
|
230
|
+
agentHost: params.agentHost,
|
|
231
|
+
events: [
|
|
232
|
+
{
|
|
233
|
+
eventType: 'agent_command_ack',
|
|
234
|
+
requestId: params.requestId,
|
|
235
|
+
taskId: params.taskId,
|
|
236
|
+
accepted: params.accepted,
|
|
237
|
+
commandEventType: params.commandEventType,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
return (Array.isArray(payload.results) ? payload.results[0] : null) ?? {};
|
|
242
|
+
}
|
|
243
|
+
async commitTaskStopAck(params) {
|
|
244
|
+
const payload = await this.commitAgentEvents({
|
|
245
|
+
agentHost: params.agentHost,
|
|
246
|
+
events: [
|
|
247
|
+
{
|
|
248
|
+
eventType: 'task_stop_ack',
|
|
249
|
+
taskId: params.taskId,
|
|
250
|
+
requestId: params.requestId,
|
|
251
|
+
accepted: params.accepted,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
return (Array.isArray(payload.results) ? payload.results[0] : null) ?? {};
|
|
256
|
+
}
|
|
155
257
|
async matchProjectByPath(params) {
|
|
156
258
|
const response = await this.request('POST', '/projects/match-path', {
|
|
157
259
|
body: JSON.stringify(params),
|
package/dist/client.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BackendApiClient } from './backend/index.js';
|
|
2
2
|
import { ConductorConfig } from './config/index.js';
|
|
3
3
|
import { MessageRouter } from './message/index.js';
|
|
4
|
+
import { DownstreamCursorStore, DurableUpstreamOutboxStore } from './outbox/index.js';
|
|
4
5
|
import { SessionDiskStore, SessionManager } from './session/index.js';
|
|
5
6
|
import { ConductorWebSocketClient } from './ws/index.js';
|
|
6
|
-
type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'updateTask' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
|
|
7
|
+
type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'updateTask' | 'commitSdkMessage' | 'commitTaskStatusUpdate' | 'commitAgentCommandAck' | 'commitTaskStopAck' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
|
|
7
8
|
type RealtimeClientLike = Pick<ConductorWebSocketClient, 'registerHandler' | 'connect' | 'disconnect' | 'sendJson'>;
|
|
8
9
|
export interface ConductorClientConnectOptions {
|
|
9
10
|
config?: ConductorConfig;
|
|
@@ -11,11 +12,14 @@ export interface ConductorClientConnectOptions {
|
|
|
11
12
|
env?: Record<string, string | undefined>;
|
|
12
13
|
extraEnv?: Record<string, string | undefined>;
|
|
13
14
|
projectPath?: string;
|
|
15
|
+
deliveryScopeId?: string;
|
|
14
16
|
backendApi?: BackendApiLike;
|
|
15
17
|
wsClient?: RealtimeClientLike;
|
|
16
18
|
sessionManager?: SessionManager;
|
|
17
19
|
sessionStore?: SessionDiskStore;
|
|
18
20
|
messageRouter?: MessageRouter;
|
|
21
|
+
upstreamOutbox?: DurableUpstreamOutboxStore;
|
|
22
|
+
downstreamCursorStore?: DownstreamCursorStore;
|
|
19
23
|
agentHost?: string;
|
|
20
24
|
onConnected?: (event: {
|
|
21
25
|
isReconnect: boolean;
|
|
@@ -27,11 +31,14 @@ interface ConductorClientInit {
|
|
|
27
31
|
config: ConductorConfig;
|
|
28
32
|
env: Record<string, string | undefined>;
|
|
29
33
|
projectPath: string;
|
|
34
|
+
deliveryScopeId: string;
|
|
30
35
|
backendApi: BackendApiLike;
|
|
31
36
|
wsClient: RealtimeClientLike;
|
|
32
37
|
sessionManager: SessionManager;
|
|
33
38
|
sessionStore: SessionDiskStore;
|
|
34
39
|
messageRouter: MessageRouter;
|
|
40
|
+
upstreamOutbox: DurableUpstreamOutboxStore;
|
|
41
|
+
downstreamCursorStore: DownstreamCursorStore;
|
|
35
42
|
agentHost: string;
|
|
36
43
|
onStopTask?: (event: StopTaskEvent) => Promise<void> | void;
|
|
37
44
|
}
|
|
@@ -49,11 +56,15 @@ export declare class ConductorClient {
|
|
|
49
56
|
private readonly sessions;
|
|
50
57
|
private readonly sessionStore;
|
|
51
58
|
private readonly messageRouter;
|
|
59
|
+
private upstreamOutbox;
|
|
60
|
+
private downstreamCursorStore;
|
|
52
61
|
private readonly agentHost;
|
|
53
62
|
private readonly onStopTask?;
|
|
63
|
+
private deliveryScopeId;
|
|
54
64
|
private closed;
|
|
55
|
-
private
|
|
56
|
-
private
|
|
65
|
+
private durableOutboxFlushPromise;
|
|
66
|
+
private durableOutboxTimer;
|
|
67
|
+
private durableOutboxTimerDueAt;
|
|
57
68
|
constructor(init: ConductorClientInit);
|
|
58
69
|
static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
|
|
59
70
|
close(): Promise<void>;
|
|
@@ -65,6 +76,10 @@ export declare class ConductorClient {
|
|
|
65
76
|
active_tasks?: string[];
|
|
66
77
|
source?: string;
|
|
67
78
|
metadata?: Record<string, any>;
|
|
79
|
+
last_applied_cursor?: {
|
|
80
|
+
created_at: string;
|
|
81
|
+
request_id: string;
|
|
82
|
+
};
|
|
68
83
|
}): Promise<Record<string, any>>;
|
|
69
84
|
sendAgentCommandAck(payload: {
|
|
70
85
|
request_id: string;
|
|
@@ -88,28 +103,22 @@ export declare class ConductorClient {
|
|
|
88
103
|
matchProjectByPath(payload?: Record<string, any>): Promise<Record<string, any>>;
|
|
89
104
|
bindProjectPath(projectId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
|
|
90
105
|
private readonly handleBackendEvent;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
* Called initially and on reconnection.
|
|
103
|
-
*/
|
|
104
|
-
private doSendConfirmable;
|
|
105
|
-
/**
|
|
106
|
-
* Flush all pending confirmable messages.
|
|
107
|
-
* Called after websocket reconnection.
|
|
108
|
-
*/
|
|
109
|
-
private flushPendingOutbound;
|
|
106
|
+
private extractDownstreamCommandContext;
|
|
107
|
+
private handleStopTaskCommand;
|
|
108
|
+
private invokeStopTaskHandler;
|
|
109
|
+
private persistAndCommitUpstreamEvent;
|
|
110
|
+
private requestDurableOutboxFlush;
|
|
111
|
+
private hasEarlierPendingSdkMessage;
|
|
112
|
+
private commitDurableUpstreamEvent;
|
|
113
|
+
private isRetryableUpstreamError;
|
|
114
|
+
private computeDurableOutboxRetryDelay;
|
|
115
|
+
private scheduleDurableOutboxFlush;
|
|
116
|
+
private clearDurableOutboxTimer;
|
|
110
117
|
private sendEnvelope;
|
|
111
118
|
private maybeAckInboundCommand;
|
|
112
|
-
private
|
|
119
|
+
private promoteTaskDeliveryScope;
|
|
120
|
+
private setDeliveryScopeId;
|
|
121
|
+
private shouldAutoFlushDurableOutbox;
|
|
113
122
|
private resolveHostname;
|
|
114
123
|
private waitForTaskCreation;
|
|
115
124
|
private readIntEnv;
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { BackendApiClient } from './backend/index.js';
|
|
2
|
+
import { BackendApiClient, BackendApiError } 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 { DownstreamCursorStore, DurableUpstreamOutboxStore, normalizeDownstreamCommandCursor, } from './outbox/index.js';
|
|
6
7
|
import { SessionDiskStore, SessionManager, currentHostname } from './session/index.js';
|
|
7
8
|
import { ConductorWebSocketClient } from './ws/index.js';
|
|
8
9
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -15,11 +16,15 @@ export class ConductorClient {
|
|
|
15
16
|
sessions;
|
|
16
17
|
sessionStore;
|
|
17
18
|
messageRouter;
|
|
19
|
+
upstreamOutbox;
|
|
20
|
+
downstreamCursorStore;
|
|
18
21
|
agentHost;
|
|
19
22
|
onStopTask;
|
|
23
|
+
deliveryScopeId;
|
|
20
24
|
closed = false;
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
durableOutboxFlushPromise = null;
|
|
26
|
+
durableOutboxTimer = null;
|
|
27
|
+
durableOutboxTimerDueAt = null;
|
|
23
28
|
constructor(init) {
|
|
24
29
|
this.config = init.config;
|
|
25
30
|
this.env = init.env;
|
|
@@ -29,8 +34,11 @@ export class ConductorClient {
|
|
|
29
34
|
this.sessions = init.sessionManager;
|
|
30
35
|
this.sessionStore = init.sessionStore;
|
|
31
36
|
this.messageRouter = init.messageRouter;
|
|
37
|
+
this.upstreamOutbox = init.upstreamOutbox;
|
|
38
|
+
this.downstreamCursorStore = init.downstreamCursorStore;
|
|
32
39
|
this.agentHost = init.agentHost;
|
|
33
40
|
this.onStopTask = init.onStopTask;
|
|
41
|
+
this.deliveryScopeId = init.deliveryScopeId;
|
|
34
42
|
this.wsClient.registerHandler(this.handleBackendEvent);
|
|
35
43
|
}
|
|
36
44
|
static async connect(options = {}) {
|
|
@@ -42,29 +50,39 @@ export class ConductorClient {
|
|
|
42
50
|
const sessionStore = options.sessionStore ?? SessionDiskStore.forBackendUrl(config.backendUrl);
|
|
43
51
|
const messageRouter = options.messageRouter ?? new MessageRouter(sessions);
|
|
44
52
|
const agentHost = resolveAgentHost(env, options.agentHost);
|
|
53
|
+
const deliveryScopeId = normalizeDeliveryScopeId(options.deliveryScopeId ?? `agent:${agentHost}`);
|
|
54
|
+
const upstreamOutbox = options.upstreamOutbox ?? DurableUpstreamOutboxStore.forProjectPath(projectPath, deliveryScopeId);
|
|
55
|
+
const downstreamCursorStore = options.downstreamCursorStore ?? DownstreamCursorStore.forProjectPath(projectPath, deliveryScopeId);
|
|
45
56
|
const wsClient = options.wsClient ??
|
|
46
57
|
new ConductorWebSocketClient(config, {
|
|
47
58
|
hostName: agentHost,
|
|
48
59
|
onConnected: options.onConnected,
|
|
49
60
|
onDisconnected: options.onDisconnected,
|
|
50
61
|
onReconnected: () => {
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
if (client.shouldAutoFlushDurableOutbox()) {
|
|
63
|
+
void client.requestDurableOutboxFlush(true);
|
|
64
|
+
}
|
|
53
65
|
},
|
|
54
66
|
});
|
|
55
67
|
const client = new ConductorClient({
|
|
56
68
|
config,
|
|
57
69
|
env,
|
|
58
70
|
projectPath,
|
|
71
|
+
deliveryScopeId,
|
|
59
72
|
backendApi,
|
|
60
73
|
wsClient,
|
|
61
74
|
sessionManager: sessions,
|
|
62
75
|
sessionStore,
|
|
63
76
|
messageRouter,
|
|
77
|
+
upstreamOutbox,
|
|
78
|
+
downstreamCursorStore,
|
|
64
79
|
agentHost,
|
|
65
80
|
onStopTask: options.onStopTask,
|
|
66
81
|
});
|
|
67
82
|
await client.wsClient.connect();
|
|
83
|
+
if (client.shouldAutoFlushDurableOutbox()) {
|
|
84
|
+
void client.requestDurableOutboxFlush(true);
|
|
85
|
+
}
|
|
68
86
|
return client;
|
|
69
87
|
}
|
|
70
88
|
async close() {
|
|
@@ -72,6 +90,7 @@ export class ConductorClient {
|
|
|
72
90
|
return;
|
|
73
91
|
}
|
|
74
92
|
this.closed = true;
|
|
93
|
+
this.clearDurableOutboxTimer();
|
|
75
94
|
await this.wsClient.disconnect();
|
|
76
95
|
}
|
|
77
96
|
async createTaskSession(payload) {
|
|
@@ -132,6 +151,7 @@ export class ConductorClient {
|
|
|
132
151
|
throw error;
|
|
133
152
|
}
|
|
134
153
|
await this.waitForTaskCreation(projectId, taskId);
|
|
154
|
+
this.promoteTaskDeliveryScope(taskId);
|
|
135
155
|
const projectPath = typeof payload.project_path === 'string' && payload.project_path
|
|
136
156
|
? payload.project_path
|
|
137
157
|
: this.projectPath;
|
|
@@ -152,25 +172,31 @@ export class ConductorClient {
|
|
|
152
172
|
}
|
|
153
173
|
async sendMessage(taskId, content, metadata) {
|
|
154
174
|
const messageId = safeRandomUuid();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
176
|
+
stableId: messageId,
|
|
177
|
+
eventType: 'sdk_message',
|
|
178
|
+
payload: {
|
|
179
|
+
taskId,
|
|
180
|
+
content,
|
|
181
|
+
metadata,
|
|
182
|
+
messageId,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return { ...result, message_id: messageId };
|
|
163
186
|
}
|
|
164
187
|
async sendTaskStatus(taskId, payload) {
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
const statusEventId = safeRandomUuid();
|
|
189
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
190
|
+
stableId: statusEventId,
|
|
191
|
+
eventType: 'task_status_update',
|
|
167
192
|
payload: {
|
|
168
|
-
|
|
193
|
+
taskId,
|
|
169
194
|
status: payload?.status,
|
|
170
195
|
summary: payload?.summary,
|
|
196
|
+
statusEventId,
|
|
171
197
|
},
|
|
172
198
|
});
|
|
173
|
-
return {
|
|
199
|
+
return { ...result, status_event_id: statusEventId };
|
|
174
200
|
}
|
|
175
201
|
async sendRuntimeStatus(taskId, payload) {
|
|
176
202
|
await this.sendEnvelope({
|
|
@@ -199,6 +225,13 @@ export class ConductorClient {
|
|
|
199
225
|
return { delivered: true };
|
|
200
226
|
}
|
|
201
227
|
async sendAgentResume(payload = {}) {
|
|
228
|
+
const storedCursor = this.downstreamCursorStore.get(this.agentHost);
|
|
229
|
+
const lastAppliedCursor = payload.last_applied_cursor ?? (storedCursor
|
|
230
|
+
? {
|
|
231
|
+
created_at: storedCursor.createdAt,
|
|
232
|
+
request_id: storedCursor.requestId,
|
|
233
|
+
}
|
|
234
|
+
: undefined);
|
|
202
235
|
await this.sendEnvelope({
|
|
203
236
|
type: 'agent_resume',
|
|
204
237
|
payload: {
|
|
@@ -207,6 +240,7 @@ export class ConductorClient {
|
|
|
207
240
|
: [],
|
|
208
241
|
source: payload.source,
|
|
209
242
|
metadata: payload.metadata,
|
|
243
|
+
last_applied_cursor: lastAppliedCursor,
|
|
210
244
|
},
|
|
211
245
|
});
|
|
212
246
|
return { delivered: true };
|
|
@@ -222,8 +256,17 @@ export class ConductorClient {
|
|
|
222
256
|
event_type: payload.event_type,
|
|
223
257
|
accepted: payload.accepted !== false,
|
|
224
258
|
};
|
|
225
|
-
await this.
|
|
226
|
-
|
|
259
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
260
|
+
stableId: requestId,
|
|
261
|
+
eventType: 'agent_command_ack',
|
|
262
|
+
payload: {
|
|
263
|
+
requestId,
|
|
264
|
+
taskId: envelopePayload.task_id,
|
|
265
|
+
commandEventType: envelopePayload.event_type,
|
|
266
|
+
accepted: envelopePayload.accepted,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return { ...result, request_id: requestId };
|
|
227
270
|
}
|
|
228
271
|
async sendTaskStopAck(payload) {
|
|
229
272
|
const taskId = String(payload.task_id || '').trim();
|
|
@@ -234,15 +277,16 @@ export class ConductorClient {
|
|
|
234
277
|
if (!requestId) {
|
|
235
278
|
throw new Error('request_id is required');
|
|
236
279
|
}
|
|
237
|
-
await this.
|
|
238
|
-
|
|
280
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
281
|
+
stableId: requestId,
|
|
282
|
+
eventType: 'task_stop_ack',
|
|
239
283
|
payload: {
|
|
240
|
-
|
|
241
|
-
|
|
284
|
+
taskId,
|
|
285
|
+
requestId,
|
|
242
286
|
accepted: payload.accepted !== false,
|
|
243
287
|
},
|
|
244
288
|
});
|
|
245
|
-
return {
|
|
289
|
+
return { ...result, request_id: requestId, task_id: taskId };
|
|
246
290
|
}
|
|
247
291
|
async receiveMessages(taskId, limit = 20) {
|
|
248
292
|
const messages = await this.sessions.popMessages(taskId, limit);
|
|
@@ -330,6 +374,7 @@ export class ConductorClient {
|
|
|
330
374
|
if (!normalizedTaskId) {
|
|
331
375
|
throw new Error('task_id is required');
|
|
332
376
|
}
|
|
377
|
+
this.promoteTaskDeliveryScope(normalizedTaskId);
|
|
333
378
|
const existing = this.sessionStore.findByTaskId(normalizedTaskId);
|
|
334
379
|
const inMemorySession = await this.sessions.getSession(normalizedTaskId);
|
|
335
380
|
const projectId = existing?.projectId ||
|
|
@@ -422,142 +467,270 @@ export class ConductorClient {
|
|
|
422
467
|
};
|
|
423
468
|
}
|
|
424
469
|
handleBackendEvent = async (payload) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
470
|
+
const command = this.extractDownstreamCommandContext(payload);
|
|
471
|
+
if (command?.eventType === 'stop_task') {
|
|
472
|
+
await this.handleStopTaskCommand(payload, command);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const alreadyApplied = command ? this.downstreamCursorStore.hasApplied(this.agentHost, command.cursor) : false;
|
|
476
|
+
if (!alreadyApplied) {
|
|
477
|
+
await this.messageRouter.handleBackendEvent(payload);
|
|
478
|
+
if (command) {
|
|
479
|
+
this.downstreamCursorStore.advance(this.agentHost, command.cursor);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
429
482
|
await this.maybeAckInboundCommand(payload, {
|
|
430
|
-
accepted:
|
|
431
|
-
? stopCommandAccepted
|
|
432
|
-
: undefined,
|
|
483
|
+
accepted: true,
|
|
433
484
|
});
|
|
434
485
|
};
|
|
435
|
-
|
|
436
|
-
* Handle acknowledgment messages from backend for confirmable outbound messages
|
|
437
|
-
*/
|
|
438
|
-
handleAcknowledgment(payload) {
|
|
486
|
+
extractDownstreamCommandContext(payload) {
|
|
439
487
|
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
488
|
+
if (eventType !== 'task_user_message' && eventType !== 'task_action' && eventType !== 'stop_task') {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
440
491
|
const data = payload?.payload && typeof payload.payload === 'object'
|
|
441
492
|
? payload.payload
|
|
442
493
|
: null;
|
|
443
|
-
if (!data)
|
|
444
|
-
return;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
494
|
+
if (!data) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
|
|
498
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
|
|
499
|
+
const cursor = normalizeDownstreamCommandCursor(data.delivery_cursor);
|
|
500
|
+
if (!taskId || !requestId || !cursor) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
eventType,
|
|
505
|
+
taskId,
|
|
506
|
+
requestId,
|
|
507
|
+
cursor,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
async handleStopTaskCommand(payload, command) {
|
|
511
|
+
const alreadyApplied = this.downstreamCursorStore.hasApplied(this.agentHost, command.cursor);
|
|
512
|
+
let accepted = false;
|
|
513
|
+
if (alreadyApplied) {
|
|
514
|
+
accepted = true;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
accepted = await this.invokeStopTaskHandler(payload);
|
|
518
|
+
if (accepted) {
|
|
519
|
+
this.downstreamCursorStore.advance(this.agentHost, command.cursor);
|
|
454
520
|
}
|
|
455
521
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
522
|
+
if (!command.requestId) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
await this.sendTaskStopAck({
|
|
527
|
+
task_id: command.taskId,
|
|
528
|
+
request_id: command.requestId,
|
|
529
|
+
accepted,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
534
|
+
console.warn(`[sdk] failed to send task_stop_ack for task ${command.taskId}: ${message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async invokeStopTaskHandler(payload) {
|
|
538
|
+
const data = payload?.payload && typeof payload.payload === 'object'
|
|
539
|
+
? payload.payload
|
|
540
|
+
: null;
|
|
541
|
+
if (!data) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
|
|
545
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
|
|
546
|
+
const reason = typeof data.reason === 'string' ? data.reason.trim() : '';
|
|
547
|
+
if (!taskId) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
if (!this.onStopTask) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
await this.onStopTask({
|
|
555
|
+
taskId,
|
|
556
|
+
requestId: requestId || undefined,
|
|
557
|
+
reason: reason || undefined,
|
|
558
|
+
});
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
563
|
+
console.warn(`[sdk] stop_task callback failed for task ${taskId}: ${message}`);
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async persistAndCommitUpstreamEvent(entry) {
|
|
568
|
+
const stored = this.upstreamOutbox.upsert(entry);
|
|
569
|
+
if (stored.eventType === 'sdk_message' && this.hasEarlierPendingSdkMessage(stored.stableId)) {
|
|
570
|
+
this.scheduleDurableOutboxFlush(0);
|
|
571
|
+
return { delivered: false, pending: true };
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
await this.commitDurableUpstreamEvent(stored);
|
|
575
|
+
this.upstreamOutbox.remove(stored.stableId);
|
|
576
|
+
return { delivered: true };
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
if (!this.isRetryableUpstreamError(error)) {
|
|
580
|
+
this.upstreamOutbox.remove(stored.stableId);
|
|
581
|
+
throw error;
|
|
465
582
|
}
|
|
583
|
+
const updated = this.upstreamOutbox.markRetry(stored.stableId, this.computeDurableOutboxRetryDelay(stored.attemptCount + 1));
|
|
584
|
+
const delay = updated?.nextAttemptAt
|
|
585
|
+
? Math.max(Date.parse(updated.nextAttemptAt) - Date.now(), 0)
|
|
586
|
+
: this.computeDurableOutboxRetryDelay(stored.attemptCount + 1);
|
|
587
|
+
console.warn(`[sdk] queued ${stored.eventType} (${stored.stableId}) for durable retry after HTTP failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
588
|
+
this.scheduleDurableOutboxFlush(delay);
|
|
589
|
+
return { delivered: false, pending: true };
|
|
466
590
|
}
|
|
467
591
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
592
|
+
async requestDurableOutboxFlush(force = false) {
|
|
593
|
+
if (this.closed) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (this.durableOutboxFlushPromise) {
|
|
597
|
+
return this.durableOutboxFlushPromise;
|
|
598
|
+
}
|
|
599
|
+
this.durableOutboxFlushPromise = (async () => {
|
|
600
|
+
const readyEntries = force ? this.upstreamOutbox.load() : this.upstreamOutbox.listReady();
|
|
601
|
+
let sdkMessageBlocked = false;
|
|
602
|
+
for (const entry of readyEntries) {
|
|
603
|
+
if (this.closed) {
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
if (sdkMessageBlocked && entry.eventType === 'sdk_message') {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
await this.commitDurableUpstreamEvent(entry);
|
|
611
|
+
this.upstreamOutbox.remove(entry.stableId);
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
if (!this.isRetryableUpstreamError(error)) {
|
|
615
|
+
this.upstreamOutbox.remove(entry.stableId);
|
|
616
|
+
console.warn(`[sdk] dropping durable upstream event ${entry.eventType} (${entry.stableId}) after terminal HTTP failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
617
|
+
continue;
|
|
482
618
|
}
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
619
|
+
const updated = this.upstreamOutbox.markRetry(entry.stableId, this.computeDurableOutboxRetryDelay(entry.attemptCount + 1));
|
|
620
|
+
console.warn(`[sdk] durable retry failed for ${entry.eventType} (${entry.stableId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
621
|
+
if (entry.eventType === 'sdk_message') {
|
|
622
|
+
sdkMessageBlocked = true;
|
|
623
|
+
}
|
|
624
|
+
if (updated?.nextAttemptAt) {
|
|
625
|
+
this.scheduleDurableOutboxFlush(Math.max(Date.parse(updated.nextAttemptAt) - Date.now(), 0));
|
|
487
626
|
}
|
|
488
|
-
setTimeout(checkResolved, 100);
|
|
489
|
-
};
|
|
490
|
-
checkResolved();
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
return new Promise((resolve, reject) => {
|
|
494
|
-
const now = Date.now();
|
|
495
|
-
const pendingMessage = {
|
|
496
|
-
type,
|
|
497
|
-
payload,
|
|
498
|
-
stableId,
|
|
499
|
-
firstSentAt: now,
|
|
500
|
-
lastSentAt: now,
|
|
501
|
-
retryCount: 0,
|
|
502
|
-
resolve,
|
|
503
|
-
reject,
|
|
504
|
-
};
|
|
505
|
-
this.pendingOutbound.set(stableId, pendingMessage);
|
|
506
|
-
// Send the message
|
|
507
|
-
void this.doSendConfirmable(stableId);
|
|
508
|
-
// Set timeout for acknowledgment
|
|
509
|
-
setTimeout(() => {
|
|
510
|
-
if (this.pendingOutbound.has(stableId)) {
|
|
511
|
-
this.pendingOutbound.delete(stableId);
|
|
512
|
-
reject(new Error(`Timeout waiting for acknowledgment of ${type} (${stableId})`));
|
|
513
627
|
}
|
|
514
|
-
}
|
|
628
|
+
}
|
|
629
|
+
const nextDelay = this.upstreamOutbox.nextRetryDelay();
|
|
630
|
+
if (nextDelay !== null) {
|
|
631
|
+
this.scheduleDurableOutboxFlush(nextDelay);
|
|
632
|
+
}
|
|
633
|
+
})().finally(() => {
|
|
634
|
+
this.durableOutboxFlushPromise = null;
|
|
515
635
|
});
|
|
636
|
+
return this.durableOutboxFlushPromise;
|
|
516
637
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
638
|
+
hasEarlierPendingSdkMessage(stableId) {
|
|
639
|
+
const entries = this.upstreamOutbox.load();
|
|
640
|
+
const currentIndex = entries.findIndex((candidate) => candidate.stableId === stableId);
|
|
641
|
+
if (currentIndex <= 0) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
return entries.slice(0, currentIndex).some((candidate) => candidate.eventType === 'sdk_message');
|
|
645
|
+
}
|
|
646
|
+
async commitDurableUpstreamEvent(entry) {
|
|
647
|
+
if (entry.eventType === 'sdk_message') {
|
|
648
|
+
await this.backendApi.commitSdkMessage({
|
|
649
|
+
agentHost: this.agentHost,
|
|
650
|
+
taskId: String(entry.payload.taskId || ''),
|
|
651
|
+
content: String(entry.payload.content || ''),
|
|
652
|
+
metadata: entry.payload.metadata && typeof entry.payload.metadata === 'object' && !Array.isArray(entry.payload.metadata)
|
|
653
|
+
? entry.payload.metadata
|
|
654
|
+
: undefined,
|
|
655
|
+
messageId: entry.payload.messageId ? String(entry.payload.messageId) : null,
|
|
529
656
|
});
|
|
530
|
-
|
|
531
|
-
pending.retryCount++;
|
|
657
|
+
return;
|
|
532
658
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
659
|
+
if (entry.eventType === 'task_status_update') {
|
|
660
|
+
await this.backendApi.commitTaskStatusUpdate({
|
|
661
|
+
agentHost: this.agentHost,
|
|
662
|
+
taskId: String(entry.payload.taskId || ''),
|
|
663
|
+
status: String(entry.payload.status || ''),
|
|
664
|
+
summary: entry.payload.summary ? String(entry.payload.summary) : null,
|
|
665
|
+
statusEventId: entry.payload.statusEventId ? String(entry.payload.statusEventId) : null,
|
|
666
|
+
});
|
|
667
|
+
return;
|
|
536
668
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
669
|
+
if (entry.eventType === 'task_stop_ack') {
|
|
670
|
+
await this.backendApi.commitTaskStopAck({
|
|
671
|
+
agentHost: this.agentHost,
|
|
672
|
+
taskId: String(entry.payload.taskId || ''),
|
|
673
|
+
requestId: String(entry.payload.requestId || ''),
|
|
674
|
+
accepted: entry.payload.accepted !== false,
|
|
675
|
+
});
|
|
544
676
|
return;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Errors are logged in doSendConfirmable, messages remain pending
|
|
677
|
+
}
|
|
678
|
+
await this.backendApi.commitAgentCommandAck({
|
|
679
|
+
agentHost: this.agentHost,
|
|
680
|
+
requestId: String(entry.payload.requestId || ''),
|
|
681
|
+
taskId: entry.payload.taskId ? String(entry.payload.taskId) : null,
|
|
682
|
+
accepted: entry.payload.accepted !== false,
|
|
683
|
+
commandEventType: entry.payload.commandEventType ? String(entry.payload.commandEventType) : null,
|
|
553
684
|
});
|
|
554
685
|
}
|
|
686
|
+
isRetryableUpstreamError(error) {
|
|
687
|
+
if (!(error instanceof BackendApiError)) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
if (error.statusCode === undefined || error.statusCode === null) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
if (error.statusCode >= 500) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
return error.statusCode === 408 || error.statusCode === 429;
|
|
697
|
+
}
|
|
698
|
+
computeDurableOutboxRetryDelay(attemptCount) {
|
|
699
|
+
const cappedExponent = Math.max(0, Math.min(attemptCount - 1, 5));
|
|
700
|
+
return 1_000 * 2 ** cappedExponent;
|
|
701
|
+
}
|
|
702
|
+
scheduleDurableOutboxFlush(delayMs) {
|
|
703
|
+
if (this.closed) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const normalizedDelay = Math.max(delayMs, 0);
|
|
707
|
+
const dueAt = Date.now() + normalizedDelay;
|
|
708
|
+
if (this.durableOutboxTimer &&
|
|
709
|
+
this.durableOutboxTimerDueAt !== null &&
|
|
710
|
+
this.durableOutboxTimerDueAt <= dueAt) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
this.clearDurableOutboxTimer();
|
|
714
|
+
this.durableOutboxTimerDueAt = dueAt;
|
|
715
|
+
this.durableOutboxTimer = setTimeout(() => {
|
|
716
|
+
this.durableOutboxTimer = null;
|
|
717
|
+
this.durableOutboxTimerDueAt = null;
|
|
718
|
+
void this.requestDurableOutboxFlush(false);
|
|
719
|
+
}, normalizedDelay);
|
|
720
|
+
}
|
|
721
|
+
clearDurableOutboxTimer() {
|
|
722
|
+
if (this.durableOutboxTimer) {
|
|
723
|
+
clearTimeout(this.durableOutboxTimer);
|
|
724
|
+
this.durableOutboxTimer = null;
|
|
725
|
+
}
|
|
726
|
+
this.durableOutboxTimerDueAt = null;
|
|
727
|
+
}
|
|
555
728
|
async sendEnvelope(envelope) {
|
|
556
729
|
await this.wsClient.sendJson(envelope);
|
|
557
730
|
}
|
|
558
731
|
async maybeAckInboundCommand(payload, options = {}) {
|
|
559
732
|
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
560
|
-
if (eventType !== 'task_user_message' && eventType !== 'task_action'
|
|
733
|
+
if (eventType !== 'task_user_message' && eventType !== 'task_action') {
|
|
561
734
|
return;
|
|
562
735
|
}
|
|
563
736
|
const data = payload?.payload && typeof payload.payload === 'object'
|
|
@@ -583,51 +756,41 @@ export class ConductorClient {
|
|
|
583
756
|
console.warn(`[sdk] failed to ack inbound command ${requestId}: ${message}`);
|
|
584
757
|
}
|
|
585
758
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const data = payload?.payload && typeof payload.payload === 'object'
|
|
591
|
-
? payload.payload
|
|
592
|
-
: null;
|
|
593
|
-
if (!data) {
|
|
594
|
-
return false;
|
|
759
|
+
promoteTaskDeliveryScope(taskId) {
|
|
760
|
+
const normalizedTaskId = String(taskId || '').trim();
|
|
761
|
+
if (!normalizedTaskId) {
|
|
762
|
+
return;
|
|
595
763
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
764
|
+
this.setDeliveryScopeId(`task:${normalizedTaskId}`);
|
|
765
|
+
}
|
|
766
|
+
setDeliveryScopeId(scopeId) {
|
|
767
|
+
const nextScopeId = normalizeDeliveryScopeId(scopeId);
|
|
768
|
+
if (nextScopeId === this.deliveryScopeId) {
|
|
769
|
+
return;
|
|
601
770
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
-
console.warn(`[sdk] stop_task callback failed for task ${taskId}: ${message}`);
|
|
615
|
-
}
|
|
771
|
+
const currentUpstream = this.upstreamOutbox;
|
|
772
|
+
const currentCursorStore = this.downstreamCursorStore;
|
|
773
|
+
const nextUpstream = DurableUpstreamOutboxStore.forProjectPath(this.projectPath, nextScopeId);
|
|
774
|
+
const nextCursorStore = DownstreamCursorStore.forProjectPath(this.projectPath, nextScopeId);
|
|
775
|
+
for (const entry of currentUpstream.load()) {
|
|
776
|
+
nextUpstream.upsert({
|
|
777
|
+
stableId: entry.stableId,
|
|
778
|
+
eventType: entry.eventType,
|
|
779
|
+
payload: entry.payload,
|
|
780
|
+
});
|
|
781
|
+
currentUpstream.remove(entry.stableId);
|
|
616
782
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
task_id: taskId,
|
|
621
|
-
request_id: requestId,
|
|
622
|
-
accepted,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
catch (error) {
|
|
626
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
627
|
-
console.warn(`[sdk] failed to send task_stop_ack for task ${taskId}: ${message}`);
|
|
628
|
-
}
|
|
783
|
+
const currentCursor = currentCursorStore.get(this.agentHost);
|
|
784
|
+
if (currentCursor && !nextCursorStore.get(this.agentHost)) {
|
|
785
|
+
nextCursorStore.advance(this.agentHost, currentCursor);
|
|
629
786
|
}
|
|
630
|
-
|
|
787
|
+
this.upstreamOutbox = nextUpstream;
|
|
788
|
+
this.downstreamCursorStore = nextCursorStore;
|
|
789
|
+
this.deliveryScopeId = nextScopeId;
|
|
790
|
+
void this.requestDurableOutboxFlush(true);
|
|
791
|
+
}
|
|
792
|
+
shouldAutoFlushDurableOutbox() {
|
|
793
|
+
return this.deliveryScopeId.startsWith('task:') || this.deliveryScopeId.startsWith('session:');
|
|
631
794
|
}
|
|
632
795
|
resolveHostname() {
|
|
633
796
|
const records = this.sessionStore.load();
|
|
@@ -706,3 +869,7 @@ function resolveAgentHost(env, explicit) {
|
|
|
706
869
|
const host = env.HOSTNAME || env.COMPUTERNAME || 'unknown-host';
|
|
707
870
|
return `conductor-fire-${host}-${pid}`;
|
|
708
871
|
}
|
|
872
|
+
function normalizeDeliveryScopeId(scopeId) {
|
|
873
|
+
const normalized = String(scopeId || '').trim();
|
|
874
|
+
return normalized || 'default';
|
|
875
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DownstreamCommandCursor {
|
|
2
|
+
createdAt: string;
|
|
3
|
+
requestId: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function normalizeDownstreamCommandCursor(value: unknown): DownstreamCommandCursor | null;
|
|
6
|
+
export declare class DownstreamCursorStore {
|
|
7
|
+
private readonly filePath;
|
|
8
|
+
constructor(filePath: string);
|
|
9
|
+
static filePathForProjectPath(projectPath: string, scopeId: string): string;
|
|
10
|
+
static forProjectPath(projectPath: string, scopeId: string): DownstreamCursorStore;
|
|
11
|
+
get(agentHost: string): DownstreamCommandCursor | null;
|
|
12
|
+
hasApplied(agentHost: string, cursor: DownstreamCommandCursor): boolean;
|
|
13
|
+
advance(agentHost: string, cursor: DownstreamCommandCursor): DownstreamCommandCursor;
|
|
14
|
+
private loadState;
|
|
15
|
+
private saveState;
|
|
16
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const STATE_DIR = path.join('.conductor', 'state');
|
|
4
|
+
const CURSOR_BASENAME = 'agent-downstream-cursor';
|
|
5
|
+
function normalizeCursorValue(value) {
|
|
6
|
+
if (typeof value !== 'string') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const normalized = value.trim();
|
|
10
|
+
return normalized || null;
|
|
11
|
+
}
|
|
12
|
+
function compareCursor(left, right) {
|
|
13
|
+
const leftTime = Date.parse(left.createdAt);
|
|
14
|
+
const rightTime = Date.parse(right.createdAt);
|
|
15
|
+
const normalizedLeftTime = Number.isFinite(leftTime) ? leftTime : 0;
|
|
16
|
+
const normalizedRightTime = Number.isFinite(rightTime) ? rightTime : 0;
|
|
17
|
+
if (normalizedLeftTime !== normalizedRightTime) {
|
|
18
|
+
return normalizedLeftTime < normalizedRightTime ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
return left.requestId.localeCompare(right.requestId);
|
|
21
|
+
}
|
|
22
|
+
const sanitizeScopeId = (scopeId) => scopeId
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '_')
|
|
25
|
+
.replace(/^_+|_+$/g, '')
|
|
26
|
+
|| 'default';
|
|
27
|
+
export function normalizeDownstreamCommandCursor(value) {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const source = value;
|
|
32
|
+
const createdAt = normalizeCursorValue(source.created_at) ?? normalizeCursorValue(source.createdAt);
|
|
33
|
+
const requestId = normalizeCursorValue(source.request_id) ?? normalizeCursorValue(source.requestId);
|
|
34
|
+
if (!createdAt || !requestId) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
createdAt,
|
|
39
|
+
requestId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export class DownstreamCursorStore {
|
|
43
|
+
filePath;
|
|
44
|
+
constructor(filePath) {
|
|
45
|
+
this.filePath = path.resolve(filePath);
|
|
46
|
+
}
|
|
47
|
+
static filePathForProjectPath(projectPath, scopeId) {
|
|
48
|
+
return path.join(projectPath, STATE_DIR, `${CURSOR_BASENAME}.${sanitizeScopeId(scopeId)}.json`);
|
|
49
|
+
}
|
|
50
|
+
static forProjectPath(projectPath, scopeId) {
|
|
51
|
+
return new DownstreamCursorStore(DownstreamCursorStore.filePathForProjectPath(projectPath, scopeId));
|
|
52
|
+
}
|
|
53
|
+
get(agentHost) {
|
|
54
|
+
const state = this.loadState();
|
|
55
|
+
return state.agents[agentHost] ?? null;
|
|
56
|
+
}
|
|
57
|
+
hasApplied(agentHost, cursor) {
|
|
58
|
+
const current = this.get(agentHost);
|
|
59
|
+
if (!current) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return compareCursor(current, cursor) >= 0;
|
|
63
|
+
}
|
|
64
|
+
advance(agentHost, cursor) {
|
|
65
|
+
const state = this.loadState();
|
|
66
|
+
const current = state.agents[agentHost] ?? null;
|
|
67
|
+
if (current && compareCursor(current, cursor) >= 0) {
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
state.agents[agentHost] = cursor;
|
|
71
|
+
this.saveState(state);
|
|
72
|
+
return cursor;
|
|
73
|
+
}
|
|
74
|
+
loadState() {
|
|
75
|
+
if (!fs.existsSync(this.filePath)) {
|
|
76
|
+
return { agents: {} };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const contents = fs.readFileSync(this.filePath, 'utf-8');
|
|
80
|
+
const parsed = JSON.parse(contents);
|
|
81
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.agents || typeof parsed.agents !== 'object') {
|
|
82
|
+
return { agents: {} };
|
|
83
|
+
}
|
|
84
|
+
return { agents: parsed.agents };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { agents: {} };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
saveState(state) {
|
|
91
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
92
|
+
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
93
|
+
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), 'utf-8');
|
|
94
|
+
fs.renameSync(tempPath, this.filePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type DurableUpstreamEventType = 'sdk_message' | 'task_status_update' | 'agent_command_ack' | 'task_stop_ack';
|
|
2
|
+
export interface DurableUpstreamEvent {
|
|
3
|
+
stableId: string;
|
|
4
|
+
eventType: DurableUpstreamEventType;
|
|
5
|
+
payload: Record<string, unknown>;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
attemptCount: number;
|
|
8
|
+
lastAttemptAt?: string | null;
|
|
9
|
+
nextAttemptAt?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare class DurableUpstreamOutboxStore {
|
|
12
|
+
private readonly filePath;
|
|
13
|
+
private readonly lockPath;
|
|
14
|
+
constructor(filePath: string);
|
|
15
|
+
static filePathForProjectPath(projectPath: string, scopeId: string): string;
|
|
16
|
+
static forProjectPath(projectPath: string, scopeId: string): DurableUpstreamOutboxStore;
|
|
17
|
+
load(): DurableUpstreamEvent[];
|
|
18
|
+
upsert(entry: {
|
|
19
|
+
stableId: string;
|
|
20
|
+
eventType: DurableUpstreamEventType;
|
|
21
|
+
payload: Record<string, unknown>;
|
|
22
|
+
}): DurableUpstreamEvent;
|
|
23
|
+
remove(stableId: string): void;
|
|
24
|
+
markRetry(stableId: string, delayMs: number): DurableUpstreamEvent | null;
|
|
25
|
+
listReady(nowMs?: number): DurableUpstreamEvent[];
|
|
26
|
+
nextRetryDelay(nowMs?: number): number | null;
|
|
27
|
+
private loadUnlocked;
|
|
28
|
+
private saveUnlocked;
|
|
29
|
+
private withLock;
|
|
30
|
+
private acquireLock;
|
|
31
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const OUTBOX_DIR = path.join('.conductor', 'state');
|
|
4
|
+
const OUTBOX_BASENAME = 'agent-upstream-outbox';
|
|
5
|
+
const LOCK_TIMEOUT_MS = 10_000;
|
|
6
|
+
const LOCK_RETRY_MS = 50;
|
|
7
|
+
const sleepSync = (ms) => {
|
|
8
|
+
if (ms <= 0)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
const buffer = new SharedArrayBuffer(4);
|
|
12
|
+
const arr = new Int32Array(buffer);
|
|
13
|
+
Atomics.wait(arr, 0, 0, ms);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
while (Date.now() - startedAt < ms) {
|
|
18
|
+
// busy wait fallback
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const sanitizeScopeId = (scopeId) => scopeId
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '_')
|
|
25
|
+
.replace(/^_+|_+$/g, '')
|
|
26
|
+
|| 'default';
|
|
27
|
+
export class DurableUpstreamOutboxStore {
|
|
28
|
+
filePath;
|
|
29
|
+
lockPath;
|
|
30
|
+
constructor(filePath) {
|
|
31
|
+
this.filePath = path.resolve(filePath);
|
|
32
|
+
this.lockPath = `${this.filePath}.lock`;
|
|
33
|
+
}
|
|
34
|
+
static filePathForProjectPath(projectPath, scopeId) {
|
|
35
|
+
return path.join(projectPath, OUTBOX_DIR, `${OUTBOX_BASENAME}.${sanitizeScopeId(scopeId)}.json`);
|
|
36
|
+
}
|
|
37
|
+
static forProjectPath(projectPath, scopeId) {
|
|
38
|
+
return new DurableUpstreamOutboxStore(DurableUpstreamOutboxStore.filePathForProjectPath(projectPath, scopeId));
|
|
39
|
+
}
|
|
40
|
+
load() {
|
|
41
|
+
return this.withLock(() => this.loadUnlocked());
|
|
42
|
+
}
|
|
43
|
+
upsert(entry) {
|
|
44
|
+
return this.withLock(() => {
|
|
45
|
+
const entries = this.loadUnlocked();
|
|
46
|
+
const existing = entries.find((candidate) => candidate.stableId === entry.stableId);
|
|
47
|
+
const record = {
|
|
48
|
+
stableId: entry.stableId,
|
|
49
|
+
eventType: entry.eventType,
|
|
50
|
+
payload: entry.payload,
|
|
51
|
+
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
52
|
+
attemptCount: existing?.attemptCount ?? 0,
|
|
53
|
+
lastAttemptAt: existing?.lastAttemptAt ?? null,
|
|
54
|
+
nextAttemptAt: existing?.nextAttemptAt ?? new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
const nextEntries = existing
|
|
57
|
+
? entries.map((candidate) => (candidate.stableId === entry.stableId ? record : candidate))
|
|
58
|
+
: [...entries, record];
|
|
59
|
+
this.saveUnlocked(nextEntries);
|
|
60
|
+
return record;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
remove(stableId) {
|
|
64
|
+
this.withLock(() => {
|
|
65
|
+
const entries = this.loadUnlocked().filter((candidate) => candidate.stableId !== stableId);
|
|
66
|
+
this.saveUnlocked(entries);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
markRetry(stableId, delayMs) {
|
|
70
|
+
return this.withLock(() => {
|
|
71
|
+
const entries = this.loadUnlocked();
|
|
72
|
+
const index = entries.findIndex((candidate) => candidate.stableId === stableId);
|
|
73
|
+
if (index < 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const updated = {
|
|
78
|
+
...entries[index],
|
|
79
|
+
attemptCount: entries[index].attemptCount + 1,
|
|
80
|
+
lastAttemptAt: new Date(now).toISOString(),
|
|
81
|
+
nextAttemptAt: new Date(now + Math.max(delayMs, 0)).toISOString(),
|
|
82
|
+
};
|
|
83
|
+
entries[index] = updated;
|
|
84
|
+
this.saveUnlocked(entries);
|
|
85
|
+
return updated;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
listReady(nowMs = Date.now()) {
|
|
89
|
+
return this.load()
|
|
90
|
+
.filter((entry) => {
|
|
91
|
+
if (!entry.nextAttemptAt)
|
|
92
|
+
return true;
|
|
93
|
+
const nextAttemptMs = Date.parse(entry.nextAttemptAt);
|
|
94
|
+
return !Number.isFinite(nextAttemptMs) || nextAttemptMs <= nowMs;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
nextRetryDelay(nowMs = Date.now()) {
|
|
98
|
+
const entries = this.load();
|
|
99
|
+
let minDelay = null;
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.nextAttemptAt) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const nextAttemptMs = Date.parse(entry.nextAttemptAt);
|
|
105
|
+
if (!Number.isFinite(nextAttemptMs)) {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
const delay = Math.max(nextAttemptMs - nowMs, 0);
|
|
109
|
+
minDelay = minDelay === null ? delay : Math.min(minDelay, delay);
|
|
110
|
+
}
|
|
111
|
+
return minDelay;
|
|
112
|
+
}
|
|
113
|
+
loadUnlocked() {
|
|
114
|
+
if (!fs.existsSync(this.filePath)) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const contents = fs.readFileSync(this.filePath, 'utf-8');
|
|
119
|
+
const parsed = JSON.parse(contents);
|
|
120
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
saveUnlocked(entries) {
|
|
127
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
128
|
+
const payload = { entries };
|
|
129
|
+
const tempPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
130
|
+
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
131
|
+
fs.renameSync(tempPath, this.filePath);
|
|
132
|
+
}
|
|
133
|
+
withLock(fn) {
|
|
134
|
+
const release = this.acquireLock();
|
|
135
|
+
try {
|
|
136
|
+
return fn();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
release();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
acquireLock() {
|
|
143
|
+
const startedAt = Date.now();
|
|
144
|
+
fs.mkdirSync(path.dirname(this.lockPath), { recursive: true });
|
|
145
|
+
while (true) {
|
|
146
|
+
try {
|
|
147
|
+
const fd = fs.openSync(this.lockPath, 'wx');
|
|
148
|
+
return () => {
|
|
149
|
+
try {
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
fs.unlinkSync(this.lockPath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// ignore
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error?.code !== 'EEXIST') {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
|
|
168
|
+
throw new Error(`Timed out waiting for outbox lock: ${this.lockPath}`);
|
|
169
|
+
}
|
|
170
|
+
sleepSync(LOCK_RETRY_MS);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
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.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"typescript": "^5.6.3",
|
|
28
28
|
"vitest": "^2.1.4"
|
|
29
29
|
},
|
|
30
|
-
"gitCommitId": "
|
|
30
|
+
"gitCommitId": "c2654e1"
|
|
31
31
|
}
|