@love-moon/conductor-sdk 0.2.16 → 0.2.18
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 +58 -0
- package/dist/backend/client.js +102 -0
- package/dist/client.d.ts +35 -28
- package/dist/client.js +344 -174
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/message/router.d.ts +2 -0
- package/dist/message/router.js +18 -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/dist/session/manager.d.ts +6 -0
- package/dist/session/manager.js +18 -0
- package/dist/ws/client.d.ts +41 -4
- package/dist/ws/client.js +189 -13
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
|
-
import { ConductorWebSocketClient } from './ws/index.js';
|
|
8
|
+
import { ConductorWebSocketClient, } from './ws/index.js';
|
|
8
9
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
10
|
export class ConductorClient {
|
|
10
11
|
config;
|
|
@@ -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) {
|
|
@@ -112,6 +131,7 @@ export class ConductorClient {
|
|
|
112
131
|
id: taskId,
|
|
113
132
|
projectId,
|
|
114
133
|
title,
|
|
134
|
+
status: 'running',
|
|
115
135
|
backendType,
|
|
116
136
|
sessionId: logicalSessionId,
|
|
117
137
|
sessionFilePath: explicitSessionFilePath,
|
|
@@ -132,6 +152,7 @@ export class ConductorClient {
|
|
|
132
152
|
throw error;
|
|
133
153
|
}
|
|
134
154
|
await this.waitForTaskCreation(projectId, taskId);
|
|
155
|
+
this.promoteTaskDeliveryScope(taskId);
|
|
135
156
|
const projectPath = typeof payload.project_path === 'string' && payload.project_path
|
|
136
157
|
? payload.project_path
|
|
137
158
|
: this.projectPath;
|
|
@@ -152,25 +173,31 @@ export class ConductorClient {
|
|
|
152
173
|
}
|
|
153
174
|
async sendMessage(taskId, content, metadata) {
|
|
154
175
|
const messageId = safeRandomUuid();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
176
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
177
|
+
stableId: messageId,
|
|
178
|
+
eventType: 'sdk_message',
|
|
179
|
+
payload: {
|
|
180
|
+
taskId,
|
|
181
|
+
content,
|
|
182
|
+
metadata,
|
|
183
|
+
messageId,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return { ...result, message_id: messageId };
|
|
163
187
|
}
|
|
164
188
|
async sendTaskStatus(taskId, payload) {
|
|
165
|
-
|
|
166
|
-
|
|
189
|
+
const statusEventId = safeRandomUuid();
|
|
190
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
191
|
+
stableId: statusEventId,
|
|
192
|
+
eventType: 'task_status_update',
|
|
167
193
|
payload: {
|
|
168
|
-
|
|
194
|
+
taskId,
|
|
169
195
|
status: payload?.status,
|
|
170
196
|
summary: payload?.summary,
|
|
197
|
+
statusEventId,
|
|
171
198
|
},
|
|
172
199
|
});
|
|
173
|
-
return {
|
|
200
|
+
return { ...result, status_event_id: statusEventId };
|
|
174
201
|
}
|
|
175
202
|
async sendRuntimeStatus(taskId, payload) {
|
|
176
203
|
await this.sendEnvelope({
|
|
@@ -199,6 +226,13 @@ export class ConductorClient {
|
|
|
199
226
|
return { delivered: true };
|
|
200
227
|
}
|
|
201
228
|
async sendAgentResume(payload = {}) {
|
|
229
|
+
const storedCursor = this.downstreamCursorStore.get(this.agentHost);
|
|
230
|
+
const lastAppliedCursor = payload.last_applied_cursor ?? (storedCursor
|
|
231
|
+
? {
|
|
232
|
+
created_at: storedCursor.createdAt,
|
|
233
|
+
request_id: storedCursor.requestId,
|
|
234
|
+
}
|
|
235
|
+
: undefined);
|
|
202
236
|
await this.sendEnvelope({
|
|
203
237
|
type: 'agent_resume',
|
|
204
238
|
payload: {
|
|
@@ -207,6 +241,7 @@ export class ConductorClient {
|
|
|
207
241
|
: [],
|
|
208
242
|
source: payload.source,
|
|
209
243
|
metadata: payload.metadata,
|
|
244
|
+
last_applied_cursor: lastAppliedCursor,
|
|
210
245
|
},
|
|
211
246
|
});
|
|
212
247
|
return { delivered: true };
|
|
@@ -222,8 +257,17 @@ export class ConductorClient {
|
|
|
222
257
|
event_type: payload.event_type,
|
|
223
258
|
accepted: payload.accepted !== false,
|
|
224
259
|
};
|
|
225
|
-
await this.
|
|
226
|
-
|
|
260
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
261
|
+
stableId: requestId,
|
|
262
|
+
eventType: 'agent_command_ack',
|
|
263
|
+
payload: {
|
|
264
|
+
requestId,
|
|
265
|
+
taskId: envelopePayload.task_id,
|
|
266
|
+
commandEventType: envelopePayload.event_type,
|
|
267
|
+
accepted: envelopePayload.accepted,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
return { ...result, request_id: requestId };
|
|
227
271
|
}
|
|
228
272
|
async sendTaskStopAck(payload) {
|
|
229
273
|
const taskId = String(payload.task_id || '').trim();
|
|
@@ -234,15 +278,16 @@ export class ConductorClient {
|
|
|
234
278
|
if (!requestId) {
|
|
235
279
|
throw new Error('request_id is required');
|
|
236
280
|
}
|
|
237
|
-
await this.
|
|
238
|
-
|
|
281
|
+
const result = await this.persistAndCommitUpstreamEvent({
|
|
282
|
+
stableId: requestId,
|
|
283
|
+
eventType: 'task_stop_ack',
|
|
239
284
|
payload: {
|
|
240
|
-
|
|
241
|
-
|
|
285
|
+
taskId,
|
|
286
|
+
requestId,
|
|
242
287
|
accepted: payload.accepted !== false,
|
|
243
288
|
},
|
|
244
289
|
});
|
|
245
|
-
return {
|
|
290
|
+
return { ...result, request_id: requestId, task_id: taskId };
|
|
246
291
|
}
|
|
247
292
|
async receiveMessages(taskId, limit = 20) {
|
|
248
293
|
const messages = await this.sessions.popMessages(taskId, limit);
|
|
@@ -330,6 +375,7 @@ export class ConductorClient {
|
|
|
330
375
|
if (!normalizedTaskId) {
|
|
331
376
|
throw new Error('task_id is required');
|
|
332
377
|
}
|
|
378
|
+
this.promoteTaskDeliveryScope(normalizedTaskId);
|
|
333
379
|
const existing = this.sessionStore.findByTaskId(normalizedTaskId);
|
|
334
380
|
const inMemorySession = await this.sessions.getSession(normalizedTaskId);
|
|
335
381
|
const projectId = existing?.projectId ||
|
|
@@ -422,142 +468,270 @@ export class ConductorClient {
|
|
|
422
468
|
};
|
|
423
469
|
}
|
|
424
470
|
handleBackendEvent = async (payload) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
471
|
+
const command = this.extractDownstreamCommandContext(payload);
|
|
472
|
+
if (command?.eventType === 'stop_task') {
|
|
473
|
+
await this.handleStopTaskCommand(payload, command);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const alreadyApplied = command ? this.downstreamCursorStore.hasApplied(this.agentHost, command.cursor) : false;
|
|
477
|
+
if (!alreadyApplied) {
|
|
478
|
+
await this.messageRouter.handleBackendEvent(payload);
|
|
479
|
+
if (command) {
|
|
480
|
+
this.downstreamCursorStore.advance(this.agentHost, command.cursor);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
429
483
|
await this.maybeAckInboundCommand(payload, {
|
|
430
|
-
accepted:
|
|
431
|
-
? stopCommandAccepted
|
|
432
|
-
: undefined,
|
|
484
|
+
accepted: true,
|
|
433
485
|
});
|
|
434
486
|
};
|
|
435
|
-
|
|
436
|
-
* Handle acknowledgment messages from backend for confirmable outbound messages
|
|
437
|
-
*/
|
|
438
|
-
handleAcknowledgment(payload) {
|
|
487
|
+
extractDownstreamCommandContext(payload) {
|
|
439
488
|
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
489
|
+
if (eventType !== 'task_user_message' && eventType !== 'task_action' && eventType !== 'stop_task') {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
440
492
|
const data = payload?.payload && typeof payload.payload === 'object'
|
|
441
493
|
? payload.payload
|
|
442
494
|
: null;
|
|
443
|
-
if (!data)
|
|
444
|
-
return;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
495
|
+
if (!data) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
|
|
499
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
|
|
500
|
+
const cursor = normalizeDownstreamCommandCursor(data.delivery_cursor);
|
|
501
|
+
if (!taskId || !requestId || !cursor) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
eventType,
|
|
506
|
+
taskId,
|
|
507
|
+
requestId,
|
|
508
|
+
cursor,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async handleStopTaskCommand(payload, command) {
|
|
512
|
+
const alreadyApplied = this.downstreamCursorStore.hasApplied(this.agentHost, command.cursor);
|
|
513
|
+
let accepted = false;
|
|
514
|
+
if (alreadyApplied) {
|
|
515
|
+
accepted = true;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
accepted = await this.invokeStopTaskHandler(payload);
|
|
519
|
+
if (accepted) {
|
|
520
|
+
this.downstreamCursorStore.advance(this.agentHost, command.cursor);
|
|
454
521
|
}
|
|
455
522
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
523
|
+
if (!command.requestId) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
await this.sendTaskStopAck({
|
|
528
|
+
task_id: command.taskId,
|
|
529
|
+
request_id: command.requestId,
|
|
530
|
+
accepted,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
console.warn(`[sdk] failed to send task_stop_ack for task ${command.taskId}: ${message}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async invokeStopTaskHandler(payload) {
|
|
539
|
+
const data = payload?.payload && typeof payload.payload === 'object'
|
|
540
|
+
? payload.payload
|
|
541
|
+
: null;
|
|
542
|
+
if (!data) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
|
|
546
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
|
|
547
|
+
const reason = typeof data.reason === 'string' ? data.reason.trim() : '';
|
|
548
|
+
if (!taskId) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (!this.onStopTask) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
await this.onStopTask({
|
|
556
|
+
taskId,
|
|
557
|
+
requestId: requestId || undefined,
|
|
558
|
+
reason: reason || undefined,
|
|
559
|
+
});
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
564
|
+
console.warn(`[sdk] stop_task callback failed for task ${taskId}: ${message}`);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async persistAndCommitUpstreamEvent(entry) {
|
|
569
|
+
const stored = this.upstreamOutbox.upsert(entry);
|
|
570
|
+
if (stored.eventType === 'sdk_message' && this.hasEarlierPendingSdkMessage(stored.stableId)) {
|
|
571
|
+
this.scheduleDurableOutboxFlush(0);
|
|
572
|
+
return { delivered: false, pending: true };
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
await this.commitDurableUpstreamEvent(stored);
|
|
576
|
+
this.upstreamOutbox.remove(stored.stableId);
|
|
577
|
+
return { delivered: true };
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (!this.isRetryableUpstreamError(error)) {
|
|
581
|
+
this.upstreamOutbox.remove(stored.stableId);
|
|
582
|
+
throw error;
|
|
465
583
|
}
|
|
584
|
+
const updated = this.upstreamOutbox.markRetry(stored.stableId, this.computeDurableOutboxRetryDelay(stored.attemptCount + 1));
|
|
585
|
+
const delay = updated?.nextAttemptAt
|
|
586
|
+
? Math.max(Date.parse(updated.nextAttemptAt) - Date.now(), 0)
|
|
587
|
+
: this.computeDurableOutboxRetryDelay(stored.attemptCount + 1);
|
|
588
|
+
console.warn(`[sdk] queued ${stored.eventType} (${stored.stableId}) for durable retry after HTTP failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
589
|
+
this.scheduleDurableOutboxFlush(delay);
|
|
590
|
+
return { delivered: false, pending: true };
|
|
466
591
|
}
|
|
467
592
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
593
|
+
async requestDurableOutboxFlush(force = false) {
|
|
594
|
+
if (this.closed) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (this.durableOutboxFlushPromise) {
|
|
598
|
+
return this.durableOutboxFlushPromise;
|
|
599
|
+
}
|
|
600
|
+
this.durableOutboxFlushPromise = (async () => {
|
|
601
|
+
const readyEntries = force ? this.upstreamOutbox.load() : this.upstreamOutbox.listReady();
|
|
602
|
+
let sdkMessageBlocked = false;
|
|
603
|
+
for (const entry of readyEntries) {
|
|
604
|
+
if (this.closed) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
if (sdkMessageBlocked && entry.eventType === 'sdk_message') {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
await this.commitDurableUpstreamEvent(entry);
|
|
612
|
+
this.upstreamOutbox.remove(entry.stableId);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (!this.isRetryableUpstreamError(error)) {
|
|
616
|
+
this.upstreamOutbox.remove(entry.stableId);
|
|
617
|
+
console.warn(`[sdk] dropping durable upstream event ${entry.eventType} (${entry.stableId}) after terminal HTTP failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
618
|
+
continue;
|
|
482
619
|
}
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
620
|
+
const updated = this.upstreamOutbox.markRetry(entry.stableId, this.computeDurableOutboxRetryDelay(entry.attemptCount + 1));
|
|
621
|
+
console.warn(`[sdk] durable retry failed for ${entry.eventType} (${entry.stableId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
622
|
+
if (entry.eventType === 'sdk_message') {
|
|
623
|
+
sdkMessageBlocked = true;
|
|
624
|
+
}
|
|
625
|
+
if (updated?.nextAttemptAt) {
|
|
626
|
+
this.scheduleDurableOutboxFlush(Math.max(Date.parse(updated.nextAttemptAt) - Date.now(), 0));
|
|
487
627
|
}
|
|
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
628
|
}
|
|
514
|
-
}
|
|
629
|
+
}
|
|
630
|
+
const nextDelay = this.upstreamOutbox.nextRetryDelay();
|
|
631
|
+
if (nextDelay !== null) {
|
|
632
|
+
this.scheduleDurableOutboxFlush(nextDelay);
|
|
633
|
+
}
|
|
634
|
+
})().finally(() => {
|
|
635
|
+
this.durableOutboxFlushPromise = null;
|
|
515
636
|
});
|
|
637
|
+
return this.durableOutboxFlushPromise;
|
|
516
638
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
639
|
+
hasEarlierPendingSdkMessage(stableId) {
|
|
640
|
+
const entries = this.upstreamOutbox.load();
|
|
641
|
+
const currentIndex = entries.findIndex((candidate) => candidate.stableId === stableId);
|
|
642
|
+
if (currentIndex <= 0) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
return entries.slice(0, currentIndex).some((candidate) => candidate.eventType === 'sdk_message');
|
|
646
|
+
}
|
|
647
|
+
async commitDurableUpstreamEvent(entry) {
|
|
648
|
+
if (entry.eventType === 'sdk_message') {
|
|
649
|
+
await this.backendApi.commitSdkMessage({
|
|
650
|
+
agentHost: this.agentHost,
|
|
651
|
+
taskId: String(entry.payload.taskId || ''),
|
|
652
|
+
content: String(entry.payload.content || ''),
|
|
653
|
+
metadata: entry.payload.metadata && typeof entry.payload.metadata === 'object' && !Array.isArray(entry.payload.metadata)
|
|
654
|
+
? entry.payload.metadata
|
|
655
|
+
: undefined,
|
|
656
|
+
messageId: entry.payload.messageId ? String(entry.payload.messageId) : null,
|
|
529
657
|
});
|
|
530
|
-
|
|
531
|
-
pending.retryCount++;
|
|
658
|
+
return;
|
|
532
659
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
660
|
+
if (entry.eventType === 'task_status_update') {
|
|
661
|
+
await this.backendApi.commitTaskStatusUpdate({
|
|
662
|
+
agentHost: this.agentHost,
|
|
663
|
+
taskId: String(entry.payload.taskId || ''),
|
|
664
|
+
status: String(entry.payload.status || ''),
|
|
665
|
+
summary: entry.payload.summary ? String(entry.payload.summary) : null,
|
|
666
|
+
statusEventId: entry.payload.statusEventId ? String(entry.payload.statusEventId) : null,
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
536
669
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
670
|
+
if (entry.eventType === 'task_stop_ack') {
|
|
671
|
+
await this.backendApi.commitTaskStopAck({
|
|
672
|
+
agentHost: this.agentHost,
|
|
673
|
+
taskId: String(entry.payload.taskId || ''),
|
|
674
|
+
requestId: String(entry.payload.requestId || ''),
|
|
675
|
+
accepted: entry.payload.accepted !== false,
|
|
676
|
+
});
|
|
544
677
|
return;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Errors are logged in doSendConfirmable, messages remain pending
|
|
678
|
+
}
|
|
679
|
+
await this.backendApi.commitAgentCommandAck({
|
|
680
|
+
agentHost: this.agentHost,
|
|
681
|
+
requestId: String(entry.payload.requestId || ''),
|
|
682
|
+
taskId: entry.payload.taskId ? String(entry.payload.taskId) : null,
|
|
683
|
+
accepted: entry.payload.accepted !== false,
|
|
684
|
+
commandEventType: entry.payload.commandEventType ? String(entry.payload.commandEventType) : null,
|
|
553
685
|
});
|
|
554
686
|
}
|
|
687
|
+
isRetryableUpstreamError(error) {
|
|
688
|
+
if (!(error instanceof BackendApiError)) {
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
if (error.statusCode === undefined || error.statusCode === null) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
if (error.statusCode >= 500) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
return error.statusCode === 408 || error.statusCode === 429;
|
|
698
|
+
}
|
|
699
|
+
computeDurableOutboxRetryDelay(attemptCount) {
|
|
700
|
+
const cappedExponent = Math.max(0, Math.min(attemptCount - 1, 5));
|
|
701
|
+
return 1_000 * 2 ** cappedExponent;
|
|
702
|
+
}
|
|
703
|
+
scheduleDurableOutboxFlush(delayMs) {
|
|
704
|
+
if (this.closed) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const normalizedDelay = Math.max(delayMs, 0);
|
|
708
|
+
const dueAt = Date.now() + normalizedDelay;
|
|
709
|
+
if (this.durableOutboxTimer &&
|
|
710
|
+
this.durableOutboxTimerDueAt !== null &&
|
|
711
|
+
this.durableOutboxTimerDueAt <= dueAt) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.clearDurableOutboxTimer();
|
|
715
|
+
this.durableOutboxTimerDueAt = dueAt;
|
|
716
|
+
this.durableOutboxTimer = setTimeout(() => {
|
|
717
|
+
this.durableOutboxTimer = null;
|
|
718
|
+
this.durableOutboxTimerDueAt = null;
|
|
719
|
+
void this.requestDurableOutboxFlush(false);
|
|
720
|
+
}, normalizedDelay);
|
|
721
|
+
}
|
|
722
|
+
clearDurableOutboxTimer() {
|
|
723
|
+
if (this.durableOutboxTimer) {
|
|
724
|
+
clearTimeout(this.durableOutboxTimer);
|
|
725
|
+
this.durableOutboxTimer = null;
|
|
726
|
+
}
|
|
727
|
+
this.durableOutboxTimerDueAt = null;
|
|
728
|
+
}
|
|
555
729
|
async sendEnvelope(envelope) {
|
|
556
730
|
await this.wsClient.sendJson(envelope);
|
|
557
731
|
}
|
|
558
732
|
async maybeAckInboundCommand(payload, options = {}) {
|
|
559
733
|
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
560
|
-
if (eventType !== 'task_user_message' && eventType !== 'task_action'
|
|
734
|
+
if (eventType !== 'task_user_message' && eventType !== 'task_action') {
|
|
561
735
|
return;
|
|
562
736
|
}
|
|
563
737
|
const data = payload?.payload && typeof payload.payload === 'object'
|
|
@@ -583,51 +757,41 @@ export class ConductorClient {
|
|
|
583
757
|
console.warn(`[sdk] failed to ack inbound command ${requestId}: ${message}`);
|
|
584
758
|
}
|
|
585
759
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const data = payload?.payload && typeof payload.payload === 'object'
|
|
591
|
-
? payload.payload
|
|
592
|
-
: null;
|
|
593
|
-
if (!data) {
|
|
594
|
-
return false;
|
|
760
|
+
promoteTaskDeliveryScope(taskId) {
|
|
761
|
+
const normalizedTaskId = String(taskId || '').trim();
|
|
762
|
+
if (!normalizedTaskId) {
|
|
763
|
+
return;
|
|
595
764
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
765
|
+
this.setDeliveryScopeId(`task:${normalizedTaskId}`);
|
|
766
|
+
}
|
|
767
|
+
setDeliveryScopeId(scopeId) {
|
|
768
|
+
const nextScopeId = normalizeDeliveryScopeId(scopeId);
|
|
769
|
+
if (nextScopeId === this.deliveryScopeId) {
|
|
770
|
+
return;
|
|
601
771
|
}
|
|
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
|
-
}
|
|
772
|
+
const currentUpstream = this.upstreamOutbox;
|
|
773
|
+
const currentCursorStore = this.downstreamCursorStore;
|
|
774
|
+
const nextUpstream = DurableUpstreamOutboxStore.forProjectPath(this.projectPath, nextScopeId);
|
|
775
|
+
const nextCursorStore = DownstreamCursorStore.forProjectPath(this.projectPath, nextScopeId);
|
|
776
|
+
for (const entry of currentUpstream.load()) {
|
|
777
|
+
nextUpstream.upsert({
|
|
778
|
+
stableId: entry.stableId,
|
|
779
|
+
eventType: entry.eventType,
|
|
780
|
+
payload: entry.payload,
|
|
781
|
+
});
|
|
782
|
+
currentUpstream.remove(entry.stableId);
|
|
616
783
|
}
|
|
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
|
-
}
|
|
784
|
+
const currentCursor = currentCursorStore.get(this.agentHost);
|
|
785
|
+
if (currentCursor && !nextCursorStore.get(this.agentHost)) {
|
|
786
|
+
nextCursorStore.advance(this.agentHost, currentCursor);
|
|
629
787
|
}
|
|
630
|
-
|
|
788
|
+
this.upstreamOutbox = nextUpstream;
|
|
789
|
+
this.downstreamCursorStore = nextCursorStore;
|
|
790
|
+
this.deliveryScopeId = nextScopeId;
|
|
791
|
+
void this.requestDurableOutboxFlush(true);
|
|
792
|
+
}
|
|
793
|
+
shouldAutoFlushDurableOutbox() {
|
|
794
|
+
return this.deliveryScopeId.startsWith('task:') || this.deliveryScopeId.startsWith('session:');
|
|
631
795
|
}
|
|
632
796
|
resolveHostname() {
|
|
633
797
|
const records = this.sessionStore.load();
|
|
@@ -678,6 +842,8 @@ function formatMessagesResponse(messages) {
|
|
|
678
842
|
role: msg.role,
|
|
679
843
|
content: msg.content,
|
|
680
844
|
ack_token: msg.ackToken,
|
|
845
|
+
metadata: msg.metadata ?? undefined,
|
|
846
|
+
attachments: msg.attachments?.length ? msg.attachments : undefined,
|
|
681
847
|
created_at: msg.createdAt.toISOString(),
|
|
682
848
|
})),
|
|
683
849
|
next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
|
|
@@ -706,3 +872,7 @@ function resolveAgentHost(env, explicit) {
|
|
|
706
872
|
const host = env.HOSTNAME || env.COMPUTERNAME || 'unknown-host';
|
|
707
873
|
return `conductor-fire-${host}-${pid}`;
|
|
708
874
|
}
|
|
875
|
+
function normalizeDeliveryScopeId(scopeId) {
|
|
876
|
+
const normalized = String(scopeId || '').trim();
|
|
877
|
+
return normalized || 'default';
|
|
878
|
+
}
|